mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
This commit is contained in:
parent
c03ac54a93
commit
ee3f21cc57
5 changed files with 146 additions and 8 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
82
Sora/Views/DownloadView.swift
Normal file
82
Sora/Views/DownloadView.swift
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
|
||||
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = "<group>"; };
|
||||
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Seasonal.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -261,6 +263,7 @@
|
|||
1399FAD22D3AB34F00E97C31 /* SettingsView */,
|
||||
133F55B92D33B53E00E08EEA /* LibraryView */,
|
||||
133D7C7C2D2BE2630075467E /* SearchView.swift */,
|
||||
130217CB2D81C55E0011EFF5 /* DownloadView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue