mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
Revert to "im going insane"
This commit is contained in:
parent
5a1d3ccedb
commit
947d7fcc82
2 changed files with 126 additions and 131 deletions
|
|
@ -978,7 +978,7 @@ extension JSController {
|
|||
|
||||
/// Checks if an asset file exists before attempting to play it
|
||||
/// - Parameter asset: The asset to verify
|
||||
/// - Returns: True if the file exists, false otherwise
|
||||
/// - Returns: true if the file exists, false otherwise
|
||||
func verifyAssetFileExists(_ asset: DownloadedAsset) -> Bool {
|
||||
let fileExists = FileManager.default.fileExists(atPath: asset.localURL.path)
|
||||
|
||||
|
|
@ -1223,7 +1223,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
if ok {
|
||||
print("[SkipSidecar] Saved OP/ED sidecar for episode \(download.metadata?.episode ?? -1) at: \(persistentURL.path)")
|
||||
} else {
|
||||
Logger.shared.log("[SkipSidecar] Failed to save sidecar for episode \(download.metadata?.episode ?? -1)")
|
||||
print("[SkipSidecar] Failed to save sidecar for episode \(download.metadata?.episode ?? -1)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1673,147 +1673,151 @@ enum DownloadQueueStatus: Equatable {
|
|||
// MARK: - Skip Sidecar (OP/ED) Fetch
|
||||
/// 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
|
||||
/// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
private func fetchSkipTimestampsFor(request: JSActiveDownload, persistentURL: URL, completion: @escaping (Bool)->Void)
|
||||
{
|
||||
// Determine preferred ID
|
||||
let epNumber = request.metadata?.episode ?? 0
|
||||
let useMAL = (request.isFiller == true) && (request.malID != nil)
|
||||
let idType = useMAL ? "mal" : "anilist"
|
||||
guard let seriesID = useMAL ? request.malID : request.aniListID else {
|
||||
print("[SkipSidecar] Missing series ID for AniSkip (MAL/AniList)")
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
// Single AniSkip v1 call for both OP/ED
|
||||
let url = URL(string: "https://api.aniskip.com/v1/skip-times/\(seriesID)/\(epNumber)?types=op&types=ed")!
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
if let e = error {
|
||||
print("[SkipSidecar] AniSkip fetch error: \(e.localizedDescription)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
var opStart: Double? = nil
|
||||
var opEnd: Double? = nil
|
||||
var edStart: Double? = nil
|
||||
var edEnd: Double? = nil
|
||||
|
||||
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
|
||||
guard let data = data else { completion(false); return }
|
||||
struct Resp: Decodable { let found: Bool; let results: [Res]? }
|
||||
struct Res: Decodable { let skip_type: String; let interval: Interval }
|
||||
struct Interval: Decodable { let start_time: Double; let end_time: Double }
|
||||
var opRange: (Double, Double)? = nil
|
||||
var edRange: (Double, Double)? = nil
|
||||
if let r = try? JSONDecoder().decode(Resp.self, from: data), r.found, let arr = r.results {
|
||||
for item in arr {
|
||||
if item.skip_type == "op" { opRange = (item.interval.start_time, item.interval.end_time) }
|
||||
if item.skip_type == "ed" { edRange = (item.interval.start_time, item.interval.end_time) }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if opRange == nil && edRange == nil { completion(false); return }
|
||||
// Determine sidecar path
|
||||
let fm = FileManager.default
|
||||
var dir = persistentURL.deletingLastPathComponent()
|
||||
var baseName = persistentURL.deletingPathExtension().lastPathComponent
|
||||
var isDir: ObjCBool = false
|
||||
if fm.fileExists(atPath: persistentURL.path, isDirectory: &isDir), isDir.boolValue {
|
||||
dir = persistentURL.deletingLastPathComponent()
|
||||
baseName = persistentURL.lastPathComponent
|
||||
}
|
||||
|
||||
// Build sidecar JSON to store next to the video file
|
||||
let baseURL = persistentURL.deletingPathExtension()
|
||||
let sidecarURL = baseURL.appendingPathExtension("skip.json")
|
||||
|
||||
let sidecar = dir.appendingPathComponent(baseName + ".skip.json")
|
||||
var payload: [String: Any] = [
|
||||
"malID": malID,
|
||||
"episode": episodeNumber,
|
||||
"source": "AniSkip",
|
||||
"version": 2
|
||||
"source": "aniskip",
|
||||
"idType": idType,
|
||||
"episode": epNumber,
|
||||
"createdAt": ISO8601DateFormatter().string(from: Date())
|
||||
]
|
||||
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 aid = request.aniListID { payload["anilistId"] = aid }
|
||||
if let mid = request.malID { payload["malId"] = mid }
|
||||
if let op = opRange { payload["op"] = ["start": op.0, "end": op.1] }
|
||||
if let ed = edRange { payload["ed"] = ["start": ed.0, "end": ed.1] }
|
||||
do {
|
||||
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")
|
||||
let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
|
||||
try data.write(to: sidecar, options: .atomic)
|
||||
print("[SkipSidecar] Wrote sidecar at: \(sidecar.path)")
|
||||
completion(true)
|
||||
} catch {
|
||||
Logger.shared.log("[SkipSidecar] Failed to save sidecar \(sidecarURL.lastPathComponent): \(error.localizedDescription)", type: "Error")
|
||||
print("[SkipSidecar] Sidecar write error: \(error.localizedDescription)")
|
||||
completion(false)
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("[SkipSidecar] JSON parse error: \(error.localizedDescription)", type: "Error")
|
||||
completion(false)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Temporary shim to resolve missing symbol error for `fetchSkipTimestampsFor(...)`.
|
||||
/// This instance method lives on `JSController` so unqualified calls inside the type resolve correctly.
|
||||
/// If a more complete global implementation exists elsewhere, consider delegating to it.
|
||||
// MARK: - AniSkip Sidecar (OP/ED) Fetch
|
||||
extension JSController {
|
||||
/// Fetch OP/ED skip timestamps sidecar for a finished download.
|
||||
/// Currently acts as a no-op fallback to avoid build failures when the symbol is unavailable.
|
||||
/// - Parameters:
|
||||
/// - request: The active download metadata.
|
||||
/// - 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) {
|
||||
Logger.shared.log("[SkipSidecar] Fallback: fetchSkipTimestampsFor not implemented in this build.", type: "Download")
|
||||
completion(false)
|
||||
/// 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.
|
||||
func fetchSkipTimestampsFor(request: JSActiveDownload,
|
||||
persistentURL: URL,
|
||||
completion: @escaping (Bool) -> Void) {
|
||||
// Determine preferred ID
|
||||
let epNumber = request.metadata?.episode ?? 0
|
||||
let useMAL = (request.isFiller == true) && (request.malID != nil)
|
||||
let idType = useMAL ? "mal" : "anilist"
|
||||
guard let seriesID = useMAL ? request.malID : request.aniListID else {
|
||||
print("[SkipSidecar] Missing series ID for AniSkip (MAL/AniList)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Single AniSkip v1 call for both OP/ED
|
||||
guard let url = URL(string: "https://api.aniskip.com/v1/skip-times/\(seriesID)/\(epNumber)?types=op&types=ed") else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
if let e = error {
|
||||
print("[SkipSidecar] AniSkip fetch error: \(e.localizedDescription)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
struct Resp: Decodable { let found: Bool; let results: [Res]? }
|
||||
struct Res: Decodable { let skip_type: String; let interval: Interval }
|
||||
struct Interval: Decodable { let start_time: Double; let end_time: Double }
|
||||
|
||||
var opRange: (Double, Double)? = nil
|
||||
var edRange: (Double, Double)? = nil
|
||||
|
||||
if let r = try? JSONDecoder().decode(Resp.self, from: data), r.found, let arr = r.results {
|
||||
for item in arr {
|
||||
if item.skip_type == "op" { opRange = (item.interval.start_time, item.interval.end_time) }
|
||||
if item.skip_type == "ed" { edRange = (item.interval.start_time, item.interval.end_time) }
|
||||
}
|
||||
}
|
||||
|
||||
if opRange == nil && edRange == nil {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine sidecar path next to the persisted video file
|
||||
let dir = persistentURL.deletingLastPathComponent()
|
||||
let baseName = persistentURL.deletingPathExtension().lastPathComponent
|
||||
let sidecar = dir.appendingPathComponent(baseName + ".skip.json")
|
||||
|
||||
var payload: [String: Any] = [
|
||||
"source": "aniskip",
|
||||
"idType": idType,
|
||||
"episode": epNumber,
|
||||
"createdAt": ISO8601DateFormatter().string(from: Date())
|
||||
]
|
||||
if let aid = request.aniListID { payload["anilistId"] = aid }
|
||||
if let mid = request.malID { payload["malId"] = mid }
|
||||
if let op = opRange { payload["op"] = ["start": op.0, "end": op.1] }
|
||||
if let ed = edRange { payload["ed"] = ["start": ed.0, "end": ed.1] }
|
||||
|
||||
do {
|
||||
let json = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
|
||||
try json.write(to: sidecar, options: .atomic)
|
||||
print("[SkipSidecar] Wrote sidecar at: \(sidecar.path)")
|
||||
completion(true)
|
||||
} catch {
|
||||
print("[SkipSidecar] Sidecar write error: \(error.localizedDescription)")
|
||||
completion(false)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -657,12 +657,10 @@ private extension EpisodeCell {
|
|||
guard isDownloading else { return }
|
||||
|
||||
if let sources = result.sources, !sources.isEmpty {
|
||||
Logger.shared.log("Download: received \(sources.count) source option(s)", type: "Download")
|
||||
if sources.count > 1 {
|
||||
showDownloadStreamSelectionAlert(streams: sources, downloadID: downloadID, subtitleURL: result.subtitles?.first)
|
||||
return
|
||||
} else if let streamUrl = sources[0]["streamUrl"] as? String, let url = URL(string: streamUrl) {
|
||||
Logger.shared.log("Download: auto-selecting first source", type: "Download")
|
||||
let subtitleURLString = sources[0]["subtitle"] as? String
|
||||
let subtitleURL = subtitleURLString.flatMap { URL(string: $0) }
|
||||
startActualDownload(url: url, streamUrl: streamUrl, downloadID: downloadID, subtitleURL: subtitleURL)
|
||||
|
|
@ -671,14 +669,12 @@ private extension EpisodeCell {
|
|||
}
|
||||
|
||||
if let streams = result.streams, !streams.isEmpty {
|
||||
Logger.shared.log("Download: received \(streams.count) stream URL(s)", type: "Download")
|
||||
if streams[0] == "[object Promise]" {
|
||||
tryNextDownloadMethod(methodIndex: methodIndex + 1, downloadID: downloadID, softsub: softsub)
|
||||
return
|
||||
}
|
||||
|
||||
if streams.count > 1 {
|
||||
Logger.shared.log("Download: user selection required among multiple streams", type: "Download")
|
||||
showDownloadStreamSelectionAlert(streams: streams, downloadID: downloadID, subtitleURL: result.subtitles?.first)
|
||||
return
|
||||
} else if let url = URL(string: streams[0]) {
|
||||
|
|
@ -692,8 +688,6 @@ private extension EpisodeCell {
|
|||
}
|
||||
|
||||
func startActualDownload(url: URL, streamUrl: String, downloadID: UUID, subtitleURL: URL? = nil) {
|
||||
|
||||
|
||||
let headers = createDownloadHeaders(for: url)
|
||||
let episodeThumbnailURL = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl)
|
||||
let showPosterImageURL = URL(string: showPosterURL ?? defaultBannerImage)
|
||||
|
|
@ -702,8 +696,6 @@ 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,
|
||||
headers: headers,
|
||||
|
|
@ -720,7 +712,6 @@ private extension EpisodeCell {
|
|||
malID: malIDFromParent,
|
||||
isFiller: isFiller
|
||||
) { success, message in
|
||||
Logger.shared.log("downloadWithStreamTypeSupport completed → success=\(success), message=\(message)", type: "Download")
|
||||
if success {
|
||||
Logger.shared.log("Started download for Episode \(self.episodeID + 1): \(self.episode)", type: "Download")
|
||||
AnalyticsManager.shared.sendEvent(
|
||||
|
|
|
|||
Loading…
Reference in a new issue