From 4f28f785bfa59ebf3d744928a557c6954383a97b Mon Sep 17 00:00:00 2001 From: scigward Date: Tue, 19 Aug 2025 08:32:21 +0300 Subject: [PATCH] Please work --- .../CustomPlayer/CustomPlayer.swift | 28 ++-- .../DownloadUtils/DownloadModels.swift | 14 ++ .../Downloads/JSController+Downloader.swift | 24 ++-- .../Downloads/JSController-Downloads.swift | 130 ++++++++---------- .../JSController-StreamTypeDownload.swift | 20 ++- Sora/Views/DownloadView.swift | 23 +--- .../EpisodeCell/EpisodeCell.swift | 8 +- Sora/Views/MediaInfoView/MediaInfoView.swift | 1 - 8 files changed, 119 insertions(+), 129 deletions(-) diff --git a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift index dc75734..dbb80c6 100644 --- a/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift +++ b/Sora/MediaUtils/CustomPlayer/CustomPlayer.swift @@ -48,9 +48,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele var isPlaying = true var currentTimeVal: Double = 0.0 var duration: Double = 0.0 - - let localSkipSegments: [(String, Double, Double)]? -var isVideoLoaded = false + var isVideoLoaded = false private var isHoldPauseEnabled: Bool { UserDefaults.standard.bool(forKey: "holdForPauseEnabled") @@ -154,6 +152,7 @@ var isVideoLoaded = false private var malID: Int? private var skipIntervals: (op: CMTimeRange?, ed: CMTimeRange?) = (nil, nil) + private var preloadedSkipInfo: SkipInfo? = nil private var skipIntroButton: UIButton! private var skipOutroButton: UIButton! @@ -268,8 +267,9 @@ var isVideoLoaded = false onWatchNext: @escaping () -> Void, subtitlesURL: String?, aniListID: Int, + skipInfo: SkipInfo? = nil, totalEpisodes: Int, - episodeImageUrl: String, localSkipSegments: [(String, Double, Double)]? = nil, headers:[String:String]?) { + episodeImageUrl: String,headers:[String:String]?) { self.module = module self.streamURL = urlString @@ -278,11 +278,11 @@ var isVideoLoaded = false self.episodeNumber = episodeNumber self.episodeImageUrl = episodeImageUrl self.episodeTitle = episodeTitle - self.localSkipSegments = localSkipSegments self.seasonNumber = seasonNumber self.onWatchNext = onWatchNext self.subtitlesURL = subtitlesURL self.aniListID = aniListID + self.preloadedSkipInfo = skipInfo self.headers = headers self.totalEpisodes = totalEpisodes @@ -396,19 +396,12 @@ var isVideoLoaded = false view.bringSubviewToFront(subtitleStackView) subtitleStackView.isHidden = !SubtitleSettingsManager.shared.settings.enabled - - if let segs = localSkipSegments { - for s in segs { - if s.0 == "op" { - self.skipIntervals.op = CMTimeRange(start: CMTime(seconds: s.1, preferredTimescale: 600), end: CMTime(seconds: s.2, preferredTimescale: 600)) - } else if s.0 == "ed" { - self.skipIntervals.ed = CMTimeRange(start: CMTime(seconds: s.1, preferredTimescale: 600), end: CMTime(seconds: s.2, preferredTimescale: 600)) - } - } + if let info = preloadedSkipInfo { + if let s = info.opStart, let e = info.opEnd { self.skipIntervals.op = CMTimeRange(start: CMTime(seconds: s, preferredTimescale: 600), end: CMTime(seconds: e, preferredTimescale: 600)) } + if let s = info.edStart, let e = info.edEnd { self.skipIntervals.ed = CMTimeRange(start: CMTime(seconds: s, preferredTimescale: 600), end: CMTime(seconds: e, preferredTimescale: 600)) } self.updateSegments() } -if localSkipSegments == nil { - AniListMutation().fetchMalID(animeId: aniListID) { [weak self] result in + AniListMutation().fetchMalID(animeId: aniListID) { [weak self] result in switch result { case .success(let mal): self?.malID = mal @@ -418,7 +411,6 @@ if localSkipSegments == nil { Logger.shared.log("Unable to fetch MAL ID: \(error)",type:"Error") } } - } for control in controlsToHide { originalHiddenStates[control] = control.isHidden @@ -3879,4 +3871,4 @@ class GradientBlurButton: UIButton { cleanupVisualEffects() super.removeFromSuperview() } -} \ No newline at end of file +} diff --git a/Sora/Utlis & Misc/DownloadUtils/DownloadModels.swift b/Sora/Utlis & Misc/DownloadUtils/DownloadModels.swift index 685a943..0e665ef 100644 --- a/Sora/Utlis & Misc/DownloadUtils/DownloadModels.swift +++ b/Sora/Utlis & Misc/DownloadUtils/DownloadModels.swift @@ -55,6 +55,18 @@ enum DownloadType: String, Codable { case .episode: return "Episode" } + +// MARK: - Skip Information +struct SkipInfo: Codable, Equatable { + let opStart: Double? + let opEnd: Double? + let edStart: Double? + let edEnd: Double? + let introURL: String? + let outroURL: String? +} + + } } @@ -70,6 +82,7 @@ struct DownloadedAsset: Identifiable, Codable, Equatable { // New fields for subtitle support let subtitleURL: URL? let localSubtitleURL: URL? + let skipInfo: SkipInfo? // For caching purposes, but not stored as part of the codable object private var _cachedFileSize: Int64? = nil @@ -413,6 +426,7 @@ struct AssetMetadata: Codable { let showPosterURL: URL? // Main show poster URL (distinct from episode-specific images) let episodeTitle: String? let seasonNumber: Int? + let anilistId: Int? init( title: String, diff --git a/Sora/Utlis & Misc/JSLoader/Downloads/JSController+Downloader.swift b/Sora/Utlis & Misc/JSLoader/Downloads/JSController+Downloader.swift index f39e824..3320f30 100644 --- a/Sora/Utlis & Misc/JSLoader/Downloads/JSController+Downloader.swift +++ b/Sora/Utlis & Misc/JSLoader/Downloads/JSController+Downloader.swift @@ -14,29 +14,28 @@ struct DownloadRequest { let headers: [String: String] let title: String? let imageURL: URL? - let aniListID: Int? let isEpisode: Bool let showTitle: String? let season: Int? let episode: Int? let subtitleURL: URL? let showPosterURL: URL? + let anilistId: Int? init(url: URL, headers: [String: String], title: String? = nil, imageURL: URL? = nil, - aniListID: Int? = nil, isEpisode: Bool = false, showTitle: String? = nil, season: Int? = nil, - episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil) { + episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil, anilistId: Int? = nil) { self.url = url self.headers = headers self.title = title self.imageURL = imageURL - self.aniListID = aniListID self.isEpisode = isEpisode self.showTitle = showTitle self.season = season self.episode = episode self.subtitleURL = subtitleURL self.showPosterURL = showPosterURL + self.anilistId = anilistId } } @@ -54,12 +53,14 @@ struct QualityOption { extension JSController { - func downloadWithM3U8Support(url: URL, headers: [String: String], title: String? = nil, imageURL: URL? = nil, aniListID: Int? = nil, isEpisode: Bool = false, showTitle: String? = nil, season: Int? = nil, episode: Int? = nil, + func downloadWithM3U8Support(url: URL, headers: [String: String], title: String? = nil, + imageURL: URL? = nil, isEpisode: Bool = false, + showTitle: String? = nil, season: Int? = nil, episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil, completionHandler: ((Bool, String) -> Void)? = nil) { let request = DownloadRequest( - url: url, headers: headers, title: title, imageURL: imageURL, aniListID: aniListID, + url: url, headers: headers, title: title, imageURL: imageURL, isEpisode: isEpisode, showTitle: showTitle, season: season, episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL ) @@ -95,7 +96,7 @@ extension JSController { if let qualityURL = URL(string: selectedQuality.url) { let qualityRequest = DownloadRequest( url: qualityURL, headers: request.headers, title: request.title, - imageURL: request.imageURL, aniListID: request.aniListID, isEpisode: request.isEpisode, + imageURL: request.imageURL, isEpisode: request.isEpisode, showTitle: request.showTitle, season: request.season, episode: request.episode, subtitleURL: request.subtitleURL, showPosterURL: request.showPosterURL @@ -123,12 +124,13 @@ extension JSController { func downloadMP4(url: URL, headers: [String: String], title: String? = nil, - aniListID: Int? = nil, imageURL: URL? = nil, aniListID: Int? = nil, isEpisode: Bool = false, showTitle: String? = nil, season: Int? = nil, episode: Int? = nil, + imageURL: URL? = nil, isEpisode: Bool = false, + showTitle: String? = nil, season: Int? = nil, episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil, completionHandler: ((Bool, String) -> Void)? = nil) { let request = DownloadRequest( - url: url, headers: headers, title: title, imageURL: imageURL, aniListID: aniListID, + url: url, headers: headers, title: title, imageURL: imageURL, isEpisode: isEpisode, showTitle: showTitle, season: season, episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL ) @@ -399,7 +401,6 @@ extension JSController { private func downloadWithOriginalMethod(request: DownloadRequest, completionHandler: ((Bool, String) -> Void)?) { self.startDownload( url: request.url, - aniListID: request.aniListID, headers: request.headers, title: request.title, imageURL: request.imageURL, @@ -409,6 +410,7 @@ extension JSController { episode: request.episode, subtitleURL: request.subtitleURL, showPosterURL: request.showPosterURL, + anilistId: request.anilistId, completionHandler: completionHandler ) } @@ -532,4 +534,4 @@ extension JSController { private func logQualitySelectionResult(quality: QualityOption, preference: String) { Logger.shared.log("Quality selected: \(quality.name) (preference: \(preference))", type: "Download") } -} \ No newline at end of file +} diff --git a/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift b/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift index 15d0959..754a889 100644 --- a/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift +++ b/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift @@ -104,7 +104,6 @@ extension JSController { /// - completionHandler: Optional callback for download status func startDownload( url: URL, - aniListID: Int? = nil, headers: [String: String] = [:], title: String? = nil, imageURL: URL? = nil, @@ -127,7 +126,6 @@ extension JSController { imageURL: imageURL, module: module, isEpisode: isEpisode, - aniListID: aniListID, showTitle: showTitle, season: season, episode: episode, @@ -179,8 +177,7 @@ extension JSController { subtitleURL: subtitleURL, asset: asset, headers: headers, - module: module, // Pass the module to store it for queue processing - aniListID: aniListID + module: module // Pass the module to store it for queue processing ) // Add to the download queue @@ -279,7 +276,6 @@ extension JSController { imageURL: queuedDownload.imageURL, module: module, isEpisode: queuedDownload.type == .episode, - aniListID: queuedDownload.aniListID, showTitle: queuedDownload.metadata?.showTitle, season: queuedDownload.metadata?.season, episode: queuedDownload.metadata?.episode, @@ -933,13 +929,6 @@ extension JSController { } } DownloadPersistence.delete(id: asset.id) - - // Remove AniSkip sidecar if present - if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { - let dir = appSupport.appendingPathComponent("SoraDownloads", isDirectory: true) - let sidecar = dir.appendingPathComponent("aniskip-\(asset.id.uuidString).json") - try? FileManager.default.removeItem(at: sidecar) - } DispatchQueue.main.async { [weak self] in self?.savedAssets = DownloadPersistence.load() self?.objectWillChange.send() @@ -1219,13 +1208,32 @@ extension JSController: AVAssetDownloadDelegate { // Add to saved assets and save DownloadPersistence.upsert(newAsset) - - // Fetch and save AniSkip OP/ED markers as a sidecar (non-blocking, optional) - if let isEp = download.metadata?.episode, isEp > 0 { - let epNumber = isEp - fetchAndSaveAniSkipSidecar(aniListID: download.aniListID, episode: epNumber, assetID: newAsset.id.uuidString) + // Fetch skip info in background if we know AniList ID & episode number + if let meta = newAsset.metadata, let aId = meta.anilistId, let ep = meta.episodeNumber { + self.fetchSkipInfo(anilistId: aId, episodeNumber: ep) { info in + guard let info = info else { return } + let updated = DownloadedAsset( + id: newAsset.id, + name: newAsset.name, + downloadDate: newAsset.downloadDate, + originalURL: newAsset.originalURL, + localURL: newAsset.localURL, + imageURL: newAsset.imageURL, + fileSize: newAsset.fileSize, + type: newAsset.type, + progress: newAsset.progress, + headers: newAsset.headers, + referer: newAsset.referer, + userAgent: newAsset.userAgent, + metadata: newAsset.metadata, + subtitleURL: newAsset.subtitleURL, + localSubtitleURL: newAsset.localSubtitleURL, + skipInfo: info + ) + DownloadPersistence.upsert(updated) + } } -DispatchQueue.main.async { [weak self] in + DispatchQueue.main.async { [weak self] in self?.savedAssets = DownloadPersistence.load() self?.objectWillChange.send() } @@ -1560,7 +1568,6 @@ struct JSActiveDownload: Identifiable, Equatable { var asset: AVURLAsset? var headers: [String: String] var module: ScrapingModule? // Add module property to store ScrapingModule - var aniListID: Int? = nil // Computed property to get the current task state var taskState: URLSessionTask.State { @@ -1604,8 +1611,7 @@ struct JSActiveDownload: Identifiable, Equatable { subtitleURL: URL? = nil, asset: AVURLAsset? = nil, headers: [String: String] = [:], - module: ScrapingModule? = nil, - aniListID: Int? = nil // Add module parameter to initializer + module: ScrapingModule? = nil // Add module parameter to initializer ) { self.id = id self.originalURL = originalURL @@ -1665,57 +1671,35 @@ enum DownloadQueueStatus: Equatable { case downloading /// Download has been completed case completed + +// MARK: - Offline Skip Info (lightweight) +private struct _AniSkipEntry: Codable { let interval: _Interval } +private struct _Interval: Codable { let startTime: Double; let endTime: Double } +private struct _AniSkipAPIResponse: Codable { let found: Bool; let results: [String:[_AniSkipEntry]]? } + +private func fetchSkipInfo(anilistId: Int, episodeNumber: Int, completion: @escaping (SkipInfo?) -> Void) { + // Convert AniList -> MAL + AniListMutation().fetchMalID(animeId: anilistId) { res in + guard case .success(let malId) = res, let malId = malId else { completion(nil); return } + // Build requests for OP and ED + let types = ["op","ed"] + var info = SkipInfo(opStart: nil, opEnd: nil, edStart: nil, edEnd: nil, introURL: nil, outroURL: nil) + let group = DispatchGroup() + for t in types { + group.enter() + let urlStr = "https://api.aniskip.com/v2/skip-times/" + String(malId) + "?episode=" + String(episodeNumber) + "&types=" + t + "&anilistID=" + String(anilistId) + guard let url = URL(string: urlStr) else { group.leave(); continue } + URLSession.shared.dataTask(with: url) { data, _, _ in + defer { group.leave() } + guard let data = data, let resp = try? JSONDecoder().decode(_AniSkipAPIResponse.self, from: data), resp.found else { return } + if let entries = resp.results?[t], let first = entries.first { + if t == "op" { info = SkipInfo(opStart: first.interval.startTime, opEnd: first.interval.endTime, edStart: info.edStart, edEnd: info.edEnd, introURL: nil, outroURL: nil) } + if t == "ed" { info = SkipInfo(opStart: info.opStart, opEnd: info.opEnd, edStart: first.interval.startTime, edEnd: first.interval.endTime, introURL: nil, outroURL: nil) } + } + }.resume() + } + group.notify(queue: .main) { completion((info.opStart != nil || info.edStart != nil) ? info : nil) } + } } - // MARK: - AniSkip Sidecar - private func fetchAndSaveAniSkipSidecar(aniListID: Int?, episode: Int, assetID: String) { - guard let ani = aniListID else { return } - AniListMutation().fetchMalID(animeId: ani) { result in - switch result { - case .success(let mal): - let types = ["op", "ed"] - var segments: [(String, Double, Double)] = [] - let group = DispatchGroup() - for t in types { - group.enter() - if let url = URL(string: "https://api.aniskip.com/v2/skip-times/\(mal)/\(episode)?types=\(t)&episodeLength=0") { - URLSession.shared.dataTask(with: url) { data, _, _ in - defer { group.leave() } - guard let d = data, - let resp = try? JSONDecoder().decode(AniSkipResponse.self, from: d), - resp.found, - let interval = resp.results.first?.interval else { return } - segments.append((t, interval.startTime, interval.endTime)) - }.resume() - } else { - group.leave() - } - } - group.notify(queue: .global()) { - guard !segments.isEmpty else { return } - let payload: [String: Any] = [ - "provider": "aniskip", - "malId": mal, - "episode": episode, - "segments": segments.map { ["type": $0.0, "start": $0.1, "end": $0.2] }, - "fetchedAt": ISO8601DateFormatter().string(from: Date()), - "v": 1 - ] - do { - if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { - let dir = appSupport.appendingPathComponent("SoraDownloads", isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - let file = dir.appendingPathComponent("aniskip-\(assetID).json") - let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted]) - try data.write(to: file, options: .atomic) - Logger.shared.log("Saved AniSkip sidecar: \(file.lastPathComponent)", type: "Download") - } - } catch { - Logger.shared.log("Failed to save AniSkip sidecar: \(error)", type: "Error") - } - } - case .failure: - break - } - } - } \ No newline at end of file +} diff --git a/Sora/Utlis & Misc/JSLoader/Downloads/JSController-StreamTypeDownload.swift b/Sora/Utlis & Misc/JSLoader/Downloads/JSController-StreamTypeDownload.swift index 283c549..d6e00a2 100644 --- a/Sora/Utlis & Misc/JSLoader/Downloads/JSController-StreamTypeDownload.swift +++ b/Sora/Utlis & Misc/JSLoader/Downloads/JSController-StreamTypeDownload.swift @@ -24,7 +24,21 @@ extension JSController { /// - episode: Episode number (optional) /// - subtitleURL: Optional subtitle URL to download after video (optional) /// - completionHandler: Called when the download is initiated or fails - func downloadWithStreamTypeSupport(url: URL, headers: [String: String], title: String? = nil, imageURL: URL? = nil, module: ScrapingModule, isEpisode: Bool = false, aniListID: Int? = nil, showTitle: String? = nil, season: Int? = nil, episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil, completionHandler: ((Bool, String) -> Void)? = nil) { + func downloadWithStreamTypeSupport( + url: URL, + headers: [String: String], + title: String? = nil, + imageURL: URL? = nil, + module: ScrapingModule, + isEpisode: Bool = false, + showTitle: String? = nil, + season: Int? = nil, + episode: Int? = nil, + anilistId: Int? = nil, + subtitleURL: URL? = nil, + showPosterURL: URL? = nil, + completionHandler: ((Bool, String) -> Void)? = nil + ) { let streamType = module.metadata.streamType.lowercased() if streamType == "hls" || streamType == "m3u8" || url.absoluteString.contains(".m3u8") { @@ -34,7 +48,6 @@ extension JSController { headers: headers, title: title, imageURL: imageURL, - aniListID: aniListID, isEpisode: isEpisode, showTitle: showTitle, season: season, @@ -50,7 +63,6 @@ extension JSController { headers: headers, title: title, imageURL: imageURL, - aniListID: aniListID, isEpisode: isEpisode, showTitle: showTitle, season: season, @@ -61,4 +73,4 @@ extension JSController { ) } } -} \ No newline at end of file +} diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index 38aaaab..31ae0c4 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -6,7 +6,6 @@ // import AVKit -import Foundation import NukeUI import SwiftUI @@ -243,24 +242,7 @@ struct DownloadView: View { metadataUrl: "" ) - - var localSkipSegments: [(String, Double, Double)]? = nil - if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { - let dir = appSupport.appendingPathComponent("SoraDownloads", isDirectory: true) - let sidecar = dir.appendingPathComponent("aniskip-\(asset.id.uuidString).json") - if let data = try? Data(contentsOf: sidecar), - let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let segs = obj["segments"] as? [[String: Any]] { - localSkipSegments = segs.compactMap { dict in - if let t = dict["type"] as? String, - let st = dict["start"] as? Double, - let en = dict["end"] as? Double { return (t, st, en) } - return nil - } - if localSkipSegments?.isEmpty == true { localSkipSegments = nil } - } - } -let customPlayer = CustomMediaPlayerViewController( + let customPlayer = CustomMediaPlayerViewController( module: dummyModule, urlString: asset.localURL.absoluteString, fullUrl: asset.originalURL.absoluteString, @@ -295,7 +277,6 @@ let customPlayer = CustomMediaPlayerViewController( aniListID: 0, totalEpisodes: asset.metadata?.episode ?? 0, episodeImageUrl: asset.metadata?.posterURL?.absoluteString ?? "", - localSkipSegments: localSkipSegments, headers: nil ) @@ -1567,4 +1548,4 @@ struct SearchableStyleModifier: ViewModifier { ) ) } -} \ No newline at end of file +} diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index 31af3b2..c84a2a3 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -691,7 +691,13 @@ private extension EpisodeCell { let fullEpisodeTitle = episodeTitle.isEmpty ? baseTitle : "\(baseTitle): \(episodeTitle)" let animeTitle = parentTitle.isEmpty ? "Unknown Anime" : parentTitle - jsController.downloadWithStreamTypeSupport(url: url, headers: headers, title: fullEpisodeTitle, imageURL: episodeThumbnailURL, module: module, isEpisode: true, aniListID: itemID, + jsController.downloadWithStreamTypeSupport( + url: url, + headers: headers, + title: fullEpisodeTitle, + imageURL: episodeThumbnailURL, + module: module, + isEpisode: true, showTitle: animeTitle, season: 1, episode: episodeID + 1, diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index a8300d7..c2920aa 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -2233,7 +2233,6 @@ struct MediaInfoView: View { imageURL: episodeThumbnailURL, module: self.module, isEpisode: true, - aniListID: self.itemID, showTitle: self.title, season: 1, episode: episode.number,