I'm going insane x2

This commit is contained in:
scigward 2025-08-20 05:34:54 +03:00
parent cb39c0ccde
commit 72791f1ac4
2 changed files with 95 additions and 102 deletions

View file

@ -116,8 +116,6 @@ extension JSController {
module: ScrapingModule? = nil,
completionHandler: ((Bool, String) -> Void)? = nil
) {
Logger.shared.log("Start download | title: \(title ?? "-") | isEpisode: \(isEpisode) | showTitle: \(showTitle ?? "-") | season: \(season?.description ?? "-") | episode: \(episode?.description ?? "-")", type: "Download")
// If a module is provided, use the stream type aware download
if let module = module {
// Use the stream type aware download method
@ -1221,12 +1219,11 @@ extension JSController: AVAssetDownloadDelegate {
// If there's a subtitle URL, download it now that the video is saved
// Also fetch OP/ED skip timestamps in parallel and save simple sidecar JSON next to the video
if download.metadata?.episode != nil && download.type == .episode {
Logger.shared.log("OP/ED: fetching skip timestamps after download save", type: "Download")
self.fetchSkipTimestampsFor(request: download, persistentURL: persistentURL) { ok in
fetchSkipTimestampsFor(request: download, persistentURL: persistentURL) { ok in
if ok {
print("[SkipSidecar] Saved OP/ED sidecar for episode \(download.metadata?.episode ?? -1) at: \(persistentURL.path)")
} else {
print("[SkipSidecar] Failed to save sidecar for episode \(download.metadata?.episode ?? -1)")
Logger.shared.log("[SkipSidecar] Failed to save sidecar for episode \(download.metadata?.episode ?? -1)")
}
}
}
@ -1677,125 +1674,126 @@ enum DownloadQueueStatus: Equatable {
/// Fetches OP & ED skip timestamps (AniSkip) and writes a minimal sidecar JSON next to the persisted video.
/// Uses MAL id for fillers when available; falls back to AniList otherwise.
private
func self.fetchSkipTimestampsFor(request: JSActiveDownload, persistentURL: URL, completion: @escaping (Bool) -> Void) {
let ep = request.episode ?? 0
let ani = request.metadata?.aniListID
let mal = request.metadata?.malID
Logger.shared.log("OP/ED: preparing fetch | anilistID: \(ani?.description ?? "nil") | malID: \(mal?.description ?? "nil") | episode: \(ep)", type: "Download")
var urls: [URL] = []
if let anilistID = ani, let url = URL(string: "https://api.aniskip.com/v2/skip-times/anilist/\(anilistID)/\(ep)?types[]=op&types[]=ed") {
urls.append(url)
/// Fetch OP/ED skip timestamps for a finished download using **MAL** only.
/// - Parameters:
/// - request: The active download metadata (must contain `malID` and `metadata?.episode`)
/// - persistentURL: Final URL where the video was persisted
/// - completion: Called with `true` on success, `false` on failure
func fetchSkipTimestampsFor(request: JSActiveDownload, persistentURL: URL, completion: @escaping (Bool) -> Void) {
// Validate MAL ID and Episode
guard let malID = request.malID else {
Logger.shared.log("[SkipSidecar] No MAL ID on request → skipping OP/ED fetching", type: "Download")
completion(false)
return
}
if let malID = mal, let url = URL(string: "https://api.aniskip.com/v2/skip-times/mal/\(malID)/\(ep)?types[]=op&types[]=ed") {
urls.append(url)
}
if let anilistID = ani, let url = URL(string: "https://api.aniskip.com/v1/skip-times/\(anilistID)/\(ep)?types=op&types=ed") {
urls.append(url)
}
if let malID = mal, let url = URL(string: "https://api.aniskip.com/v1/skip-times/\(malID)/\(ep)?types=op&types=ed") {
urls.append(url)
}
if urls.isEmpty {
Logger.shared.log("OP/ED: no IDs available to fetch skip timestamps.", type: "Download")
guard let episodeNumber = request.metadata?.episode else {
Logger.shared.log("[SkipSidecar] Missing episode number on request → skipping OP/ED fetching", type: "Download")
completion(false)
return
}
func attempt(_ idx: Int) {
if idx >= urls.count {
Logger.shared.log("OP/ED: no skip timestamps found after trying all endpoints.", type: "Download")
// Log that we are about to fetch
Logger.shared.log("[SkipSidecar] Starting fetch for MAL=\(malID) episode=\(episodeNumber)", type: "Download")
// Build AniSkip v2 endpoint (MAL provider only)
// Example: https://api.aniskip.com/v2/skip-times/mal/{malID}/{episode}?types=op,ed
guard let url = URL(string: "https://api.aniskip.com/v2/skip-times/mal/\(malID)/\(episodeNumber)?types=op,ed") else {
Logger.shared.log("[SkipSidecar] Failed to build AniSkip URL for malID=\(malID) ep=\(episodeNumber)", type: "Error")
completion(false)
return
}
var req = URLRequest(url: url)
req.timeoutInterval = 15
let task = URLSession.shared.dataTask(with: req) { data, response, error in
// Handle networking errors
if let error = error {
Logger.shared.log("[SkipSidecar] Network error while fetching skip-times: \(error.localizedDescription)", type: "Error")
completion(false)
return
}
guard let http = response as? HTTPURLResponse else {
Logger.shared.log("[SkipSidecar] No HTTP response while fetching skip-times", type: "Error")
completion(false)
return
}
guard (200...299).contains(http.statusCode), let data = data else {
let size = data?.count ?? 0
Logger.shared.log("[SkipSidecar] Bad status \(http.statusCode). Body bytes=\(size)", type: "Error")
completion(false)
return
}
let url = urls[idx]
Logger.shared.log("OP/ED: fetching from \(url.absoluteString)", type: "Download")
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "Accept")
req.setValue("Sora/1.0 (skip-fetch)", forHTTPHeaderField: "User-Agent")
URLSession.shared.dataTask(with: req) { data, response, error in
if let error = error {
Logger.shared.log("OP/ED: request error: \(error.localizedDescription)", type: "Error")
DispatchQueue.main.async { attempt(idx + 1) }
return
}
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
Logger.shared.log("OP/ED: HTTP \(http.statusCode) from AniSkip", type: "Error")
DispatchQueue.main.async { attempt(idx + 1) }
return
}
guard let data = data else {
Logger.shared.log("OP/ED: empty response", type: "Error")
DispatchQueue.main.async { attempt(idx + 1) }
// Parse response
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
guard
let dict = json as? [String: Any],
let found = dict["found"] as? Bool,
found == true,
let results = dict["results"] as? [[String: Any]]
else {
Logger.shared.log("[SkipSidecar] AniSkip returned empty results for malID=\(malID) ep=\(episodeNumber)", type: "Download")
completion(false)
return
}
var opStart: Double?
var opEnd: Double?
var edStart: Double?
var edEnd: Double?
var opStart: Double? = nil
var opEnd: Double? = nil
var edStart: Double? = nil
var edEnd: Double? = nil
do {
let obj = try JSONSerialization.jsonObject(with: data, options: [])
if let dict = obj as? [String: Any] {
if let results = dict["results"] as? [[String: Any]] {
for entry in results {
guard let type = entry["skipType"] as? String,
let interval = entry["interval"] as? [String: Any],
let s = interval["startTime"] as? Double,
let e = interval["endTime"] as? Double
else { continue }
if type.lowercased() == "op" { opStart = s; opEnd = e }
else if type.lowercased() == "ed" { edStart = s; edEnd = e }
}
} else if let found = dict["found"] as? Bool, found, let results = dict["results"] as? [[String: Any]] {
for entry in results {
guard let type = entry["skipType"] as? String,
let interval = entry["interval"] as? [String: Any],
let s = interval["startTime"] as? Double,
let e = interval["endTime"] as? Double
else { continue }
if type.lowercased() == "op" { opStart = s; opEnd = e }
else if type.lowercased() == "ed" { edStart = s; edEnd = e }
}
}
for entry in results {
guard
let interval = entry["interval"] as? [String: Any],
let start = interval["start_time"] as? Double,
let end = interval["end_time"] as? Double,
let type = entry["skip_type"] as? String
else { continue }
if type.lowercased() == "op" {
opStart = start; opEnd = end
} else if type.lowercased() == "ed" {
edStart = start; edEnd = end
}
} catch {
Logger.shared.log("OP/ED: JSON parse error: \(error.localizedDescription)", type: "Error")
}
let hasAny = (opStart != nil && opEnd != nil) || (edStart != nil && edEnd != nil)
if !hasAny {
Logger.shared.log("OP/ED: no intervals in response, trying next endpoint...", type: "Download")
DispatchQueue.main.async { attempt(idx + 1) }
if opStart == nil && edStart == nil {
Logger.shared.log("[SkipSidecar] No OP/ED entries found in results for malID=\(malID) ep=\(episodeNumber)", type: "Download")
completion(false)
return
}
var payload: [String: Any] = [:]
// Build sidecar JSON to store next to the video file
let baseURL = persistentURL.deletingPathExtension()
let sidecarURL = baseURL.appendingPathExtension("skip.json")
var payload: [String: Any] = [
"malID": malID,
"episode": episodeNumber,
"source": "AniSkip",
"version": 2
]
if let s = opStart, let e = opEnd { payload["op"] = ["start": s, "end": e] }
if let s = edStart, let e = edEnd { payload["ed"] = ["start": s, "end": e] }
if let anilistID = ani { payload["anilistId"] = anilistID }
if let malID = mal { payload["malId"] = malID }
payload["episode"] = ep
let sidecarURL = persistentURL.deletingPathExtension().appendingPathExtension("skips.json")
do {
let out = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
try out.write(to: sidecarURL, options: .atomic)
Logger.shared.log("OP/ED: saved sidecar at \(sidecarURL.lastPathComponent)", type: "Download")
let sidecarData = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
try sidecarData.write(to: sidecarURL, options: [.atomic])
Logger.shared.log("[SkipSidecar] Saved skip sidecar → \(sidecarURL.lastPathComponent)", type: "Download")
completion(true)
} catch {
Logger.shared.log("OP/ED: failed to save sidecar: \(error.localizedDescription)", type: "Error")
Logger.shared.log("[SkipSidecar] Failed to save sidecar \(sidecarURL.lastPathComponent): \(error.localizedDescription)", type: "Error")
completion(false)
}
}.resume()
} catch {
Logger.shared.log("[SkipSidecar] JSON parse error: \(error.localizedDescription)", type: "Error")
completion(false)
}
}
attempt(0)
task.resume()
}

View file

@ -588,8 +588,6 @@ private extension EpisodeCell {
}
isDownloading = true
let animeTitle = parentTitle.isEmpty ? "Unknown Anime" : parentTitle
Logger.shared.log("Download kickoff → show=\(animeTitle), ep=\(self.episodeID + 1), malID=\(String(describing: self.malID)))", type: "Download")
let downloadID = UUID()
DropManager.shared.downloadStarted(episodeNumber: episodeID + 1)
@ -618,8 +616,6 @@ private extension EpisodeCell {
}
func tryNextDownloadMethod(methodIndex: Int, downloadID: UUID, softsub: Bool) {
Logger.shared.log("Trying download method index=\(methodIndex) for ep=\(self.episodeID + 1)", type: "Download")
guard isDownloading else { return }
switch methodIndex {
@ -658,8 +654,6 @@ private extension EpisodeCell {
methodIndex: Int,
softsub: Bool
) {
Logger.shared.log("handleDownloadResult received: sources=\(result.sources?.count ?? 0), streams=\(result.streams?.count ?? 0)", type: "Download")
guard isDownloading else { return }
if let sources = result.sources, !sources.isEmpty {
@ -698,7 +692,7 @@ private extension EpisodeCell {
}
func startActualDownload(url: URL, streamUrl: String, downloadID: UUID, subtitleURL: URL? = nil) {
Logger.shared.log("Preparing actual download (streamUrl=\(streamUrl))", type: "Download")
let headers = createDownloadHeaders(for: url)
let episodeThumbnailURL = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl)
@ -708,6 +702,7 @@ private extension EpisodeCell {
let fullEpisodeTitle = episodeTitle.isEmpty ? baseTitle : "\(baseTitle): \(episodeTitle)"
let animeTitle = parentTitle.isEmpty ? "Unknown Anime" : parentTitle
Logger.shared.log("Download kickoff → show=\(animeTitle), ep=\(self.episodeID + 1), malID=\(String(describing: self.malID)), streamUrl=\(streamUrl)", type: "Download")
Logger.shared.log("Starting downloadWithStreamTypeSupport (MAL id pass-through) → mal=\(String(describing: malIDFromParent)), anilist=\(itemID), title=\(fullEpisodeTitle)", type: "Download");
jsController.downloadWithStreamTypeSupport(
url: url,