mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 16:42:01 +00:00
add bulk download support (#254)
This commit is contained in:
parent
914c3c4990
commit
611646902c
1 changed files with 220 additions and 9 deletions
|
|
@ -65,6 +65,7 @@ struct MediaInfoView: View {
|
||||||
@State private var isBulkDownloading: Bool = false
|
@State private var isBulkDownloading: Bool = false
|
||||||
@State private var bulkDownloadProgress: String = ""
|
@State private var bulkDownloadProgress: String = ""
|
||||||
@State private var isSingleEpisodeDownloading: Bool = false
|
@State private var isSingleEpisodeDownloading: Bool = false
|
||||||
|
@State private var bulkTask: Task<Void, Never>? = nil
|
||||||
|
|
||||||
@State private var isModuleSelectorPresented = false
|
@State private var isModuleSelectorPresented = false
|
||||||
@State private var isMatchingPresented = false
|
@State private var isMatchingPresented = false
|
||||||
|
|
@ -233,6 +234,7 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
currentFetchTask?.cancel()
|
currentFetchTask?.cancel()
|
||||||
|
bulkTask?.cancel()
|
||||||
activeFetchID = nil
|
activeFetchID = nil
|
||||||
UserDefaults.standard.set(false, forKey: "isMediaInfoActive")
|
UserDefaults.standard.set(false, forKey: "isMediaInfoActive")
|
||||||
UIScrollView.appearance().bounces = true
|
UIScrollView.appearance().bounces = true
|
||||||
|
|
@ -890,7 +892,6 @@ struct MediaInfoView: View {
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if activeProvider == "AniList" {
|
if activeProvider == "AniList" {
|
||||||
Button("Match with AniList") {
|
Button("Match with AniList") {
|
||||||
isMatchingPresented = true
|
isMatchingPresented = true
|
||||||
|
|
@ -898,7 +899,6 @@ struct MediaInfoView: View {
|
||||||
Button(action: { resetAniListID() }) {
|
Button(action: { resetAniListID() }) {
|
||||||
Label("Reset AniList ID", systemImage: "arrow.clockwise")
|
Label("Reset AniList ID", systemImage: "arrow.clockwise")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: { openAniListPage(id: itemID ?? 0) }) {
|
Button(action: { openAniListPage(id: itemID ?? 0) }) {
|
||||||
Label("Open in AniList", systemImage: "link")
|
Label("Open in AniList", systemImage: "link")
|
||||||
}
|
}
|
||||||
|
|
@ -913,6 +913,12 @@ struct MediaInfoView: View {
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
|
if !(module.metadata.novel ?? false) {
|
||||||
|
Button(action: { downloadAllEpisodes() }) {
|
||||||
|
Label("Download All Episodes", systemImage: "arrow.down.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button(action: { logDebugInfo() }) {
|
Button(action: { logDebugInfo() }) {
|
||||||
Label("Log Debug Info", systemImage: "terminal")
|
Label("Log Debug Info", systemImage: "terminal")
|
||||||
}
|
}
|
||||||
|
|
@ -2466,10 +2472,216 @@ struct MediaInfoView: View {
|
||||||
}.resume()
|
}.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func downloadAllEpisodes() {
|
||||||
|
bulkTask = Task {
|
||||||
|
await MainActor.run {
|
||||||
|
isBulkDownloading = true
|
||||||
|
bulkDownloadProgress = "Starting bulk download..."
|
||||||
|
}
|
||||||
|
|
||||||
|
let originalLimit = jsController.maxConcurrentDownloads
|
||||||
|
jsController.updateMaxConcurrentDownloads(Int.max)
|
||||||
|
|
||||||
|
let episodesToDownload = getEpisodesToDownload()
|
||||||
|
let total = episodesToDownload.count
|
||||||
|
|
||||||
|
if total == 0 {
|
||||||
|
await MainActor.run {
|
||||||
|
jsController.updateMaxConcurrentDownloads(originalLimit)
|
||||||
|
isBulkDownloading = false
|
||||||
|
bulkDownloadProgress = ""
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var completed = 0
|
||||||
|
|
||||||
|
await withTaskGroup(of: Bool.self) { group in
|
||||||
|
for (ep, season) in episodesToDownload {
|
||||||
|
group.addTask {
|
||||||
|
await self.downloadEpisodeIfNeeded(ep, season: season)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await success in group {
|
||||||
|
do {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
await MainActor.run {
|
||||||
|
completed += 1
|
||||||
|
bulkDownloadProgress = "Downloaded \(completed)/\(total) episodes"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsController.updateMaxConcurrentDownloads(originalLimit)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
isBulkDownloading = false
|
||||||
|
bulkDownloadProgress = ""
|
||||||
|
DropManager.shared.showDrop(
|
||||||
|
title: "Bulk downloading started",
|
||||||
|
subtitle: "",
|
||||||
|
duration: 2.0,
|
||||||
|
icon: UIImage(systemName: "checkmark.circle")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getEpisodesToDownload() -> [(EpisodeLink, Int)] {
|
||||||
|
let seasonGroups = groupedEpisodes()
|
||||||
|
var episodesToDownload: [(EpisodeLink, Int)] = []
|
||||||
|
|
||||||
|
for (seasonIndex, episodesInSeason) in seasonGroups.enumerated() {
|
||||||
|
let seasonNumber = seasonIndex + 1
|
||||||
|
for ep in episodesInSeason {
|
||||||
|
let downloadStatus = jsController.isEpisodeDownloadedOrInProgress(
|
||||||
|
showTitle: title,
|
||||||
|
episodeNumber: ep.number,
|
||||||
|
season: seasonNumber
|
||||||
|
)
|
||||||
|
if !downloadStatus.isDownloadedOrInProgress {
|
||||||
|
episodesToDownload.append((ep, seasonNumber))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodesToDownload
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadEpisodeIfNeeded(_ ep: EpisodeLink, season: Int) async -> Bool {
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
downloadEpisodeForBulk(ep, season: season) { success in
|
||||||
|
continuation.resume(returning: success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadEpisodeForBulk(_ ep: EpisodeLink, season: Int, completion: @escaping (Bool) -> Void) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let jsContent = try moduleManager.getModuleContent(module)
|
||||||
|
jsController.loadScript(jsContent)
|
||||||
|
tryNextDownloadMethodForBulk(episode: ep, season: season, methodIndex: 0, completion: completion)
|
||||||
|
} catch {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tryNextDownloadMethodForBulk(episode: EpisodeLink, season: Int, methodIndex: Int, completion: @escaping (Bool) -> Void) {
|
||||||
|
switch methodIndex {
|
||||||
|
case 0:
|
||||||
|
if module.metadata.asyncJS == true {
|
||||||
|
jsController.fetchStreamUrlJS(episodeUrl: episode.href, softsub: module.metadata.softsub == true, module: module) { result in
|
||||||
|
self.handleBulkDownloadResult(result, episode: episode, season: season, methodIndex: methodIndex, completion: completion)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tryNextDownloadMethodForBulk(episode: episode, season: season, methodIndex: methodIndex + 1, completion: completion)
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
if module.metadata.streamAsyncJS == true {
|
||||||
|
jsController.fetchStreamUrlJSSecond(episodeUrl: episode.href, softsub: module.metadata.softsub == true, module: module) { result in
|
||||||
|
self.handleBulkDownloadResult(result, episode: episode, season: season, methodIndex: methodIndex, completion: completion)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tryNextDownloadMethodForBulk(episode: episode, season: season, methodIndex: methodIndex + 1, completion: completion)
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
jsController.fetchStreamUrl(episodeUrl: episode.href, softsub: module.metadata.softsub == true, module: module) { result in
|
||||||
|
self.handleBulkDownloadResult(result, episode: episode, season: season, methodIndex: methodIndex, completion: completion)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleBulkDownloadResult(_ result: (streams: [String]?, subtitles: [String]?, sources: [[String:Any]]?), episode: EpisodeLink, season: Int, methodIndex: Int, completion: @escaping (Bool) -> Void) {
|
||||||
|
if let sources = result.sources, !sources.isEmpty {
|
||||||
|
if sources.count > 1 {
|
||||||
|
if let streamUrl = sources[0]["streamUrl"] as? String ?? sources[0]["url"] as? String, let url = URL(string: streamUrl) {
|
||||||
|
let subtitleURLString = sources[0]["subtitle"] as? String
|
||||||
|
let subtitleURL = subtitleURLString.flatMap { URL(string: $0) }
|
||||||
|
startBulkEpisodeDownload(episode: episode, url: url, streamUrl: streamUrl, subtitleURL: subtitleURL, season: season, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if let streamUrl = sources[0]["streamUrl"] as? String, let url = URL(string: streamUrl) {
|
||||||
|
let subtitleURLString = sources[0]["subtitle"] as? String
|
||||||
|
let subtitleURL = subtitleURLString.flatMap { URL(string: $0) }
|
||||||
|
startBulkEpisodeDownload(episode: episode, url: url, streamUrl: streamUrl, subtitleURL: subtitleURL, season: season, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let streams = result.streams, !streams.isEmpty {
|
||||||
|
if streams[0] == "[object Promise]" {
|
||||||
|
tryNextDownloadMethodForBulk(episode: episode, season: season, methodIndex: methodIndex + 1, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if streams.count > 1 {
|
||||||
|
if let url = URL(string: streams[0]) {
|
||||||
|
let subtitleURL = result.subtitles?.first.flatMap { URL(string: $0) }
|
||||||
|
startBulkEpisodeDownload(episode: episode, url: url, streamUrl: streams[0], subtitleURL: subtitleURL, season: season, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if let url = URL(string: streams[0]) {
|
||||||
|
let subtitleURL = result.subtitles?.first.flatMap { URL(string: $0) }
|
||||||
|
startBulkEpisodeDownload(episode: episode, url: url, streamUrl: streams[0], subtitleURL: subtitleURL, season: season, completion: completion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tryNextDownloadMethodForBulk(episode: episode, season: season, methodIndex: methodIndex + 1, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startBulkEpisodeDownload(episode: EpisodeLink, url: URL, streamUrl: String, subtitleURL: URL? = nil, season: Int, completion: @escaping (Bool) -> Void) {
|
||||||
|
let headers = generateDownloadHeaders(for: url)
|
||||||
|
|
||||||
|
fetchEpisodeMetadataForDownload(episode: episode) { metadata in
|
||||||
|
let episodeTitle = metadata?.title["en"] ?? "Episode \(episode.number)"
|
||||||
|
let episodeImageUrl = metadata?.imageUrl ?? ""
|
||||||
|
|
||||||
|
let episodeThumbnailURL: URL?
|
||||||
|
if !episodeImageUrl.isEmpty {
|
||||||
|
episodeThumbnailURL = URL(string: episodeImageUrl)
|
||||||
|
} else {
|
||||||
|
episodeThumbnailURL = URL(string: self.getBannerImageBasedOnAppearance())
|
||||||
|
}
|
||||||
|
|
||||||
|
let showPosterImageURL = URL(string: self.imageUrl)
|
||||||
|
|
||||||
|
self.jsController.downloadWithStreamTypeSupport(
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
title: episodeTitle,
|
||||||
|
imageURL: episodeThumbnailURL,
|
||||||
|
module: self.module,
|
||||||
|
isEpisode: true,
|
||||||
|
showTitle: self.title,
|
||||||
|
season: season,
|
||||||
|
episode: episode.number,
|
||||||
|
subtitleURL: subtitleURL,
|
||||||
|
showPosterURL: showPosterImageURL,
|
||||||
|
completionHandler: { success, message in
|
||||||
|
if success {
|
||||||
|
Logger.shared.log("Started bulk download for Episode \(episode.number): \(episode.href)", type: "Download")
|
||||||
|
} else {
|
||||||
|
Logger.shared.log("Bulk download failed for Episode \(episode.number): \(message)", type: "Download")
|
||||||
|
}
|
||||||
|
completion(success)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func presentAlert(_ alert: UIAlertController) {
|
private func presentAlert(_ alert: UIAlertController) {
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
let window = windowScene.windows.first,
|
let window = windowScene.windows.first,
|
||||||
let rootVC = window.rootViewController {
|
let rootVC = window.rootViewController {
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
if let popover = alert.popoverPresentationController {
|
if let popover = alert.popoverPresentationController {
|
||||||
|
|
@ -2483,7 +2695,6 @@ struct MediaInfoView: View {
|
||||||
popover.permittedArrowDirections = []
|
popover.permittedArrowDirections = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findTopViewController.findViewController(rootVC).present(alert, animated: true)
|
findTopViewController.findViewController(rootVC).present(alert, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue