From ee3f21cc57e67fb11babb8c04039bacc280e4f00 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:13:49 +0100 Subject: [PATCH] well lets see --- Sora/ContentView.swift | 4 + Sora/DownloadManager/DownloadManager.swift | 58 ++++++++++++-- Sora/Views/DownloadView.swift | 82 ++++++++++++++++++++ Sora/Views/MediaInfoView/MediaInfoView.swift | 6 ++ Sulfur.xcodeproj/project.pbxproj | 4 + 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 Sora/Views/DownloadView.swift diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift index afbd4ea..df3d6e8 100644 --- a/Sora/ContentView.swift +++ b/Sora/ContentView.swift @@ -15,6 +15,10 @@ struct ContentView: View { .tabItem { Label("Library", systemImage: "books.vertical") } + DownloadView() + .tabItem { + Label("Downloads", systemImage: "arrow.down.circle.fill") + } SearchView() .tabItem { Label("Search", systemImage: "magnifyingglass") diff --git a/Sora/DownloadManager/DownloadManager.swift b/Sora/DownloadManager/DownloadManager.swift index 2fb78b4..3eebe51 100644 --- a/Sora/DownloadManager/DownloadManager.swift +++ b/Sora/DownloadManager/DownloadManager.swift @@ -8,18 +8,22 @@ import Foundation import FFmpegSupport +extension Notification.Name { + static let DownloadManagerStatusUpdate = Notification.Name("DownloadManagerStatusUpdate") +} + class DownloadManager { static let shared = DownloadManager() private init() {} - func downloadAndConvertHLS(from url: URL, title: String, episode: Int, subtitleURL: URL? = nil, completion: @escaping (Bool, URL?) -> Void) { + func downloadAndConvertHLS(from url: URL, title: String, episode: Int, subtitleURL: URL? = nil, sourceName: String, 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) + let folderURL = documentsDirectory.appendingPathComponent(title + "-" + sourceName) if !FileManager.default.fileExists(atPath: folderURL.path) { do { try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) @@ -30,16 +34,31 @@ class DownloadManager { } } - let outputFileName = "\(title)_Episode\(episode).mp4" + let outputFileName = "\(title)_Episode\(episode)_\(sourceName).mp4" let outputFileURL = folderURL.appendingPathComponent(outputFileName) let fileExtension = url.pathExtension.lowercased() if fileExtension == "mp4" { + NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ + "title": title, + "episode": episode, + "type": "mp4", + "status": "Downloading", + "progress": 0.0 + ]) + let task = URLSession.custom.downloadTask(with: url) { tempLocalURL, response, error in if let tempLocalURL = tempLocalURL { do { try FileManager.default.moveItem(at: tempLocalURL, to: outputFileURL) + NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ + "title": title, + "episode": episode, + "type": "mp4", + "status": "Completed", + "progress": 1.0 + ]) DispatchQueue.main.async { Logger.shared.log("Download successful: \(outputFileURL)") completion(true, outputFileURL) @@ -60,12 +79,20 @@ class DownloadManager { task.resume() } else if fileExtension == "m3u8" { DispatchQueue.global(qos: .background).async { + NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ + "title": title, + "episode": episode, + "type": "hls", + "status": "Converting", + "progress": 0.0 + ]) + let multiThreads = UserDefaults.standard.bool(forKey: "multiThreads") var ffmpegCommand: [String] if multiThreads { - ffmpegCommand = ["ffmpeg", "-threads", "0", "-i", url.absoluteString] + ffmpegCommand = ["ffmpeg", "-y", "-threads", "0", "-i", url.absoluteString] } else { - ffmpegCommand = ["ffmpeg", "-i", url.absoluteString] + ffmpegCommand = ["ffmpeg", "-y", "-i", url.absoluteString] } if let subtitleURL = subtitleURL { @@ -79,18 +106,33 @@ class DownloadManager { let subtitleLocalURL = folderURL.appendingPathComponent(subtitleFileName) try subtitleData.write(to: subtitleLocalURL) ffmpegCommand.append(contentsOf: ["-i", subtitleLocalURL.path]) - ffmpegCommand.append(contentsOf: ["-c", "copy", "-c:s", "mov_text", outputFileURL.path]) + ffmpegCommand.append(contentsOf: ["-c:v", "copy", "-c:a", "copy", "-c:s", "mov_text", outputFileURL.path]) } catch { Logger.shared.log("Subtitle download failed: \(error)") - ffmpegCommand.append(contentsOf: ["-c", "copy", outputFileURL.path]) + ffmpegCommand.append(contentsOf: ["-c:v", "copy", "-c:a", "copy", outputFileURL.path]) } } else { - ffmpegCommand.append(contentsOf: ["-c", "copy", outputFileURL.path]) + ffmpegCommand.append(contentsOf: ["-c:v", "copy", "-c:a", "copy", outputFileURL.path]) } + NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ + "title": title, + "episode": episode, + "type": "hls", + "status": "Converting", + "progress": 0.5 + ]) + let success = ffmpeg(ffmpegCommand) DispatchQueue.main.async { if success == 0 { + NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ + "title": title, + "episode": episode, + "type": "hls", + "status": "Completed", + "progress": 1.0 + ]) Logger.shared.log("Conversion successful: \(outputFileURL)") completion(true, outputFileURL) } else { diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift new file mode 100644 index 0000000..8cfc4a2 --- /dev/null +++ b/Sora/Views/DownloadView.swift @@ -0,0 +1,82 @@ +// +// DownloadView.swift +// Sulfur +// +// Created by Francesco on 12/03/25. +// + +import SwiftUI + +struct DownloadItem: Identifiable { + let id = UUID() + let title: String + let episode: Int + let type: String + var progress: Double + var status: String +} + +class DownloadViewModel: ObservableObject { + @Published var downloads: [DownloadItem] = [] + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(updateStatus(_:)), name: .DownloadManagerStatusUpdate, object: nil) + } + + @objc func updateStatus(_ notification: Notification) { + guard let info = notification.userInfo, + let title = info["title"] as? String, + let episode = info["episode"] as? Int, + let type = info["type"] as? String, + let status = info["status"] as? String, + let progress = info["progress"] as? Double else { return } + + if let index = downloads.firstIndex(where: { $0.title == title && $0.episode == episode }) { + downloads[index] = DownloadItem(title: title, episode: episode, type: type, progress: progress, status: status) + } else { + let newDownload = DownloadItem(title: title, episode: episode, type: type, progress: progress, status: status) + downloads.append(newDownload) + } + } +} + +struct DownloadView: View { + @StateObject var viewModel = DownloadViewModel() + + var body: some View { + NavigationView { + List(viewModel.downloads) { download in + HStack(spacing: 16) { + Image(systemName: iconName(for: download)) + .resizable() + .frame(width: 30, height: 30) + .foregroundColor(.accentColor) + + VStack(alignment: .leading, spacing: 4) { + Text("\(download.title) - Episode \(download.episode)") + .font(.headline) + + ProgressView(value: download.progress) + .progressViewStyle(LinearProgressViewStyle(tint: .accentColor)) + .frame(height: 8) + + Text(download.status) + .font(.subheadline) + .foregroundColor(.secondary) + } + Spacer() + } + .padding(.vertical, 8) + } + .navigationTitle("Downloads") + } + } + + func iconName(for download: DownloadItem) -> String { + if download.type == "hls" { + return download.status.lowercased().contains("converting") ? "arrow.triangle.2.circlepath.circle.fill" : "checkmark.circle.fill" + } else { + return download.progress >= 1.0 ? "checkmark.circle.fill" : "arrow.down.circle.fill" + } + } +} \ No newline at end of file diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 1504c12..0360363 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -490,6 +490,12 @@ struct MediaInfoView: View { func playStream(url: String, fullURL: String, subtitles: String? = nil) { DispatchQueue.main.async { + guard let streamURL = URL(string: url) else { return } + let subtitleFileURL = subtitles != nil ? URL(string: subtitles!) : nil + DownloadManager.shared.downloadAndConvertHLS(from: streamURL, title: title, episode: selectedEpisodeNumber, subtitleURL: subtitleFileURL, sourceName: module.metadata.sourceName) { success, fileURL in + return + } + let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Default" var scheme: String? diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index aba11bf..6e03a51 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; }; 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; }; 13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */; }; 13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E852D58A328000F0673 /* AniList-Trending.swift */; }; @@ -63,6 +64,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 130217CB2D81C55E0011EFF5 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; 130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; }; 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = ""; }; 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Seasonal.swift"; sourceTree = ""; }; @@ -261,6 +263,7 @@ 1399FAD22D3AB34F00E97C31 /* SettingsView */, 133F55B92D33B53E00E08EEA /* LibraryView */, 133D7C7C2D2BE2630075467E /* SearchView.swift */, + 130217CB2D81C55E0011EFF5 /* DownloadView.swift */, ); path = Views; sourceTree = ""; @@ -557,6 +560,7 @@ 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, 136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */, + 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, 13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */, 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,