mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
fix
This commit is contained in:
parent
848776a0b1
commit
f0f934fb62
1 changed files with 156 additions and 71 deletions
|
|
@ -1265,7 +1265,7 @@ extension JSController: AVAssetDownloadDelegate {
|
|||
}
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Unable to fetch MAL ID: \(error)", type: "Error")
|
||||
Logger.shared.log("[SkipSidecar] Missing MAL ID for AniSkip v2 request", type: "Download")
|
||||
Logger.shared.log("[SkipSidecar] Missing MAL ID for AniSkip request", type: "Download")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1728,84 +1728,169 @@ extension JSController {
|
|||
func fetchSkipTimestampsFor(request: JSActiveDownload,
|
||||
persistentURL: URL,
|
||||
completion: @escaping (Bool) -> Void) {
|
||||
// Use MAL ID only
|
||||
guard let malID = request.malID else {
|
||||
Logger.shared.log("[SkipSidecar] No MAL ID available for AniSkip v2 request", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let episodeNumber = request.metadata?.episode else {
|
||||
Logger.shared.log("[SkipSidecar] Missing episode number for AniSkip v2 request", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Build v2 URL (op,ed only)
|
||||
let mal = malID
|
||||
let type = "op,ed"
|
||||
|
||||
guard let url = URL(string: "https://api.aniskip.com/v2/skip-times/\(mal)/\(episodeNumber)?types=\(type)&episodeLength=0") else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
if let e = error {
|
||||
Logger.shared.log("[SkipSidecar] AniSkip v2 (MAL) fetch error: \(e.localizedDescription)", type: "Download")
|
||||
// Attempt to obtain the MAL ID. If it's not present on the request but an AniList ID is,
|
||||
// use AniListMutation to fetch it. This mirrors the logic used by CustomMediaPlayer.
|
||||
func proceed(with malID: Int) {
|
||||
// Ensure the episode number is available before making the AniSkip request
|
||||
guard let episodeNumber = request.metadata?.episode else {
|
||||
Logger.shared.log("[SkipSidecar] Missing episode number for AniSkip request", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
|
||||
// Build v2 URL and include separate query items for each type. The AniSkip API expects
|
||||
// repeated `types` parameters, not a comma-separated list. Using URLComponents ensures
|
||||
// proper encoding of the query items.
|
||||
var components = URLComponents()
|
||||
components.scheme = "https"
|
||||
components.host = "api.aniskip.com"
|
||||
components.path = "/v2/skip-times/\(malID)/\(episodeNumber)"
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "types", value: "op"),
|
||||
URLQueryItem(name: "types", value: "ed"),
|
||||
URLQueryItem(name: "episodeLength", value: "0")
|
||||
]
|
||||
guard let url = components.url else {
|
||||
Logger.shared.log("[SkipSidecar] Failed to construct AniSkip URL", type: "Download")
|
||||
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 {
|
||||
switch item.skip_type.lowercased() {
|
||||
case "op": opRange = (item.interval.start_time, item.interval.end_time)
|
||||
case "ed": edRange = (item.interval.start_time, item.interval.end_time)
|
||||
default: break
|
||||
// Log the exact URL being fetched to aid debugging
|
||||
Logger.shared.log("[SkipSidecar] Fetching AniSkip: \(url.absoluteString)", type: "Download")
|
||||
|
||||
// Perform the request and capture the response object so we can log status codes
|
||||
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let e = error {
|
||||
Logger.shared.log("[SkipSidecar] AniSkip (MAL) fetch error: \(e.localizedDescription)", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
if let http = response as? HTTPURLResponse {
|
||||
Logger.shared.log("[SkipSidecar] AniSkip response status: \(http.statusCode)", type: "Download")
|
||||
}
|
||||
guard let data = data else {
|
||||
Logger.shared.log("[SkipSidecar] AniSkip returned empty body", type: "Download")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Flexible decoder: supports both camelCase (skipType, startTime) and snake_case (skip_type, start_time)
|
||||
struct AniSkipV2Response: Decodable {
|
||||
struct Result: Decodable {
|
||||
struct Interval: Decodable {
|
||||
let startTime: Double
|
||||
let endTime: Double
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
if let start = try? c.decode(Double.self, forKey: .startTime),
|
||||
let end = try? c.decode(Double.self, forKey: .endTime) {
|
||||
startTime = start
|
||||
endTime = end
|
||||
} else {
|
||||
startTime = try c.decode(Double.self, forKey: .start_time)
|
||||
endTime = try c.decode(Double.self, forKey: .end_time)
|
||||
}
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case startTime
|
||||
case endTime
|
||||
case start_time
|
||||
case end_time
|
||||
}
|
||||
}
|
||||
let skipType: String
|
||||
let interval: Interval
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
if let st = try? c.decode(String.self, forKey: .skipType) {
|
||||
skipType = st
|
||||
} else {
|
||||
skipType = try c.decode(String.self, forKey: .skip_type)
|
||||
}
|
||||
interval = try c.decode(Interval.self, forKey: .interval)
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case skipType
|
||||
case skip_type
|
||||
case interval
|
||||
}
|
||||
}
|
||||
let found: Bool
|
||||
let results: [Result]?
|
||||
}
|
||||
|
||||
var opRange: (Double, Double)? = nil
|
||||
var edRange: (Double, Double)? = nil
|
||||
|
||||
if let resp = try? JSONDecoder().decode(AniSkipV2Response.self, from: data), resp.found, let arr = resp.results {
|
||||
for item in arr {
|
||||
switch item.skipType.lowercased() {
|
||||
case "op": opRange = (item.interval.startTime, item.interval.endTime)
|
||||
case "ed": edRange = (item.interval.startTime, item.interval.endTime)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Log a small preview of the response to help debugging
|
||||
let preview = String(data: data, encoding: .utf8) ?? "<non-utf8>"
|
||||
Logger.shared.log("[SkipSidecar] AniSkip decode failed or not found. Body: \(preview.prefix(200))", type: "Download")
|
||||
}
|
||||
|
||||
// If no ranges were found, gracefully return without writing a sidecar
|
||||
if opRange == nil && edRange == nil {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 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": "mal",
|
||||
"malId": malID,
|
||||
"episode": episodeNumber,
|
||||
"createdAt": ISO8601DateFormatter().string(from: Date())
|
||||
]
|
||||
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)
|
||||
Logger.shared.log("[SkipSidecar] Wrote sidecar at: \(sidecar.path)", type: "Download")
|
||||
completion(true)
|
||||
} catch {
|
||||
Logger.shared.log("[SkipSidecar] Sidecar write error: \(error.localizedDescription)", type: "Download")
|
||||
completion(false)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
if let existingMalID = request.malID {
|
||||
// Already have the MAL ID; proceed directly
|
||||
proceed(with: existingMalID)
|
||||
return
|
||||
}
|
||||
// Attempt to fetch MAL ID using AniList ID if available
|
||||
if let aniListId = request.aniListID {
|
||||
AniListMutation().fetchMalID(animeId: aniListId) { result in
|
||||
switch result {
|
||||
case .success(let mal):
|
||||
// Save the fetched MAL ID to the download object if possible (JSActiveDownload is a struct so we cannot mutate here)
|
||||
// but we can proceed using the fetched value. It is logged by CustomMediaPlayer too.
|
||||
proceed(with: mal)
|
||||
case .failure(let error):
|
||||
Logger.shared.log("Unable to fetch MAL ID: \(error)", type: "Error")
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
if opRange == nil && edRange == nil {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 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": "mal",
|
||||
"malId": mal,
|
||||
"episode": episodeNumber,
|
||||
"createdAt": ISO8601DateFormatter().string(from: Date())
|
||||
]
|
||||
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)
|
||||
Logger.shared.log("[SkipSidecar] Wrote sidecar at: \(sidecar.path)", type: "Download")
|
||||
completion(true)
|
||||
} catch {
|
||||
Logger.shared.log("[SkipSidecar] Sidecar write error: \(error.localizedDescription)", type: "Download")
|
||||
completion(false)
|
||||
}
|
||||
}.resume()
|
||||
return
|
||||
}
|
||||
// No MAL ID or AniList ID available; cannot proceed
|
||||
Logger.shared.log("[SkipSidecar] No MAL ID or AniList ID available for AniSkip request", type: "Download")
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue