This commit is contained in:
Francesco 2025-05-25 09:45:47 +02:00
parent f7bdb4dc4d
commit 6ff7fe06e2
4 changed files with 145 additions and 138 deletions

View file

@ -382,10 +382,3 @@ extension JSController {
)
}
}
// MARK: - Private API Compatibility Extension
// This extension ensures compatibility with the existing JSController-Downloads.swift implementation
private extension JSController {
// No longer needed since JSController-Downloads.swift has been implemented
// Remove the duplicate startDownload method to avoid conflicts
}

View file

@ -38,6 +38,12 @@ extension JSController {
print("Subtitle URL: \(subtitle.absoluteString)")
}
// Validate URL
guard url.scheme == "http" || url.scheme == "https" else {
completionHandler?(false, "Invalid URL scheme")
return
}
// Create metadata for the download
var metadata: AssetMetadata? = nil
if let title = title {
@ -47,7 +53,7 @@ extension JSController {
showTitle: showTitle,
season: season,
episode: episode,
showPosterURL: imageURL // Use the correct show poster URL
showPosterURL: imageURL
)
}
@ -57,12 +63,24 @@ extension JSController {
// Generate a unique download ID
let downloadID = UUID()
// Get access to the download directory
guard let downloadDirectory = getPersistentDownloadDirectory() else {
print("MP4 Download: Failed to get download directory")
completionHandler?(false, "Failed to create download directory")
return
}
// Generate a safe filename for the MP4 file
let sanitizedTitle = title?.replacingOccurrences(of: "[^A-Za-z0-9 ._-]", with: "", options: .regularExpression) ?? "download"
let filename = "\(sanitizedTitle)_\(downloadID.uuidString.prefix(8)).mp4"
let destinationURL = downloadDirectory.appendingPathComponent(filename)
// Create an active download object
let activeDownload = JSActiveDownload(
id: downloadID,
originalURL: url,
task: nil, // We'll set this after creating the task
queueStatus: .queued,
task: nil,
queueStatus: .downloading,
type: downloadType,
metadata: metadata,
title: title,
@ -74,84 +92,79 @@ extension JSController {
// Add to active downloads
activeDownloads.append(activeDownload)
// Create a URL session task for downloading the MP4 file
// Create request with headers
var request = URLRequest(url: url)
request.timeoutInterval = 30.0
for (key, value) in headers {
request.addValue(value, forHTTPHeaderField: key)
}
// Get access to the download directory using the shared instance method
guard let downloadDirectory = getPersistentDownloadDirectory() else {
print("MP4 Download: Failed to get download directory")
completionHandler?(false, "Failed to create download directory")
return
}
// Generate a unique filename for the MP4 file
let filename = "\(downloadID.uuidString).mp4"
let destinationURL = downloadDirectory.appendingPathComponent(filename)
// Use a session configuration that allows handling SSL issues
// Enhanced session configuration
let sessionConfig = URLSessionConfiguration.default
// Set a longer timeout for large files
sessionConfig.timeoutIntervalForRequest = 60.0
sessionConfig.timeoutIntervalForResource = 600.0
sessionConfig.timeoutIntervalForResource = 1800.0 // 30 minutes for large files
sessionConfig.httpMaximumConnectionsPerHost = 1
sessionConfig.allowsCellularAccess = true
// Create a URL session that handles SSL certificate validation issues
// Create custom session with delegate
let customSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
// Create the download task with the custom session
let downloadTask = customSession.downloadTask(with: request) { (tempURL, response, error) in
// 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)")
// Update active download status
if let index = self.activeDownloads.firstIndex(where: { $0.id == downloadID }) {
self.activeDownloads[index].queueStatus = .queued
}
// Clean up resources
self.mp4ProgressObservations?[downloadID] = nil
self.mp4CustomSessions?[downloadID] = nil
// Remove the download after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.activeDownloads.removeAll { $0.id == downloadID }
}
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
}
if httpResponse.statusCode >= 400 {
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 {
// Move the temporary file to the permanent location
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 moved file to \(destinationURL.path)")
print("MP4 Download: Successfully saved to \(destinationURL.path)")
// Create the downloaded asset
// 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(),
@ -162,112 +175,122 @@ extension JSController {
subtitleURL: subtitleURL
)
// Add to saved assets
// Save asset
self.savedAssets.append(downloadedAsset)
self.saveAssets()
// Update active download and remove after a delay
if let index = self.activeDownloads.firstIndex(where: { $0.id == downloadID }) {
self.activeDownloads[index].progress = 1.0
self.activeDownloads[index].queueStatus = .completed
}
// 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 observers - use downloadCompleted since the download finished
NotificationCenter.default.post(name: NSNotification.Name("downloadCompleted"), object: nil)
// Notify completion
NotificationCenter.default.post(name: NSNotification.Name("downloadCompleted"), object: downloadedAsset)
completionHandler?(true, "Download completed successfully")
// Clean up resources
self.mp4ProgressObservations?[downloadID] = nil
self.mp4CustomSessions?[downloadID] = nil
// Remove the completed download from active list after a delay
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.activeDownloads.removeAll { $0.id == downloadID }
// Remove from active downloads after success
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.removeActiveDownload(downloadID: downloadID)
}
} catch {
print("MP4 Download Error moving file: \(error.localizedDescription)")
print("MP4 Download Error saving file: \(error.localizedDescription)")
self.removeActiveDownload(downloadID: downloadID)
completionHandler?(false, "Error saving download: \(error.localizedDescription)")
}
}
}
// Set up progress tracking
// Set up progress observation
setupProgressObservation(for: downloadTask, downloadID: downloadID)
// Store session reference
storeSessionReference(session: customSession, for: downloadID)
// Start download
downloadTask.resume()
print("MP4 Download: Task started for \(filename)")
// Update the task in the active download
if let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) {
activeDownloads[index].queueStatus = .downloading
// Store reference to the downloadTask directly - no need to access private properties
print("MP4 Download: Task started")
// We can't directly store URLSessionDownloadTask in place of AVAssetDownloadTask
// Just continue tracking progress separately
}
// Set up progress observation - fix the key path specification
let observation = downloadTask.progress.observe(\Progress.fractionCompleted) { progress, _ in
// Initial success callback
completionHandler?(true, "Download started")
}
// MARK: - Helper Methods
private func removeActiveDownload(downloadID: UUID) {
activeDownloads.removeAll { $0.id == downloadID }
}
private func updateDownloadProgress(downloadID: UUID, progress: Double) {
guard let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) else { return }
activeDownloads[index].progress = progress
}
private func setupProgressObservation(for task: URLSessionDownloadTask, downloadID: UUID) {
let observation = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
DispatchQueue.main.async {
if let index = self.activeDownloads.firstIndex(where: { $0.id == downloadID }) {
self.activeDownloads[index].progress = progress.fractionCompleted
// Notify observers of progress update
NotificationCenter.default.post(name: NSNotification.Name("downloadProgressUpdated"), object: nil)
}
guard let self = self else { return }
self.updateDownloadProgress(downloadID: downloadID, progress: progress.fractionCompleted)
NotificationCenter.default.post(name: NSNotification.Name("downloadProgressUpdated"), object: nil)
}
}
// Store the observation somewhere to keep it alive - using nonatomic property from main class
if self.mp4ProgressObservations == nil {
self.mp4ProgressObservations = [:]
if mp4ProgressObservations == nil {
mp4ProgressObservations = [:]
}
self.mp4ProgressObservations?[downloadID] = observation
// Store the custom session to keep it alive until download is complete
if self.mp4CustomSessions == nil {
self.mp4CustomSessions = [:]
mp4ProgressObservations?[downloadID] = observation
}
private func storeSessionReference(session: URLSession, for downloadID: UUID) {
if mp4CustomSessions == nil {
mp4CustomSessions = [:]
}
self.mp4CustomSessions?[downloadID] = customSession
// Notify that download started successfully
completionHandler?(true, "Download started")
mp4CustomSessions?[downloadID] = session
}
private func cleanupDownloadResources(for downloadID: UUID) {
mp4ProgressObservations?[downloadID] = nil
mp4CustomSessions?[downloadID] = nil
}
}
// Extension for handling SSL certificate validation for MP4 downloads
// MARK: - URLSessionDelegate
extension JSController: URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Handle SSL/TLS certificate validation
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let host = challenge.protectionSpace.host
print("MP4 Download: Handling server trust challenge for host: \(host)")
// Accept the server's certificate for known problematic domains
// or for domains in our custom session downloads
if host.contains("streamtales.cc") ||
host.contains("frembed.xyz") ||
host.contains("vidclouds.cc") ||
self.mp4CustomSessions?.values.contains(session) == true {
if let serverTrust = challenge.protectionSpace.serverTrust {
// Log detailed info about the trust
print("MP4 Download: Accepting certificate for \(host)")
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
return
}
}
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// For other authentication challenges, use default handling
print("MP4 Download: Using default handling for auth challenge")
completionHandler(.performDefaultHandling, nil)
let host = challenge.protectionSpace.host
print("MP4 Download: Handling server trust challenge for host: \(host)")
// Define trusted hosts for MP4 downloads
let trustedHosts = [
"streamtales.cc",
"frembed.xyz",
"vidclouds.cc"
]
let isTrustedHost = trustedHosts.contains { host.contains($0) }
let isCustomSession = mp4CustomSessions?.values.contains(session) == true
if isTrustedHost || isCustomSession {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
print("MP4 Download: Accepting certificate for \(host)")
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
print("MP4 Download: Using default handling for \(host)")
completionHandler(.performDefaultHandling, nil)
}
}
}
}

View file

@ -47,17 +47,10 @@ extension JSController {
if let subtitle = subtitleURL {
print("Subtitle URL: \(subtitle.absoluteString)")
}
// Check the stream type from the module metadata
let streamType = module.metadata.streamType.lowercased()
// Determine which download method to use based on streamType
if streamType == "mp4" || streamType == "direct" || url.absoluteString.contains(".mp4") {
print("MP4 URL detected - downloading not supported")
completionHandler?(false, "MP4 direct downloads are not supported. Please use HLS streams for downloading.")
return
} else if streamType == "hls" || streamType == "m3u8" || url.absoluteString.contains(".m3u8") {
print("Using HLS download method")
if streamType == "hls" || streamType == "m3u8" || url.absoluteString.contains(".m3u8") {
Logger.shared.log("Using HLS download method")
downloadWithM3U8Support(
url: url,
headers: headers,
@ -71,22 +64,20 @@ extension JSController {
showPosterURL: showPosterURL,
completionHandler: completionHandler
)
} else {
// Default to M3U8 method for unknown types, as it has fallback mechanisms
print("Using default HLS download method for unknown stream type: \(streamType)")
downloadWithM3U8Support(
}else {
Logger.shared.log("Using MP4 download method")
downloadMP4(
url: url,
headers: headers,
title: title,
imageURL: imageURL,
imageURL: imageURL ?? showPosterURL,
isEpisode: isEpisode,
showTitle: showTitle,
season: season,
episode: episode,
subtitleURL: subtitleURL,
showPosterURL: showPosterURL,
completionHandler: completionHandler
)
}
}
}
}

View file

@ -197,8 +197,8 @@
13103E8C2D58E037000F0673 /* SkeletonCells */ = {
isa = PBXGroup;
children = (
13103E8D2D58E04A000F0673 /* SkeletonCell.swift */,
13B7F4C02D58FFDD0045714A /* Shimmer.swift */,
13103E8D2D58E04A000F0673 /* SkeletonCell.swift */,
);
path = SkeletonCells;
sourceTree = "<group>";