From 5567619e0f64534e4791f0409986ce0165c80e59 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sat, 31 May 2025 22:04:50 +0200 Subject: [PATCH] Revert "test" This reverts commit e14b00fc1390e512fa72574ba0133bd6b35d6bfc. --- .../Downloads/JSController+MP4Download.swift | 295 +++++++----------- .../Downloads/JSController-Downloads.swift | 27 +- Sora/Views/DownloadView.swift | 54 +--- 3 files changed, 124 insertions(+), 252 deletions(-) diff --git a/Sora/Utils/JSLoader/Downloads/JSController+MP4Download.swift b/Sora/Utils/JSLoader/Downloads/JSController+MP4Download.swift index 7c2a960..dacd549 100644 --- a/Sora/Utils/JSLoader/Downloads/JSController+MP4Download.swift +++ b/Sora/Utils/JSLoader/Downloads/JSController+MP4Download.swift @@ -75,11 +75,11 @@ extension JSController { let filename = "\(sanitizedTitle)_\(downloadID.uuidString.prefix(8)).mp4" let destinationURL = downloadDirectory.appendingPathComponent(filename) - // Create an active download object with proper initial status - var activeDownload = JSActiveDownload( + // Create an active download object + let activeDownload = JSActiveDownload( id: downloadID, originalURL: url, - task: nil, // Will be set after task creation + task: nil, queueStatus: .downloading, type: downloadType, metadata: metadata, @@ -89,27 +89,123 @@ extension JSController { headers: headers ) - // Enhanced session configuration for background downloads - let sessionConfig = URLSessionConfiguration.background(withIdentifier: "mp4-download-\(downloadID.uuidString)") + // 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 sessionConfig.timeoutIntervalForRequest = 60.0 - sessionConfig.timeoutIntervalForResource = 3600.0 // 1 hour for large files + sessionConfig.timeoutIntervalForResource = 1800.0 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: .main) + let customSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) // Create the download task - let downloadTask = customSession.downloadTask(with: request) + 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)") + } + } + } - // Update active download with the task - activeDownload.task = downloadTask - - // Add to active downloads and create task mapping - activeDownloads.append(activeDownload) - activeDownloadMap[downloadTask] = downloadID + // Set up progress observation + setupProgressObservation(for: downloadTask, downloadID: downloadID) // Store session reference storeSessionReference(session: customSession, for: downloadID) @@ -118,18 +214,6 @@ 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") } @@ -137,47 +221,12 @@ extension JSController { // MARK: - Helper Methods private func removeActiveDownload(downloadID: UUID) { - // 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) - } + activeDownloads.removeAll { $0.id == downloadID } } private func updateDownloadProgress(downloadID: UUID, progress: Double) { guard let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) else { return } - - 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 - ]) - } - } + activeDownloads[index].progress = progress } private func setupProgressObservation(for task: URLSessionDownloadTask, downloadID: UUID) { @@ -204,126 +253,10 @@ 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) { diff --git a/Sora/Utils/JSLoader/Downloads/JSController-Downloads.swift b/Sora/Utils/JSLoader/Downloads/JSController-Downloads.swift index a5c52a7..a6e9f6b 100644 --- a/Sora/Utils/JSLoader/Downloads/JSController-Downloads.swift +++ b/Sora/Utils/JSLoader/Downloads/JSController-Downloads.swift @@ -1124,32 +1124,9 @@ 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) - } - } } -private struct AssociatedKeys { - static var mp4CustomSessions = "mp4CustomSessions" - static var mp4ProgressObservations = "mp4ProgressObservations" -} - - +// MARK: - AVAssetDownloadDelegate extension JSController: AVAssetDownloadDelegate { /// Called when a download task finishes downloading the asset @@ -1575,4 +1552,4 @@ enum DownloadQueueStatus: Equatable { case downloading /// Download has been completed case completed -} \ No newline at end of file +} \ No newline at end of file diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index c4cd90b..aa38615 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -729,7 +729,6 @@ 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 @@ -843,17 +842,6 @@ 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 { @@ -861,10 +849,8 @@ struct EnhancedActiveDownloadCard: View { return .orange } else if taskState == .running { return .green - } else if taskState == .suspended { - return .orange } else { - return .red + return .orange } } @@ -873,54 +859,30 @@ struct EnhancedActiveDownloadCard: View { return "Queued" } else if taskState == .running { return "Downloading" - } else if taskState == .suspended { - return "Paused" } else { - return "Stopped" + return "Paused" } } - 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.2)) { + withAnimation(.easeInOut(duration: 0.1)) { 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 { - task.suspend() + download.task?.suspend() taskState = .suspended } else if taskState == .suspended { - task.resume() + download.task?.resume() taskState = .running } - - // Post status change notification - NotificationCenter.default.post(name: NSNotification.Name("downloadStatusChanged"), object: nil) } private func cancelDownload() {