mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-05 00:59:55 +00:00
fix
This commit is contained in:
parent
947d7fcc82
commit
6b4fe44fb3
1 changed files with 153 additions and 219 deletions
|
|
@ -44,25 +44,25 @@ extension JSController {
|
|||
|
||||
func initializeDownloadSession() {
|
||||
#if targetEnvironment(simulator)
|
||||
Logger.shared.log("Download Sessions are not available on Simulator", type: "Error")
|
||||
Logger.shared.log("Download Sessions are not available on Simulator", type: "Download")
|
||||
#else
|
||||
Task {
|
||||
let sessionIdentifier = "hls-downloader-\(UUID().uuidString)"
|
||||
|
||||
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: sessionIdentifier)
|
||||
|
||||
|
||||
configuration.allowsCellularAccess = true
|
||||
configuration.shouldUseExtendedBackgroundIdleMode = true
|
||||
configuration.waitsForConnectivity = true
|
||||
|
||||
|
||||
await MainActor.run {
|
||||
self.downloadURLSession = AVAssetDownloadURLSession(
|
||||
configuration: configuration,
|
||||
assetDownloadDelegate: self,
|
||||
delegateQueue: .main
|
||||
)
|
||||
|
||||
print("Download session initialized with ID: \(sessionIdentifier)")
|
||||
|
||||
Logger.shared.log("Download session initialized with ID: \(sessionIdentifier)", type: "Download")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -73,7 +73,7 @@ extension JSController {
|
|||
/// Sets up JavaScript download function if needed
|
||||
func setupDownloadFunction() {
|
||||
// No JavaScript-side setup needed for now
|
||||
print("Download function setup completed")
|
||||
Logger.shared.log("Download function setup completed", type: "Download")
|
||||
}
|
||||
|
||||
/// Helper function to post download notifications with proper naming
|
||||
|
|
@ -177,7 +177,7 @@ extension JSController {
|
|||
subtitleURL: subtitleURL,
|
||||
asset: asset,
|
||||
headers: headers,
|
||||
module: module // Pass the module to store it for queue processing
|
||||
module: module
|
||||
)
|
||||
|
||||
// Add to the download queue
|
||||
|
|
@ -211,7 +211,7 @@ extension JSController {
|
|||
|
||||
// Check if download session is ready before processing queue
|
||||
guard downloadURLSession != nil else {
|
||||
print("Download session not ready, deferring queue processing...")
|
||||
Logger.shared.log("Download session not ready, deferring queue processing...", type: "Download")
|
||||
isProcessingQueue = false
|
||||
// Retry after a delay to allow session initialization
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||
|
|
@ -262,11 +262,11 @@ extension JSController {
|
|||
|
||||
/// Start a previously queued download
|
||||
private func startQueuedDownload(_ queuedDownload: JSActiveDownload) {
|
||||
print("Starting queued download: \(queuedDownload.title ?? queuedDownload.originalURL.lastPathComponent)")
|
||||
Logger.shared.log("Starting queued download: \(queuedDownload.title ?? queuedDownload.originalURL.lastPathComponent)", type: "Download")
|
||||
|
||||
// If we have a module, use the same method as manual downloads (this fixes the bug!)
|
||||
if let module = queuedDownload.module {
|
||||
print("Using downloadWithStreamTypeSupport for queued download (same as manual downloads)")
|
||||
Logger.shared.log("Using downloadWithStreamTypeSupport for queued download (same as manual downloads)", type: "Download")
|
||||
|
||||
// Use the exact same method that manual downloads use
|
||||
downloadWithStreamTypeSupport(
|
||||
|
|
@ -283,9 +283,9 @@ extension JSController {
|
|||
showPosterURL: queuedDownload.metadata?.showPosterURL,
|
||||
completionHandler: { success, message in
|
||||
if success {
|
||||
print("Queued download started successfully via downloadWithStreamTypeSupport")
|
||||
Logger.shared.log("Queued download started successfully via downloadWithStreamTypeSupport", type: "Download")
|
||||
} else {
|
||||
print("Queued download failed: \(message)")
|
||||
Logger.shared.log("Queued download failed: \(message)", type: "Download")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -293,15 +293,15 @@ extension JSController {
|
|||
}
|
||||
|
||||
// Legacy fallback for downloads without module (should rarely be used now)
|
||||
print("Using legacy download method for queued download (no module available)")
|
||||
Logger.shared.log("Using legacy download method for queued download (no module available)", type: "Download")
|
||||
|
||||
guard let asset = queuedDownload.asset else {
|
||||
print("Missing asset for queued download")
|
||||
Logger.shared.log("Missing asset for queued download", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
guard let downloadSession = downloadURLSession else {
|
||||
print("Download session not yet initialized, retrying in background...")
|
||||
Logger.shared.log("Download session not yet initialized, retrying in background...", type: "Download")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.startQueuedDownload(queuedDownload)
|
||||
}
|
||||
|
|
@ -314,7 +314,7 @@ extension JSController {
|
|||
assetArtworkData: nil,
|
||||
options: nil
|
||||
) else {
|
||||
print("Failed to create download task for queued download")
|
||||
Logger.shared.log("Failed to create download task for queued download", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -345,7 +345,7 @@ extension JSController {
|
|||
|
||||
// Start the download
|
||||
task.resume()
|
||||
print("Queued download started: \(download.title ?? download.originalURL.lastPathComponent)")
|
||||
Logger.shared.log("Queued download started: \(download.title ?? download.originalURL.lastPathComponent)", type: "Download")
|
||||
|
||||
// Save the download state
|
||||
saveDownloadState()
|
||||
|
|
@ -406,8 +406,7 @@ extension JSController {
|
|||
|
||||
saveDownloadState()
|
||||
|
||||
print("Cleaned up download task")
|
||||
|
||||
Logger.shared.log("Cleaned up download task", type: "Download")
|
||||
// Start processing the queue again if we have pending downloads
|
||||
if !downloadQueue.isEmpty && !isProcessingQueue {
|
||||
processDownloadQueue()
|
||||
|
|
@ -458,11 +457,11 @@ extension JSController {
|
|||
/// - subtitleURL: The URL of the subtitle file to download
|
||||
/// - assetID: The ID of the asset this subtitle is associated with
|
||||
func downloadSubtitle(subtitleURL: URL, assetID: String) {
|
||||
print("Downloading subtitle from: \(subtitleURL.absoluteString) for asset ID: \(assetID)")
|
||||
Logger.shared.log("Downloading subtitle from: \(subtitleURL.absoluteString) for asset ID: \(assetID)", type: "Download")
|
||||
|
||||
// Check if this asset belongs to a cancelled download - if so, don't download subtitle
|
||||
if let assetUUID = UUID(uuidString: assetID), cancelledDownloadIDs.contains(assetUUID) {
|
||||
print("Skipping subtitle download for cancelled download: \(assetID)")
|
||||
Logger.shared.log("Skipping subtitle download for cancelled download: \(assetID)", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -482,34 +481,34 @@ extension JSController {
|
|||
request.addValue(referer, forHTTPHeaderField: "Origin")
|
||||
}
|
||||
|
||||
print("Subtitle download request headers: \(request.allHTTPHeaderFields ?? [:])")
|
||||
Logger.shared.log("Subtitle download request headers: \(request.allHTTPHeaderFields ?? [:])", type: "Download")
|
||||
|
||||
// Create a task to download the subtitle file
|
||||
let task = session.downloadTask(with: request) { [weak self] (tempURL, response, error) in
|
||||
guard let self = self else {
|
||||
print("Self reference lost during subtitle download")
|
||||
Logger.shared.log("Self reference lost during subtitle download", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
print("Subtitle download error: \(error.localizedDescription)")
|
||||
Logger.shared.log("Subtitle download error: \(error.localizedDescription)", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
print("No temporary URL received for subtitle download")
|
||||
Logger.shared.log("No temporary URL received for subtitle download", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
guard let downloadDir = self.getPersistentDownloadDirectory() else {
|
||||
print("Failed to get persistent download directory for subtitle")
|
||||
Logger.shared.log("Failed to get persistent download directory for subtitle", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
// Log response details for debugging
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
print("Subtitle download HTTP status: \(httpResponse.statusCode)")
|
||||
print("Subtitle download content type: \(httpResponse.mimeType ?? "unknown")")
|
||||
Logger.shared.log("Subtitle download HTTP status: \(httpResponse.statusCode)", type: "Download")
|
||||
Logger.shared.log("Subtitle download content type: \(httpResponse.mimeType ?? "unknown")", type: "Download")
|
||||
}
|
||||
|
||||
// Try to read content to validate it's actually a subtitle file
|
||||
|
|
@ -518,19 +517,19 @@ extension JSController {
|
|||
let subtitleContent = String(data: subtitleData, encoding: .utf8) ?? ""
|
||||
|
||||
if subtitleContent.isEmpty {
|
||||
print("Warning: Subtitle file appears to be empty")
|
||||
Logger.shared.log("Warning: Subtitle file appears to be empty", type: "Download")
|
||||
} else {
|
||||
print("Subtitle file contains \(subtitleData.count) bytes of data")
|
||||
Logger.shared.log("Subtitle file contains \(subtitleData.count) bytes of data", type: "Download")
|
||||
if subtitleContent.hasPrefix("WEBVTT") {
|
||||
print("Valid WebVTT subtitle detected")
|
||||
Logger.shared.log("Valid WebVTT subtitle detected", type: "Download")
|
||||
} else if subtitleContent.contains(" --> ") {
|
||||
print("Subtitle file contains timing markers")
|
||||
Logger.shared.log("Subtitle file contains timing markers", type: "Download")
|
||||
} else {
|
||||
print("Warning: Subtitle content doesn't appear to be in a recognized format")
|
||||
Logger.shared.log("Warning: Subtitle content doesn't appear to be in a recognized format", type: "Download")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error reading subtitle content for validation: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error reading subtitle content for validation: \(error.localizedDescription)", type: "Download")
|
||||
}
|
||||
|
||||
// Determine file extension based on the content type or URL
|
||||
|
|
@ -557,18 +556,16 @@ extension JSController {
|
|||
// If file already exists, remove it
|
||||
if FileManager.default.fileExists(atPath: localURL.path) {
|
||||
try FileManager.default.removeItem(at: localURL)
|
||||
print("Removed existing subtitle file at \(localURL.path)")
|
||||
Logger.shared.log("Removed existing subtitle file at \(localURL.path)", type: "Download")
|
||||
}
|
||||
|
||||
// Move the downloaded file to the persistent location
|
||||
try FileManager.default.moveItem(at: tempURL, to: localURL)
|
||||
|
||||
// Update the asset with the subtitle URL
|
||||
self.updateAssetWithSubtitle(assetID: assetID,
|
||||
subtitleURL: subtitleURL,
|
||||
localSubtitleURL: localURL)
|
||||
self.updateAssetWithSubtitle(assetID: assetID, subtitleURL: subtitleURL, localSubtitleURL: localURL)
|
||||
|
||||
print("Subtitle downloaded successfully: \(localURL.path)")
|
||||
Logger.shared.log("Subtitle downloaded successfully: \(localURL.path)", type: "Download")
|
||||
|
||||
// Show success notification
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -594,12 +591,12 @@ extension JSController {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error moving subtitle file: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error moving subtitle file: \(error.localizedDescription)", type: "Download")
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
print("Subtitle download task started")
|
||||
Logger.shared.log("Subtitle download task started", type: "Download")
|
||||
}
|
||||
|
||||
/// Updates an asset with subtitle information after subtitle download completes
|
||||
|
|
@ -620,7 +617,7 @@ extension JSController {
|
|||
localURL: existingAsset.localURL,
|
||||
type: existingAsset.type,
|
||||
metadata: existingAsset.metadata,
|
||||
subtitleURL: subtitleURL,
|
||||
subtitleURL: existingAsset.subtitleURL,
|
||||
localSubtitleURL: localSubtitleURL
|
||||
)
|
||||
|
||||
|
|
@ -661,7 +658,7 @@ extension JSController {
|
|||
do {
|
||||
if !fileManager.fileExists(atPath: persistentDir.path) {
|
||||
try fileManager.createDirectory(at: persistentDir, withIntermediateDirectories: true)
|
||||
print("Created persistent download directory at \(persistentDir.path)")
|
||||
Logger.shared.log("Created persistent download directory at \(persistentDir.path)", type: "Download")
|
||||
}
|
||||
|
||||
// Find any video files (.movpkg, .mp4) in the Documents directory
|
||||
|
|
@ -669,7 +666,7 @@ extension JSController {
|
|||
let videoFiles = files.filter { ["movpkg", "mp4"].contains($0.pathExtension.lowercased()) }
|
||||
|
||||
if !videoFiles.isEmpty {
|
||||
print("Found \(videoFiles.count) video files in Documents directory to migrate")
|
||||
Logger.shared.log("Found \(videoFiles.count) video files in Documents directory to migrate", type: "Download")
|
||||
|
||||
// Migrate each file
|
||||
for fileURL in videoFiles {
|
||||
|
|
@ -682,18 +679,18 @@ extension JSController {
|
|||
let uniqueID = UUID().uuidString
|
||||
let newDestinationURL = persistentDir.appendingPathComponent("\(filename)-\(uniqueID)")
|
||||
try fileManager.copyItem(at: fileURL, to: newDestinationURL)
|
||||
print("Migrated file with unique name: \(filename) → \(newDestinationURL.lastPathComponent)")
|
||||
Logger.shared.log("Migrated file with unique name: \(filename) → \(newDestinationURL.lastPathComponent)", type: "Download")
|
||||
} else {
|
||||
// Move the file to the persistent directory
|
||||
try fileManager.copyItem(at: fileURL, to: destinationURL)
|
||||
print("Migrated file: \(filename)")
|
||||
Logger.shared.log("Migrated file: \(filename)", type: "Download")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("No video files found in Documents directory for migration")
|
||||
Logger.shared.log("No video files found in Documents directory for migration", type: "Download")
|
||||
}
|
||||
} catch {
|
||||
print("Error during migration: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error during migration: \(error.localizedDescription)", type: "Download")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -710,12 +707,12 @@ extension JSController {
|
|||
|
||||
// Check if the video file exists at the stored path
|
||||
if !fileManager.fileExists(atPath: asset.localURL.path) {
|
||||
print("Asset file not found at saved path: \(asset.localURL.path)")
|
||||
Logger.shared.log("Asset file not found at saved path: \(asset.localURL.path)", type: "Download")
|
||||
|
||||
// Try to find the file in the persistent directory
|
||||
if let persistentURL = findAssetInPersistentStorage(assetName: asset.name) {
|
||||
// Update the asset with the new video URL
|
||||
print("Found asset in persistent storage: \(persistentURL.path)")
|
||||
Logger.shared.log("Found asset in persistent storage: \(persistentURL.path)", type: "Download")
|
||||
updatedAsset = DownloadedAsset(
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
|
|
@ -730,7 +727,7 @@ extension JSController {
|
|||
needsUpdate = true
|
||||
} else {
|
||||
// If we can't find the video file, mark it for removal
|
||||
print("Asset not found in persistent storage. Marking for removal: \(asset.name)")
|
||||
Logger.shared.log("Asset not found in persistent storage. Marking for removal: \(asset.name)", type: "Download")
|
||||
assetsToRemove.append(asset.id)
|
||||
updatedAssets = true
|
||||
continue // Skip subtitle validation for assets being removed
|
||||
|
|
@ -740,11 +737,11 @@ extension JSController {
|
|||
// Check if the subtitle file exists (if one is expected)
|
||||
if let localSubtitleURL = updatedAsset.localSubtitleURL {
|
||||
if !fileManager.fileExists(atPath: localSubtitleURL.path) {
|
||||
print("Subtitle file not found at saved path: \(localSubtitleURL.path)")
|
||||
Logger.shared.log("Subtitle file not found at saved path: \(localSubtitleURL.path)", type: "Download")
|
||||
|
||||
// Try to find the subtitle file in the persistent directory
|
||||
if let foundSubtitleURL = findSubtitleInPersistentStorage(assetID: updatedAsset.id.uuidString) {
|
||||
print("Found subtitle file in persistent storage: \(foundSubtitleURL.path)")
|
||||
Logger.shared.log("Found subtitle file in persistent storage: \(foundSubtitleURL.path)", type: "Download")
|
||||
updatedAsset = DownloadedAsset(
|
||||
id: updatedAsset.id,
|
||||
name: updatedAsset.name,
|
||||
|
|
@ -759,7 +756,7 @@ extension JSController {
|
|||
needsUpdate = true
|
||||
} else {
|
||||
// Subtitle file is missing - remove the subtitle reference but keep the video
|
||||
print("Subtitle file not found in persistent storage for asset: \(updatedAsset.name)")
|
||||
Logger.shared.log("Subtitle file not found in persistent storage for asset: \(updatedAsset.name)", type: "Download")
|
||||
updatedAsset = DownloadedAsset(
|
||||
id: updatedAsset.id,
|
||||
name: updatedAsset.name,
|
||||
|
|
@ -780,7 +777,7 @@ extension JSController {
|
|||
if needsUpdate {
|
||||
savedAssets[index] = updatedAsset
|
||||
updatedAssets = true
|
||||
print("Updated asset paths for: \(updatedAsset.name)")
|
||||
Logger.shared.log("Updated asset paths for: \(updatedAsset.name)", type: "Download")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -788,7 +785,7 @@ extension JSController {
|
|||
if !assetsToRemove.isEmpty {
|
||||
let countBefore = savedAssets.count
|
||||
savedAssets.removeAll { assetsToRemove.contains($0.id) }
|
||||
print("Removed \(countBefore - savedAssets.count) missing assets from the library")
|
||||
Logger.shared.log("Removed \(countBefore - savedAssets.count) missing assets from the library", type: "Download")
|
||||
|
||||
// Notify observers of the change (library cleanup requires cache clearing)
|
||||
postDownloadNotification(.cleanup)
|
||||
|
|
@ -797,7 +794,7 @@ extension JSController {
|
|||
// Save the updated asset information if changes were made
|
||||
if updatedAssets {
|
||||
saveAssets()
|
||||
print("Asset validation complete. Updated \(updatedAssets ? "some" : "no") asset paths.")
|
||||
Logger.shared.log("Asset validation complete. Updated \(updatedAssets ? "some" : "no") asset paths.", type: "Download")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -834,7 +831,7 @@ extension JSController {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error searching for asset in persistent storage: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error searching for asset in persistent storage: \(error.localizedDescription)", type: "Download")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -848,7 +845,7 @@ extension JSController {
|
|||
|
||||
// Get Application Support directory
|
||||
guard let appSupportDir = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
print("Cannot access Application Support directory for subtitle search")
|
||||
Logger.shared.log("Cannot access Application Support directory for subtitle search", type: "Download")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -857,7 +854,7 @@ extension JSController {
|
|||
|
||||
// Check if directory exists
|
||||
guard fileManager.fileExists(atPath: downloadDir.path) else {
|
||||
print("Download directory does not exist for subtitle search")
|
||||
Logger.shared.log("Download directory does not exist for subtitle search", type: "Download")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -876,14 +873,14 @@ extension JSController {
|
|||
// Check if this is a subtitle file with the correct naming pattern
|
||||
if subtitleExtensions.contains(fileExtension) &&
|
||||
filename.hasPrefix("subtitle-\(assetID).") {
|
||||
print("Found subtitle file for asset \(assetID): \(filename)")
|
||||
Logger.shared.log("Found subtitle file for asset \(assetID): \(filename)", type: "Download")
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
||||
print("No subtitle file found for asset ID: \(assetID)")
|
||||
Logger.shared.log("No subtitle file found for asset ID: \(assetID)", type: "Download")
|
||||
} catch {
|
||||
print("Error searching for subtitle in persistent storage: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error searching for subtitle in persistent storage: \(error.localizedDescription)", type: "Download")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -892,7 +889,7 @@ extension JSController {
|
|||
/// Save assets to UserDefaults
|
||||
func saveAssets() {
|
||||
DownloadPersistence.save(savedAssets)
|
||||
print("Saved \(savedAssets.count) assets to persistence")
|
||||
Logger.shared.log("Saved \(savedAssets.count) assets to persistence", type: "Download")
|
||||
}
|
||||
|
||||
/// Save the current state of downloads
|
||||
|
|
@ -908,7 +905,7 @@ extension JSController {
|
|||
}
|
||||
|
||||
UserDefaults.standard.set(downloadInfo, forKey: "activeDownloads")
|
||||
print("Saved download state with \(downloadInfo.count) active downloads")
|
||||
Logger.shared.log("Saved download state with \(downloadInfo.count) active downloads", type: "Download")
|
||||
}
|
||||
|
||||
/// Delete an asset
|
||||
|
|
@ -945,7 +942,7 @@ extension JSController {
|
|||
func removeAssetFromLibrary(_ asset: DownloadedAsset) {
|
||||
// Only remove the entry from savedAssets
|
||||
DownloadPersistence.delete(id: asset.id)
|
||||
print("Removed asset from library (file preserved): \(asset.name)")
|
||||
Logger.shared.log("Removed asset from library (file preserved): \(asset.name)", type: "Download")
|
||||
|
||||
// Notify observers that the library changed (cache clearing needed)
|
||||
postDownloadNotification(.libraryChange)
|
||||
|
|
@ -957,7 +954,7 @@ extension JSController {
|
|||
|
||||
// Get Application Support directory
|
||||
guard let appSupportDir = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
print("Cannot access Application Support directory")
|
||||
Logger.shared.log("Cannot access Application Support directory", type: "Download")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -967,11 +964,11 @@ extension JSController {
|
|||
do {
|
||||
if !fileManager.fileExists(atPath: downloadDir.path) {
|
||||
try fileManager.createDirectory(at: downloadDir, withIntermediateDirectories: true)
|
||||
print("Created persistent download directory at \(downloadDir.path)")
|
||||
Logger.shared.log("Created persistent download directory at \(downloadDir.path)", type: "Download")
|
||||
}
|
||||
return downloadDir
|
||||
} catch {
|
||||
print("Error creating download directory: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error creating download directory: \(error.localizedDescription)", type: "Download")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1103,7 +1100,7 @@ extension JSController {
|
|||
// Notify of the cancellation
|
||||
postDownloadNotification(.statusChange)
|
||||
|
||||
print("Cancelled queued download: \(downloadID)")
|
||||
Logger.shared.log("Cancelled queued download: \(downloadID)", type: "Download")
|
||||
}
|
||||
|
||||
/// Cancel an active download that is currently in progress
|
||||
|
|
@ -1126,25 +1123,25 @@ extension JSController {
|
|||
// Show notification
|
||||
DropManager.shared.info("Download cancelled: \(downloadTitle)")
|
||||
|
||||
print("Cancelled active download: \(downloadTitle)")
|
||||
Logger.shared.log("Cancelled active download: \(downloadTitle)", type: "Download")
|
||||
}
|
||||
}
|
||||
|
||||
/// Pause an MP4 download
|
||||
func pauseMP4Download(_ downloadID: UUID) {
|
||||
guard let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) else {
|
||||
print("MP4 Download not found for pausing: \(downloadID)")
|
||||
Logger.shared.log("MP4 Download not found for pausing: \(downloadID)", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
let download = activeDownloads[index]
|
||||
guard let urlTask = download.urlSessionTask else {
|
||||
print("No URL session task found for MP4 download: \(downloadID)")
|
||||
Logger.shared.log("No URL session task found for MP4 download: \(downloadID)", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
urlTask.suspend()
|
||||
print("Paused MP4 download: \(download.title ?? download.originalURL.lastPathComponent)")
|
||||
Logger.shared.log("Paused MP4 download: \(download.title ?? download.originalURL.lastPathComponent)", type: "Download")
|
||||
|
||||
// Notify UI of status change
|
||||
postDownloadNotification(.statusChange)
|
||||
|
|
@ -1153,18 +1150,18 @@ extension JSController {
|
|||
/// Resume an MP4 download
|
||||
func resumeMP4Download(_ downloadID: UUID) {
|
||||
guard let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) else {
|
||||
print("MP4 Download not found for resuming: \(downloadID)")
|
||||
Logger.shared.log("MP4 Download not found for resuming: \(downloadID)", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
let download = activeDownloads[index]
|
||||
guard let urlTask = download.urlSessionTask else {
|
||||
print("No URL session task found for MP4 download: \(downloadID)")
|
||||
Logger.shared.log("No URL session task found for MP4 download: \(downloadID)", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
urlTask.resume()
|
||||
print("Resumed MP4 download: \(download.title ?? download.originalURL.lastPathComponent)")
|
||||
Logger.shared.log("Resumed MP4 download: \(download.title ?? download.originalURL.lastPathComponent)", type: "Download")
|
||||
|
||||
// Notify UI of status change
|
||||
postDownloadNotification(.statusChange)
|
||||
|
|
@ -1178,13 +1175,13 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
guard let downloadID = activeDownloadMap[assetDownloadTask],
|
||||
let downloadIndex = activeDownloads.firstIndex(where: { $0.id == downloadID }) else {
|
||||
print("Download task finished but couldn't find associated download")
|
||||
Logger.shared.log("Download task finished but couldn't find associated download", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this download was cancelled - if so, don't process completion
|
||||
if cancelledDownloadIDs.contains(downloadID) {
|
||||
print("Ignoring completion for cancelled download: \(downloadID)")
|
||||
Logger.shared.log("Ignoring completion for cancelled download: \(downloadID)", type: "Download")
|
||||
// Delete any temporary files that may have been created
|
||||
try? FileManager.default.removeItem(at: location)
|
||||
return
|
||||
|
|
@ -1194,7 +1191,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
|
||||
// Move the downloaded file to Application Support directory for persistence
|
||||
guard let persistentURL = moveToApplicationSupportDirectory(from: location, filename: download.title ?? download.originalURL.lastPathComponent, originalURL: download.originalURL) else {
|
||||
print("Failed to move downloaded file to persistent storage")
|
||||
Logger.shared.log("Failed to move downloaded file to persistent storage", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1221,9 +1218,9 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
if download.metadata?.episode != nil && download.type == .episode {
|
||||
fetchSkipTimestampsFor(request: download, persistentURL: persistentURL) { ok in
|
||||
if ok {
|
||||
print("[SkipSidecar] Saved OP/ED sidecar for episode \(download.metadata?.episode ?? -1) at: \(persistentURL.path)")
|
||||
Logger.shared.log("[SkipSidecar] Saved OP/ED sidecar for episode \(download.metadata?.episode ?? -1) at: \(persistentURL.path)", type: "Download")
|
||||
} else {
|
||||
print("[SkipSidecar] Failed to save sidecar for episode \(download.metadata?.episode ?? -1)")
|
||||
Logger.shared.log("[SkipSidecar] Failed to save sidecar for episode \(download.metadata?.episode ?? -1)", type: "Download")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1248,7 +1245,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
// Clean up the download task
|
||||
cleanupDownloadTask(assetDownloadTask)
|
||||
|
||||
print("Download completed and moved to persistent storage: \(newAsset.name)")
|
||||
Logger.shared.log("Download completed and moved to persistent storage: \(newAsset.name)", type: "Download")
|
||||
}
|
||||
|
||||
/// Moves a downloaded file to Application Support directory to preserve it across app updates
|
||||
|
|
@ -1262,7 +1259,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
|
||||
// Get Application Support directory
|
||||
guard let appSupportDir = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
print("Cannot access Application Support directory")
|
||||
Logger.shared.log("Cannot access Application Support directory", type: "Download")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1272,7 +1269,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
do {
|
||||
if !fileManager.fileExists(atPath: downloadDir.path) {
|
||||
try fileManager.createDirectory(at: downloadDir, withIntermediateDirectories: true)
|
||||
print("Created persistent download directory at \(downloadDir.path)")
|
||||
Logger.shared.log("Created persistent download directory at \(downloadDir.path)", type: "Download")
|
||||
}
|
||||
|
||||
// Generate unique filename with UUID to avoid conflicts
|
||||
|
|
@ -1290,34 +1287,34 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
if originalURLString.contains(".m3u8") || originalURLString.contains("/hls/") || originalURLString.contains("m3u8") {
|
||||
// This was an HLS stream, keep as .movpkg
|
||||
fileExtension = "movpkg"
|
||||
print("Using .movpkg extension for HLS download: \(safeFilename)")
|
||||
Logger.shared.log("Using .movpkg extension for HLS download: \(safeFilename)", type: "Download")
|
||||
} else if originalPathExtension == "mp4" || originalURLString.contains(".mp4") || originalURLString.contains("download") {
|
||||
// This was a direct MP4 download, use .mp4 extension regardless of what AVAssetDownloadTask created
|
||||
fileExtension = "mp4"
|
||||
print("Using .mp4 extension for direct MP4 download: \(safeFilename)")
|
||||
Logger.shared.log("Using .mp4 extension for direct MP4 download: \(safeFilename)", type: "Download")
|
||||
} else {
|
||||
// Fallback: check the downloaded file extension
|
||||
let sourceExtension = location.pathExtension.lowercased()
|
||||
if sourceExtension == "movpkg" && originalURLString.contains("m3u8") {
|
||||
fileExtension = "movpkg"
|
||||
print("Using .movpkg extension for HLS stream: \(safeFilename)")
|
||||
Logger.shared.log("Using .movpkg extension for HLS stream: \(safeFilename)", type: "Download")
|
||||
} else {
|
||||
fileExtension = "mp4"
|
||||
print("Using .mp4 extension as fallback: \(safeFilename)")
|
||||
Logger.shared.log("Using .mp4 extension as fallback: \(safeFilename)", type: "Download")
|
||||
}
|
||||
}
|
||||
|
||||
print("Final destination will be: \(safeFilename)-\(uniqueID).\(fileExtension)")
|
||||
Logger.shared.log("Final destination will be: \(safeFilename)-\(uniqueID).\(fileExtension)", type: "Download")
|
||||
|
||||
let destinationURL = downloadDir.appendingPathComponent("\(safeFilename)-\(uniqueID).\(fileExtension)")
|
||||
|
||||
// Move the file to the persistent location
|
||||
try fileManager.moveItem(at: location, to: destinationURL)
|
||||
print("Successfully moved download to persistent storage: \(destinationURL.path)")
|
||||
Logger.shared.log("Successfully moved download to persistent storage: \(destinationURL.path)", type: "Download")
|
||||
|
||||
return destinationURL
|
||||
} catch {
|
||||
print("Error moving download to persistent storage: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error moving download to persistent storage: \(error.localizedDescription)", type: "Download")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1326,37 +1323,37 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if let error = error {
|
||||
// Enhanced error logging
|
||||
print("Download error: \(error.localizedDescription)")
|
||||
Logger.shared.log("Download error: \(error.localizedDescription)", type: "Download")
|
||||
|
||||
// Extract and log the underlying error details
|
||||
let nsError = error as NSError
|
||||
print("Error domain: \(nsError.domain), code: \(nsError.code)")
|
||||
Logger.shared.log("Error domain: \(nsError.domain), code: \(nsError.code)", type: "Download")
|
||||
|
||||
if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError {
|
||||
print("Underlying error: \(underlyingError)")
|
||||
Logger.shared.log("Underlying error: \(underlyingError)", type: "Download")
|
||||
}
|
||||
|
||||
for (key, value) in nsError.userInfo {
|
||||
print("Error info - \(key): \(value)")
|
||||
Logger.shared.log("Error info - \(key): \(value)", type: "Download")
|
||||
}
|
||||
|
||||
// Check if there's a system network error
|
||||
if let urlError = error as? URLError {
|
||||
print("URLError code: \(urlError.code.rawValue)")
|
||||
Logger.shared.log("URLError code: \(urlError.code.rawValue)", type: "Download")
|
||||
|
||||
// Handle cancellation specifically
|
||||
if urlError.code == .cancelled {
|
||||
print("Download was cancelled by user")
|
||||
Logger.shared.log("Download was cancelled by user", type: "Download")
|
||||
handleDownloadCancellation(task)
|
||||
return
|
||||
} else if urlError.code == .notConnectedToInternet || urlError.code == .networkConnectionLost {
|
||||
print("Network error: \(urlError.localizedDescription)")
|
||||
Logger.shared.log("Network error: \(urlError.localizedDescription)", type: "Download")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DropManager.shared.error("Network error: \(urlError.localizedDescription)")
|
||||
}
|
||||
} else if urlError.code == .userAuthenticationRequired || urlError.code == .userCancelledAuthentication {
|
||||
print("Authentication error: \(urlError.localizedDescription)")
|
||||
Logger.shared.log("Authentication error: \(urlError.localizedDescription)", type: "Download")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
DropManager.shared.error("Authentication error: Check headers")
|
||||
|
|
@ -1364,8 +1361,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
}
|
||||
} else if error.localizedDescription.contains("403") {
|
||||
// Specific handling for 403 Forbidden errors
|
||||
print("403 Forbidden error - Server rejected the request")
|
||||
|
||||
Logger.shared.log("403 Forbidden error - Server rejected the request", type: "Download")
|
||||
DispatchQueue.main.async {
|
||||
DropManager.shared.error("Access denied (403): The server refused access to this content")
|
||||
}
|
||||
|
|
@ -1382,7 +1378,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
/// Handle download cancellation - clean up without treating as completion
|
||||
private func handleDownloadCancellation(_ task: URLSessionTask) {
|
||||
guard let downloadID = activeDownloadMap[task] else {
|
||||
print("Cancelled download task not found in active downloads")
|
||||
Logger.shared.log("Cancelled download task not found in active downloads", type: "Download")
|
||||
cleanupDownloadTask(task)
|
||||
return
|
||||
}
|
||||
|
|
@ -1409,7 +1405,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
// Notify observers of cancellation (no cache clearing needed)
|
||||
postDownloadNotification(.statusChange)
|
||||
|
||||
print("Successfully handled cancellation for: \(downloadTitle)")
|
||||
Logger.shared.log("Successfully handled cancellation for: \(downloadTitle)", type: "Download")
|
||||
}
|
||||
|
||||
/// Delete any partially downloaded assets for a cancelled download
|
||||
|
|
@ -1424,15 +1420,15 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
return wasRecentlyAdded
|
||||
}) {
|
||||
let assetToDelete = savedAssets[savedAssetIndex]
|
||||
print("Removing cancelled download from saved assets: \(assetToDelete.name)")
|
||||
Logger.shared.log("Removing cancelled download from saved assets: \(assetToDelete.name)", type: "Download")
|
||||
|
||||
// Delete the actual file if it exists
|
||||
if FileManager.default.fileExists(atPath: assetToDelete.localURL.path) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: assetToDelete.localURL)
|
||||
print("Deleted partially downloaded file: \(assetToDelete.localURL.path)")
|
||||
Logger.shared.log("Deleted partially downloaded file: \(assetToDelete.localURL.path)", type: "Download")
|
||||
} catch {
|
||||
print("Error deleting partially downloaded file: \(error.localizedDescription)")
|
||||
Logger.shared.log("Error deleting partially downloaded file: \(error.localizedDescription)", type: "Download")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1454,7 +1450,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
|
||||
// Do a quick check to see if task is still registered
|
||||
guard let downloadID = activeDownloadMap[assetDownloadTask] else {
|
||||
print("Received progress for unknown download task")
|
||||
Logger.shared.log("Received progress for unknown download task", type: "Download")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1486,14 +1482,14 @@ extension JSController: URLSessionTaskDelegate {
|
|||
/// Called when a redirect is received
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
|
||||
// Log information about the redirect
|
||||
print("==== REDIRECT DETECTED ====")
|
||||
print("Redirecting to: \(request.url?.absoluteString ?? "unknown")")
|
||||
print("Redirect status code: \(response.statusCode)")
|
||||
Logger.shared.log("==== REDIRECT DETECTED ====", type: "Download")
|
||||
Logger.shared.log("Redirecting to: \(request.url?.absoluteString ?? "unknown")", type: "Download")
|
||||
Logger.shared.log("Redirect status code: \(response.statusCode)", type: "Download")
|
||||
|
||||
// Don't try to access originalRequest for AVAssetDownloadTask
|
||||
if !(task is AVAssetDownloadTask), let originalRequest = task.originalRequest {
|
||||
print("Original URL: \(originalRequest.url?.absoluteString ?? "unknown")")
|
||||
print("Original Headers: \(originalRequest.allHTTPHeaderFields ?? [:])")
|
||||
Logger.shared.log("Original URL: \(originalRequest.url?.absoluteString ?? "unknown")", type: "Download")
|
||||
Logger.shared.log("Original Headers: \(originalRequest.allHTTPHeaderFields ?? [:])", type: "Download")
|
||||
|
||||
// Create a modified request that preserves ALL original headers
|
||||
var modifiedRequest = request
|
||||
|
|
@ -1502,40 +1498,40 @@ extension JSController: URLSessionTaskDelegate {
|
|||
for (key, value) in originalRequest.allHTTPHeaderFields ?? [:] {
|
||||
// Only add if not already present in the redirect request
|
||||
if modifiedRequest.value(forHTTPHeaderField: key) == nil {
|
||||
print("Adding missing header: \(key): \(value)")
|
||||
Logger.shared.log("Adding missing header: \(key): \(value)", type: "Download")
|
||||
modifiedRequest.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
|
||||
print("Final redirect headers: \(modifiedRequest.allHTTPHeaderFields ?? [:])")
|
||||
Logger.shared.log("Final redirect headers: \(modifiedRequest.allHTTPHeaderFields ?? [:])", type: "Download")
|
||||
|
||||
// Allow the redirect with our modified request
|
||||
completionHandler(modifiedRequest)
|
||||
} else {
|
||||
// For AVAssetDownloadTask, just accept the redirect as is
|
||||
print("Accepting redirect for AVAssetDownloadTask without header modification")
|
||||
Logger.shared.log("Accepting redirect for AVAssetDownloadTask without header modification", type: "Download")
|
||||
completionHandler(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle authentication challenges
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
print("==== AUTH CHALLENGE ====")
|
||||
print("Authentication method: \(challenge.protectionSpace.authenticationMethod)")
|
||||
print("Host: \(challenge.protectionSpace.host)")
|
||||
Logger.shared.log("==== AUTH CHALLENGE ====", type: "Download")
|
||||
Logger.shared.log("Authentication method: \(challenge.protectionSpace.authenticationMethod)", type: "Download")
|
||||
Logger.shared.log("Host: \(challenge.protectionSpace.host)", type: "Download")
|
||||
|
||||
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
|
||||
// Handle SSL/TLS certificate validation
|
||||
if let serverTrust = challenge.protectionSpace.serverTrust {
|
||||
let credential = URLCredential(trust: serverTrust)
|
||||
print("Accepting server trust for host: \(challenge.protectionSpace.host)")
|
||||
Logger.shared.log("Accepting server trust for host: \(challenge.protectionSpace.host)", type: "Download")
|
||||
completionHandler(.useCredential, credential)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Default to performing authentication without credentials
|
||||
print("Using default handling for authentication challenge")
|
||||
Logger.shared.log("Using default handling for authentication challenge", type: "Download")
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -1669,103 +1665,39 @@ enum DownloadQueueStatus: Equatable {
|
|||
case downloading
|
||||
/// Download has been completed
|
||||
case completed
|
||||
|
||||
// MARK: - Skip Sidecar (OP/ED) Fetch
|
||||
/// Fetches OP & ED skip timestamps (AniSkip) and writes a minimal sidecar JSON next to the persisted video.
|
||||
/// Uses MAL id for fillers when available; falls back to AniList otherwise.
|
||||
private func fetchSkipTimestampsFor(request: JSActiveDownload, persistentURL: URL, completion: @escaping (Bool)->Void)
|
||||
{
|
||||
// Determine preferred ID
|
||||
let epNumber = request.metadata?.episode ?? 0
|
||||
let useMAL = (request.isFiller == true) && (request.malID != nil)
|
||||
let idType = useMAL ? "mal" : "anilist"
|
||||
guard let seriesID = useMAL ? request.malID : request.aniListID else {
|
||||
print("[SkipSidecar] Missing series ID for AniSkip (MAL/AniList)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
// Single AniSkip v1 call for both OP/ED
|
||||
let url = URL(string: "https://api.aniskip.com/v1/skip-times/\(seriesID)/\(epNumber)?types=op&types=ed")!
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
if let e = error {
|
||||
print("[SkipSidecar] AniSkip fetch error: \(e.localizedDescription)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let data = data else { completion(false); return }
|
||||
struct Resp: Decodable { let found: Bool; let results: [Res]? }
|
||||
struct Res: Decodable { let skip_type: String; let interval: Interval }
|
||||
struct Interval: Decodable { let start_time: Double; let end_time: Double }
|
||||
var opRange: (Double, Double)? = nil
|
||||
var edRange: (Double, Double)? = nil
|
||||
if let r = try? JSONDecoder().decode(Resp.self, from: data), r.found, let arr = r.results {
|
||||
for item in arr {
|
||||
if item.skip_type == "op" { opRange = (item.interval.start_time, item.interval.end_time) }
|
||||
if item.skip_type == "ed" { edRange = (item.interval.start_time, item.interval.end_time) }
|
||||
}
|
||||
}
|
||||
if opRange == nil && edRange == nil { completion(false); return }
|
||||
// Determine sidecar path
|
||||
let fm = FileManager.default
|
||||
var dir = persistentURL.deletingLastPathComponent()
|
||||
var baseName = persistentURL.deletingPathExtension().lastPathComponent
|
||||
var isDir: ObjCBool = false
|
||||
if fm.fileExists(atPath: persistentURL.path, isDirectory: &isDir), isDir.boolValue {
|
||||
dir = persistentURL.deletingLastPathComponent()
|
||||
baseName = persistentURL.lastPathComponent
|
||||
}
|
||||
let sidecar = dir.appendingPathComponent(baseName + ".skip.json")
|
||||
var payload: [String: Any] = [
|
||||
"source": "aniskip",
|
||||
"idType": idType,
|
||||
"episode": epNumber,
|
||||
"createdAt": ISO8601DateFormatter().string(from: Date())
|
||||
]
|
||||
if let aid = request.aniListID { payload["anilistId"] = aid }
|
||||
if let mid = request.malID { payload["malId"] = mid }
|
||||
if let op = opRange { payload["op"] = ["start": op.0, "end": op.1] }
|
||||
if let ed = edRange { payload["ed"] = ["start": ed.0, "end": ed.1] }
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
|
||||
try data.write(to: sidecar, options: .atomic)
|
||||
print("[SkipSidecar] Wrote sidecar at: \(sidecar.path)")
|
||||
completion(true)
|
||||
} catch {
|
||||
print("[SkipSidecar] Sidecar write error: \(error.localizedDescription)")
|
||||
completion(false)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AniSkip Sidecar (OP/ED) Fetch
|
||||
extension JSController {
|
||||
/// Fetches OP & ED skip timestamps (AniSkip) and writes a minimal sidecar JSON next to the persisted video.
|
||||
/// Uses MAL id for fillers when available; falls back to AniList otherwise.
|
||||
/// Uses MAL ID only (AniList is not used).
|
||||
func fetchSkipTimestampsFor(request: JSActiveDownload,
|
||||
persistentURL: URL,
|
||||
completion: @escaping (Bool) -> Void) {
|
||||
// Determine preferred ID
|
||||
let epNumber = request.metadata?.episode ?? 0
|
||||
let useMAL = (request.isFiller == true) && (request.malID != nil)
|
||||
let idType = useMAL ? "mal" : "anilist"
|
||||
guard let seriesID = useMAL ? request.malID : request.aniListID else {
|
||||
print("[SkipSidecar] Missing series ID for AniSkip (MAL/AniList)")
|
||||
// Use MAL ID only
|
||||
guard let malID = request.malID else {
|
||||
Logger.shared.log("[SkipSidecar] No MAL ID available for AniSkip v2 request", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Single AniSkip v1 call for both OP/ED
|
||||
guard let url = URL(string: "https://api.aniskip.com/v1/skip-times/\(seriesID)/\(epNumber)?types=op&types=ed") else {
|
||||
guard let episodeNumber = request.metadata?.episode else {
|
||||
Logger.shared.log("[SkipSidecar] Missing episode number for AniSkip v2 request", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Build v2 URL (op,ed only)
|
||||
let mal = malID
|
||||
let type = "op,ed"
|
||||
|
||||
guard let url = URL(string: "https://api.aniskip.com/v2/skip-times/\(mal)/\(episodeNumber)?types=\(type)&episodeLength=0") else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
if let e = error {
|
||||
print("[SkipSidecar] AniSkip fetch error: \(e.localizedDescription)")
|
||||
Logger.shared.log("[SkipSidecar] AniSkip v2 (MAL) fetch error: \(e.localizedDescription)", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
|
@ -1773,49 +1705,51 @@ extension JSController {
|
|||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
struct Resp: Decodable { let found: Bool; let results: [Res]? }
|
||||
struct Res: Decodable { let skip_type: String; let interval: Interval }
|
||||
struct Interval: Decodable { let start_time: Double; let end_time: Double }
|
||||
|
||||
|
||||
var opRange: (Double, Double)? = nil
|
||||
var edRange: (Double, Double)? = nil
|
||||
|
||||
|
||||
if let r = try? JSONDecoder().decode(Resp.self, from: data), r.found, let arr = r.results {
|
||||
for item in arr {
|
||||
if item.skip_type == "op" { opRange = (item.interval.start_time, item.interval.end_time) }
|
||||
if item.skip_type == "ed" { edRange = (item.interval.start_time, item.interval.end_time) }
|
||||
switch item.skip_type.lowercased() {
|
||||
case "op": opRange = (item.interval.start_time, item.interval.end_time)
|
||||
case "ed": edRange = (item.interval.start_time, item.interval.end_time)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if opRange == nil && edRange == nil {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine sidecar path next to the persisted video file
|
||||
|
||||
// Sidecar path: next to the persisted video file
|
||||
let dir = persistentURL.deletingLastPathComponent()
|
||||
let baseName = persistentURL.deletingPathExtension().lastPathComponent
|
||||
let sidecar = dir.appendingPathComponent(baseName + ".skip.json")
|
||||
|
||||
|
||||
var payload: [String: Any] = [
|
||||
"source": "aniskip",
|
||||
"idType": idType,
|
||||
"episode": epNumber,
|
||||
"idType": "mal",
|
||||
"malId": mal,
|
||||
"episode": episodeNumber,
|
||||
"createdAt": ISO8601DateFormatter().string(from: Date())
|
||||
]
|
||||
if let aid = request.aniListID { payload["anilistId"] = aid }
|
||||
if let mid = request.malID { payload["malId"] = mid }
|
||||
if let op = opRange { payload["op"] = ["start": op.0, "end": op.1] }
|
||||
if let ed = edRange { payload["ed"] = ["start": ed.0, "end": ed.1] }
|
||||
|
||||
|
||||
do {
|
||||
let json = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
|
||||
try json.write(to: sidecar, options: .atomic)
|
||||
print("[SkipSidecar] Wrote sidecar at: \(sidecar.path)")
|
||||
Logger.shared.log("[SkipSidecar] Wrote sidecar at: \(sidecar.path)", type: "Download")
|
||||
completion(true)
|
||||
} catch {
|
||||
print("[SkipSidecar] Sidecar write error: \(error.localizedDescription)")
|
||||
Logger.shared.log("[SkipSidecar] Sidecar write error: \(error.localizedDescription)", type: "Download")
|
||||
completion(false)
|
||||
}
|
||||
}.resume()
|
||||
|
|
|
|||
Loading…
Reference in a new issue