mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
test
This commit is contained in:
parent
e9d9101526
commit
e14b00fc13
3 changed files with 252 additions and 124 deletions
|
|
@ -75,11 +75,11 @@ extension JSController {
|
|||
let filename = "\(sanitizedTitle)_\(downloadID.uuidString.prefix(8)).mp4"
|
||||
let destinationURL = downloadDirectory.appendingPathComponent(filename)
|
||||
|
||||
// Create an active download object
|
||||
let activeDownload = JSActiveDownload(
|
||||
// Create an active download object with proper initial status
|
||||
var activeDownload = JSActiveDownload(
|
||||
id: downloadID,
|
||||
originalURL: url,
|
||||
task: nil,
|
||||
task: nil, // Will be set after task creation
|
||||
queueStatus: .downloading,
|
||||
type: downloadType,
|
||||
metadata: metadata,
|
||||
|
|
@ -89,123 +89,27 @@ extension JSController {
|
|||
headers: headers
|
||||
)
|
||||
|
||||
// Add to active downloads
|
||||
activeDownloads.append(activeDownload)
|
||||
|
||||
// Create request with headers
|
||||
var request = URLRequest(url: url)
|
||||
request.timeoutInterval = 30.0
|
||||
for (key, value) in headers {
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// Enhanced session configuration
|
||||
let sessionConfig = URLSessionConfiguration.default
|
||||
// Enhanced session configuration for background downloads
|
||||
let sessionConfig = URLSessionConfiguration.background(withIdentifier: "mp4-download-\(downloadID.uuidString)")
|
||||
sessionConfig.timeoutIntervalForRequest = 60.0
|
||||
sessionConfig.timeoutIntervalForResource = 1800.0
|
||||
sessionConfig.timeoutIntervalForResource = 3600.0 // 1 hour for large files
|
||||
sessionConfig.httpMaximumConnectionsPerHost = 1
|
||||
sessionConfig.allowsCellularAccess = true
|
||||
sessionConfig.shouldUseExtendedBackgroundIdleMode = true
|
||||
sessionConfig.waitsForConnectivity = true
|
||||
|
||||
// Create custom session with delegate
|
||||
let customSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
|
||||
let customSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: .main)
|
||||
|
||||
// Create the download task
|
||||
let downloadTask = customSession.downloadTask(with: request) { [weak self] (tempURL, response, error) in
|
||||
guard let self = self else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
defer {
|
||||
// Clean up resources
|
||||
self.cleanupDownloadResources(for: downloadID)
|
||||
}
|
||||
|
||||
// Handle error cases - just remove from active downloads
|
||||
if let error = error {
|
||||
print("MP4 Download Error: \(error.localizedDescription)")
|
||||
self.removeActiveDownload(downloadID: downloadID)
|
||||
completionHandler?(false, "Download failed: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate response
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
print("MP4 Download: Invalid response")
|
||||
self.removeActiveDownload(downloadID: downloadID)
|
||||
completionHandler?(false, "Invalid server response")
|
||||
return
|
||||
}
|
||||
|
||||
guard (200...299).contains(httpResponse.statusCode) else {
|
||||
print("MP4 Download HTTP Error: \(httpResponse.statusCode)")
|
||||
self.removeActiveDownload(downloadID: downloadID)
|
||||
completionHandler?(false, "Server error: \(httpResponse.statusCode)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
print("MP4 Download: No temporary file URL")
|
||||
self.removeActiveDownload(downloadID: downloadID)
|
||||
completionHandler?(false, "Download data not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Move file to final destination
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
|
||||
try FileManager.default.moveItem(at: tempURL, to: destinationURL)
|
||||
print("MP4 Download: Successfully saved to \(destinationURL.path)")
|
||||
|
||||
// Verify file size
|
||||
let fileSize = try FileManager.default.attributesOfItem(atPath: destinationURL.path)[.size] as? Int64 ?? 0
|
||||
guard fileSize > 0 else {
|
||||
throw NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Downloaded file is empty"])
|
||||
}
|
||||
|
||||
// Create downloaded asset
|
||||
let downloadedAsset = DownloadedAsset(
|
||||
name: title ?? url.lastPathComponent,
|
||||
downloadDate: Date(),
|
||||
originalURL: url,
|
||||
localURL: destinationURL,
|
||||
type: downloadType,
|
||||
metadata: metadata,
|
||||
subtitleURL: subtitleURL
|
||||
)
|
||||
|
||||
// Save asset
|
||||
self.savedAssets.append(downloadedAsset)
|
||||
self.saveAssets()
|
||||
|
||||
// Update progress to complete and remove after delay
|
||||
self.updateDownloadProgress(downloadID: downloadID, progress: 1.0)
|
||||
|
||||
// Download subtitle if provided
|
||||
if let subtitleURL = subtitleURL {
|
||||
self.downloadSubtitle(subtitleURL: subtitleURL, assetID: downloadedAsset.id.uuidString)
|
||||
}
|
||||
|
||||
// Notify completion
|
||||
NotificationCenter.default.post(name: NSNotification.Name("downloadCompleted"), object: downloadedAsset)
|
||||
completionHandler?(true, "Download completed successfully")
|
||||
|
||||
// Remove from active downloads after success
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
self.removeActiveDownload(downloadID: downloadID)
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("MP4 Download Error saving file: \(error.localizedDescription)")
|
||||
self.removeActiveDownload(downloadID: downloadID)
|
||||
completionHandler?(false, "Error saving download: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
let downloadTask = customSession.downloadTask(with: request)
|
||||
|
||||
// Set up progress observation
|
||||
setupProgressObservation(for: downloadTask, downloadID: downloadID)
|
||||
// Update active download with the task
|
||||
activeDownload.task = downloadTask
|
||||
|
||||
// Add to active downloads and create task mapping
|
||||
activeDownloads.append(activeDownload)
|
||||
activeDownloadMap[downloadTask] = downloadID
|
||||
|
||||
// Store session reference
|
||||
storeSessionReference(session: customSession, for: downloadID)
|
||||
|
|
@ -214,6 +118,18 @@ extension JSController {
|
|||
downloadTask.resume()
|
||||
print("MP4 Download: Task started for \(filename)")
|
||||
|
||||
// Post initial status notification
|
||||
postDownloadNotification(.statusChange)
|
||||
|
||||
// If this is an episode, post initial progress update
|
||||
if let episodeNumber = metadata?.episode {
|
||||
postDownloadNotification(.progress, userInfo: [
|
||||
"episodeNumber": episodeNumber,
|
||||
"progress": 0.0,
|
||||
"status": "downloading"
|
||||
])
|
||||
}
|
||||
|
||||
// Initial success callback
|
||||
completionHandler?(true, "Download started")
|
||||
}
|
||||
|
|
@ -221,12 +137,47 @@ extension JSController {
|
|||
// MARK: - Helper Methods
|
||||
|
||||
private func removeActiveDownload(downloadID: UUID) {
|
||||
activeDownloads.removeAll { $0.id == downloadID }
|
||||
// Find and remove the download
|
||||
if let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) {
|
||||
let download = activeDownloads[index]
|
||||
activeDownloads.remove(at: index)
|
||||
|
||||
// Clean up task mapping
|
||||
if let task = download.task {
|
||||
activeDownloadMap.removeValue(forKey: task)
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
cleanupDownloadResources(for: downloadID)
|
||||
|
||||
// Post status change notification
|
||||
postDownloadNotification(.statusChange)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateDownloadProgress(downloadID: UUID, progress: Double) {
|
||||
guard let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) else { return }
|
||||
activeDownloads[index].progress = progress
|
||||
|
||||
let previousProgress = activeDownloads[index].progress
|
||||
activeDownloads[index].progress = min(max(progress, 0.0), 1.0)
|
||||
|
||||
// Only post notifications for meaningful progress changes (every 1% or completion)
|
||||
let progressDifference = progress - previousProgress
|
||||
if progressDifference >= 0.01 || progress >= 1.0 || previousProgress == 0.0 {
|
||||
// Post general progress notification
|
||||
postDownloadNotification(.progress)
|
||||
|
||||
// Post detailed episode progress if applicable
|
||||
if let download = activeDownloads.first(where: { $0.id == downloadID }),
|
||||
let episodeNumber = download.metadata?.episode {
|
||||
let status = progress >= 1.0 ? "completed" : "downloading"
|
||||
postDownloadNotification(.progress, userInfo: [
|
||||
"episodeNumber": episodeNumber,
|
||||
"progress": progress,
|
||||
"status": status
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupProgressObservation(for task: URLSessionDownloadTask, downloadID: UUID) {
|
||||
|
|
@ -253,10 +204,126 @@ extension JSController {
|
|||
|
||||
private func cleanupDownloadResources(for downloadID: UUID) {
|
||||
mp4ProgressObservations?[downloadID] = nil
|
||||
mp4CustomSessions?[downloadID]?.invalidateAndCancel()
|
||||
mp4CustomSessions?[downloadID] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URLSessionDownloadDelegate for MP4 Downloads
|
||||
extension JSController: URLSessionDownloadDelegate {
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
// Check if this is an MP4 download by checking if we have a custom session for it
|
||||
guard let downloadID = activeDownloadMap[downloadTask] else {
|
||||
// If not found in our mapping, it might be an AVAssetDownloadTask
|
||||
// Let the existing AVAssetDownloadDelegate handle it
|
||||
return
|
||||
}
|
||||
|
||||
guard let downloadIndex = activeDownloads.firstIndex(where: { $0.id == downloadID }) else {
|
||||
print("MP4 Download: Couldn't find download for completed task")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this download was cancelled
|
||||
if cancelledDownloadIDs.contains(downloadID) {
|
||||
print("MP4 Download: Ignoring completion for cancelled download")
|
||||
try? FileManager.default.removeItem(at: location)
|
||||
removeActiveDownload(downloadID: downloadID)
|
||||
return
|
||||
}
|
||||
|
||||
let download = activeDownloads[downloadIndex]
|
||||
|
||||
// Move file to final destination
|
||||
guard let downloadDirectory = getPersistentDownloadDirectory() else {
|
||||
print("MP4 Download: Failed to get download directory")
|
||||
removeActiveDownload(downloadID: downloadID)
|
||||
return
|
||||
}
|
||||
|
||||
let sanitizedTitle = download.title?.replacingOccurrences(of: "[^A-Za-z0-9 ._-]", with: "", options: .regularExpression) ?? "download"
|
||||
let filename = "\(sanitizedTitle)_\(downloadID.uuidString.prefix(8)).mp4"
|
||||
let destinationURL = downloadDirectory.appendingPathComponent(filename)
|
||||
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
|
||||
try FileManager.default.moveItem(at: location, to: destinationURL)
|
||||
print("MP4 Download: Successfully saved to \(destinationURL.path)")
|
||||
|
||||
// Verify file size
|
||||
let fileSize = try FileManager.default.attributesOfItem(atPath: destinationURL.path)[.size] as? Int64 ?? 0
|
||||
guard fileSize > 0 else {
|
||||
throw NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Downloaded file is empty"])
|
||||
}
|
||||
|
||||
// Create downloaded asset
|
||||
let downloadedAsset = DownloadedAsset(
|
||||
name: download.title ?? download.originalURL.lastPathComponent,
|
||||
downloadDate: Date(),
|
||||
originalURL: download.originalURL,
|
||||
localURL: destinationURL,
|
||||
type: download.type,
|
||||
metadata: download.metadata,
|
||||
subtitleURL: download.subtitleURL
|
||||
)
|
||||
|
||||
// Save asset
|
||||
savedAssets.append(downloadedAsset)
|
||||
saveAssets()
|
||||
|
||||
// Update progress to complete
|
||||
updateDownloadProgress(downloadID: downloadID, progress: 1.0)
|
||||
|
||||
// Download subtitle if provided
|
||||
if let subtitleURL = download.subtitleURL {
|
||||
downloadSubtitle(subtitleURL: subtitleURL, assetID: downloadedAsset.id.uuidString)
|
||||
}
|
||||
|
||||
// Notify completion
|
||||
postDownloadNotification(.completed)
|
||||
|
||||
// Clean up after a brief delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
self.removeActiveDownload(downloadID: downloadID)
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("MP4 Download Error saving file: \(error.localizedDescription)")
|
||||
removeActiveDownload(downloadID: downloadID)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
// Check if this is one of our MP4 downloads
|
||||
guard let downloadID = activeDownloadMap[downloadTask] else { return }
|
||||
|
||||
// Calculate progress
|
||||
let progress = totalBytesExpectedToWrite > 0 ? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) : 0.0
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateDownloadProgress(downloadID: downloadID, progress: progress)
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
|
||||
// Handle resume for MP4 downloads
|
||||
guard let downloadID = activeDownloadMap[downloadTask] else { return }
|
||||
|
||||
let progress = expectedTotalBytes > 0 ? Double(fileOffset) / Double(expectedTotalBytes) : 0.0
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateDownloadProgress(downloadID: downloadID, progress: progress)
|
||||
self.postDownloadNotification(.statusChange)
|
||||
}
|
||||
|
||||
print("MP4 Download: Resumed at offset \(fileOffset) of \(expectedTotalBytes)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URLSessionDelegate
|
||||
extension JSController: URLSessionDelegate {
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
|
|
|
|||
|
|
@ -1124,9 +1124,32 @@ extension JSController {
|
|||
print("Cancelled active download: \(downloadTitle)")
|
||||
}
|
||||
}
|
||||
|
||||
var mp4CustomSessions: [UUID: URLSession]? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.mp4CustomSessions) as? [UUID: URLSession]
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.mp4CustomSessions, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
var mp4ProgressObservations: [UUID: NSKeyValueObservation]? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &AssociatedKeys.mp4ProgressObservations) as? [UUID: NSKeyValueObservation]
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.mp4ProgressObservations, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVAssetDownloadDelegate
|
||||
private struct AssociatedKeys {
|
||||
static var mp4CustomSessions = "mp4CustomSessions"
|
||||
static var mp4ProgressObservations = "mp4ProgressObservations"
|
||||
}
|
||||
|
||||
|
||||
extension JSController: AVAssetDownloadDelegate {
|
||||
|
||||
/// Called when a download task finishes downloading the asset
|
||||
|
|
@ -1552,4 +1575,4 @@ enum DownloadQueueStatus: Equatable {
|
|||
case downloading
|
||||
/// Download has been completed
|
||||
case completed
|
||||
}
|
||||
}
|
||||
|
|
@ -729,6 +729,7 @@ struct EnhancedActiveDownloadCard: View {
|
|||
let download: JSActiveDownload
|
||||
@State private var currentProgress: Double
|
||||
@State private var taskState: URLSessionTask.State
|
||||
@State private var progressUpdateTimer: Timer?
|
||||
|
||||
init(download: JSActiveDownload) {
|
||||
self.download = download
|
||||
|
|
@ -842,6 +843,17 @@ struct EnhancedActiveDownloadCard: View {
|
|||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in
|
||||
updateProgress()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadStatusChanged"))) { _ in
|
||||
updateStatus()
|
||||
}
|
||||
.onAppear {
|
||||
updateProgress()
|
||||
updateStatus()
|
||||
startProgressTimer()
|
||||
}
|
||||
.onDisappear {
|
||||
stopProgressTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
|
|
@ -849,8 +861,10 @@ struct EnhancedActiveDownloadCard: View {
|
|||
return .orange
|
||||
} else if taskState == .running {
|
||||
return .green
|
||||
} else {
|
||||
} else if taskState == .suspended {
|
||||
return .orange
|
||||
} else {
|
||||
return .red
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -859,30 +873,54 @@ struct EnhancedActiveDownloadCard: View {
|
|||
return "Queued"
|
||||
} else if taskState == .running {
|
||||
return "Downloading"
|
||||
} else {
|
||||
} else if taskState == .suspended {
|
||||
return "Paused"
|
||||
} else {
|
||||
return "Stopped"
|
||||
}
|
||||
}
|
||||
|
||||
private func startProgressTimer() {
|
||||
progressUpdateTimer?.invalidate()
|
||||
progressUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
|
||||
updateProgress()
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopProgressTimer() {
|
||||
progressUpdateTimer?.invalidate()
|
||||
progressUpdateTimer = nil
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
if let currentDownload = JSController.shared.activeDownloads.first(where: { $0.id == download.id }) {
|
||||
withAnimation(.easeInOut(duration: 0.1)) {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
currentProgress = currentDownload.progress
|
||||
}
|
||||
if let task = currentDownload.task {
|
||||
taskState = task.state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStatus() {
|
||||
if let currentDownload = JSController.shared.activeDownloads.first(where: { $0.id == download.id }),
|
||||
let task = currentDownload.task {
|
||||
taskState = task.state
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleDownload() {
|
||||
guard let task = download.task else { return }
|
||||
|
||||
if taskState == .running {
|
||||
download.task?.suspend()
|
||||
task.suspend()
|
||||
taskState = .suspended
|
||||
} else if taskState == .suspended {
|
||||
download.task?.resume()
|
||||
task.resume()
|
||||
taskState = .running
|
||||
}
|
||||
|
||||
// Post status change notification
|
||||
NotificationCenter.default.post(name: NSNotification.Name("downloadStatusChanged"), object: nil)
|
||||
}
|
||||
|
||||
private func cancelDownload() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue