From 7e611ce77fa41cb96519226198cf9c8268d0f5c3 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:57:05 +0100 Subject: [PATCH] yes -DownloadManager --- Sora/DownloadManager/DownloadManager.swift | 93 ++++++++++++++++++++++ Sora/Views/LibraryView/LibraryView.swift | 5 +- Sulfur.xcodeproj/project.pbxproj | 12 +++ 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 Sora/DownloadManager/DownloadManager.swift diff --git a/Sora/DownloadManager/DownloadManager.swift b/Sora/DownloadManager/DownloadManager.swift new file mode 100644 index 0000000..81c3f49 --- /dev/null +++ b/Sora/DownloadManager/DownloadManager.swift @@ -0,0 +1,93 @@ +// +// DownloadManager.swift +// Sulfur +// +// Created by Francesco on 09/03/25. +// + +import Foundation +import FFmpegSupport + +class DownloadManager { + static let shared = DownloadManager() + + private init() {} + + /// Downloads and converts an HLS stream to MP4, or downloads an MP4 stream normally. + /// - Parameters: + /// - url: The stream URL (either .m3u8 or .mp4). + /// - title: The title used for creating the folder. + /// - episode: The episode number used for naming the output file. + /// - completion: Completion handler with a Bool indicating success and the URL of the output file. + func downloadAndConvertHLS(from url: URL, title: String, episode: Int, completion: @escaping (Bool, URL?) -> Void) { + guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + completion(false, nil) + return + } + + let folderURL = documentsDirectory.appendingPathComponent(title) + if !FileManager.default.fileExists(atPath: folderURL.path) { + do { + try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) + } catch { + print("Error creating folder: \(error)") + completion(false, nil) + return + } + } + + let outputFileName = "\(title)_Episode\(episode).mp4" + let outputFileURL = folderURL.appendingPathComponent(outputFileName) + + let fileExtension = url.pathExtension.lowercased() + + if fileExtension == "mp4" { + let task = URLSession.shared.downloadTask(with: url) { tempLocalURL, response, error in + if let tempLocalURL = tempLocalURL { + do { + try FileManager.default.moveItem(at: tempLocalURL, to: outputFileURL) + DispatchQueue.main.async { + Logger.shared.log("✅ Download successful: \(outputFileURL)") + completion(true, outputFileURL) + } + } catch { + DispatchQueue.main.async { + Logger.shared.log("❌ Download failed: \(error)") + completion(false, nil) + } + } + } else { + DispatchQueue.main.async { + Logger.shared.log("❌ Download failed: \(error?.localizedDescription ?? "Unknown error")") + completion(false, nil) + } + } + } + task.resume() + } else if fileExtension == "m3u8" { + let ffmpegCommand = [ + "ffmpeg", + "-threads", "0", + "-i", url.absoluteString, + "-c", "copy", + outputFileURL.path + ] + + DispatchQueue.global(qos: .background).async { + let success = ffmpeg(ffmpegCommand) + DispatchQueue.main.async { + if (success == 0) { + Logger.shared.log("✅ Conversion successful: \(outputFileURL)") + completion(true, outputFileURL) + } else { + Logger.shared.log("❌ Conversion failed") + completion(false, nil) + } + } + } + } else { + Logger.shared.log("❌ Unsupported file type: \(fileExtension)") + completion(false, nil) + } + } +} diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 0b6480f..26a8433 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -21,7 +21,7 @@ struct LibraryView: View { var body: some View { NavigationView { ScrollView { - VStack(alignment: .leading, spacing: 32) { + VStack(alignment: .leading, spacing: 16) { Group { Text("Continue Watching") .font(.title2) @@ -92,7 +92,6 @@ struct LibraryView: View { .aspectRatio(contentMode: .fit) .frame(width: 35, height: 35) .clipShape(Circle()) - .padding(5) } Text(item.title) @@ -100,7 +99,6 @@ struct LibraryView: View { .foregroundColor(.primary) .lineLimit(2) .multilineTextAlignment(.leading) - .padding(.horizontal, 8) } } } @@ -139,7 +137,6 @@ struct LibraryView: View { } } -// ContinueWatchingSection and ContinueWatchingCell remain unchanged struct ContinueWatchingSection: View { @Binding var items: [ContinueWatchingItem] var markAsWatched: (ContinueWatchingItem) -> Void diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index dd56d95..aba11bf 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */; }; 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; }; 13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; }; + 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; }; 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; }; 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; }; 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; }; @@ -107,6 +108,7 @@ 13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = ""; }; 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.swift; sourceTree = ""; }; 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = ""; }; + 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; @@ -230,6 +232,7 @@ 133D7C6C2D2BE2500075467E /* Sora */ = { isa = PBXGroup; children = ( + 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 130C6BF82D53A4C200DC1432 /* Sora.entitlements */, 13DC0C412D2EC9BA00D0F966 /* Info.plist */, 13103E802D589D6C000F0673 /* Tracking Services */, @@ -398,6 +401,14 @@ path = Drops; sourceTree = ""; }; + 13DB7CEA2D7DED50004371D3 /* DownloadManager */ = { + isa = PBXGroup; + children = ( + 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */, + ); + path = DownloadManager; + sourceTree = ""; + }; 13DC0C442D302C6A00D0F966 /* MediaPlayer */ = { isa = PBXGroup; children = ( @@ -526,6 +537,7 @@ 136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */, 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, + 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */, 1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */, 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */, 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,