mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
I'm going insane x2
This commit is contained in:
parent
cb39c0ccde
commit
72791f1ac4
2 changed files with 95 additions and 102 deletions
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue