mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-18 23:22:08 +00:00
Test
This commit is contained in:
parent
970dbf2654
commit
acb62d7b78
6 changed files with 155 additions and 9 deletions
|
|
@ -300,6 +300,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
|
||||
asset = AVURLAsset(url: url)
|
||||
// Try to load OP/ED skip sidecar for local files
|
||||
self.loadLocalSkipSidecar(for: url)
|
||||
} else {
|
||||
Logger.shared.log("Loading remote URL: \(url.absoluteString)", type: "Debug")
|
||||
var request = URLRequest(url: url)
|
||||
|
|
@ -2648,6 +2650,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
|
||||
asset = AVURLAsset(url: url)
|
||||
// Try to load OP/ED skip sidecar for local files
|
||||
self.loadLocalSkipSidecar(for: url)
|
||||
} else {
|
||||
Logger.shared.log("Switching to remote URL: \(url.absoluteString)", type: "Debug")
|
||||
var request = URLRequest(url: url)
|
||||
|
|
@ -3864,3 +3868,39 @@ class GradientBlurButton: UIButton {
|
|||
super.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Load OP/ED skip data from a simple sidecar JSON saved next to the local video (if present)
|
||||
private func loadLocalSkipSidecar(for fileURL: URL) {
|
||||
let fm = FileManager.default
|
||||
var dir = fileURL.deletingLastPathComponent()
|
||||
var base = fileURL.deletingPathExtension().lastPathComponent
|
||||
var isDir: ObjCBool = false
|
||||
if fm.fileExists(atPath: fileURL.path, isDirectory: &isDir), isDir.boolValue {
|
||||
// HLS package directory: look in parent dir, use directory name
|
||||
dir = fileURL.deletingLastPathComponent()
|
||||
base = fileURL.lastPathComponent
|
||||
}
|
||||
let sidecar = dir.appendingPathComponent(base + ".skip.json")
|
||||
do {
|
||||
let data = try Data(contentsOf: sidecar)
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
if let op = (json["op"] as? [String: Any]), let s = op["start"] as? Double, let e = op["end"] as? Double {
|
||||
let range = CMTimeRange(start: CMTime(seconds: s, preferredTimescale: 600), end: CMTime(seconds: e, preferredTimescale: 600))
|
||||
self.skipIntervals.op = range
|
||||
print("[Player] Loaded local OP: \(s)-\(e)")
|
||||
}
|
||||
if let ed = (json["ed"] as? [String: Any]), let s = ed["start"] as? Double, let e = ed["end"] as? Double {
|
||||
let range = CMTimeRange(start: CMTime(seconds: s, preferredTimescale: 600), end: CMTime(seconds: e, preferredTimescale: 600))
|
||||
self.skipIntervals.ed = range
|
||||
print("[Player] Loaded local ED: \(s)-\(e)")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.updateSkipButtonsVisibility()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("[Player] No local skip sidecar found or failed to load: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -20,10 +20,13 @@ struct DownloadRequest {
|
|||
let episode: Int?
|
||||
let subtitleURL: URL?
|
||||
let showPosterURL: URL?
|
||||
let aniListID: Int?
|
||||
let malID: Int?
|
||||
let isFiller: Bool?
|
||||
|
||||
init(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) {
|
||||
episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil, aniListID: Int? = nil, malID: Int? = nil, isFiller: Bool? = nil) {
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
self.title = title
|
||||
|
|
@ -34,6 +37,9 @@ struct DownloadRequest {
|
|||
self.episode = episode
|
||||
self.subtitleURL = subtitleURL
|
||||
self.showPosterURL = showPosterURL
|
||||
self.aniListID = aniListID
|
||||
self.malID = malID
|
||||
self.isFiller = isFiller
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +66,8 @@ extension JSController {
|
|||
let request = DownloadRequest(
|
||||
url: url, headers: headers, title: title, imageURL: imageURL,
|
||||
isEpisode: isEpisode, showTitle: showTitle, season: season,
|
||||
episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL
|
||||
episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL,
|
||||
aniListID: aniListID, malID: malID, isFiller: isFiller
|
||||
)
|
||||
|
||||
logDownloadStart(request: request)
|
||||
|
|
@ -130,7 +137,8 @@ extension JSController {
|
|||
let request = DownloadRequest(
|
||||
url: url, headers: headers, title: title, imageURL: imageURL,
|
||||
isEpisode: isEpisode, showTitle: showTitle, season: season,
|
||||
episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL
|
||||
episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL,
|
||||
aniListID: aniListID, malID: malID, isFiller: isFiller
|
||||
)
|
||||
|
||||
downloadMP4(request: request, completionHandler: completionHandler)
|
||||
|
|
|
|||
|
|
@ -333,7 +333,10 @@ extension JSController {
|
|||
subtitleURL: queuedDownload.subtitleURL,
|
||||
asset: asset,
|
||||
headers: queuedDownload.headers,
|
||||
module: queuedDownload.module
|
||||
module: queuedDownload.module,
|
||||
aniListID: queuedDownload.aniListID,
|
||||
malID: queuedDownload.malID,
|
||||
isFiller: queuedDownload.isFiller
|
||||
)
|
||||
|
||||
// Add to active downloads
|
||||
|
|
@ -1214,6 +1217,17 @@ 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.isEpisode {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let subtitleURL = download.subtitleURL {
|
||||
downloadSubtitle(subtitleURL: subtitleURL, assetID: newAsset.id.uuidString)
|
||||
} else {
|
||||
|
|
@ -1543,6 +1557,9 @@ struct JSActiveDownload: Identifiable, Equatable {
|
|||
var asset: AVURLAsset?
|
||||
var headers: [String: String]
|
||||
var module: ScrapingModule? // Add module property to store ScrapingModule
|
||||
let aniListID: Int?
|
||||
let malID: Int?
|
||||
let isFiller: Bool?
|
||||
|
||||
// Computed property to get the current task state
|
||||
var taskState: URLSessionTask.State {
|
||||
|
|
@ -1646,4 +1663,73 @@ enum DownloadQueueStatus: Equatable {
|
|||
case downloading
|
||||
/// Download has been completed
|
||||
case completed
|
||||
}
|
||||
|
||||
// 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 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
|
||||
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
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
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 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 {
|
||||
print("[SkipSidecar] Sidecar write error: \(error.localizedDescription)")
|
||||
completion(false)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@ extension JSController {
|
|||
episode: episode,
|
||||
subtitleURL: subtitleURL,
|
||||
showPosterURL: showPosterURL,
|
||||
completionHandler: completionHandler
|
||||
aniListID: aniListID,
|
||||
malID: malID,
|
||||
isFiller: isFiller,
|
||||
aniListID: aniListID, malID: malID, isFiller: isFiller, completionHandler: completionHandler
|
||||
)
|
||||
}else {
|
||||
Logger.shared.log("Using MP4 download method")
|
||||
|
|
@ -68,7 +71,10 @@ extension JSController {
|
|||
episode: episode,
|
||||
subtitleURL: subtitleURL,
|
||||
showPosterURL: showPosterURL,
|
||||
completionHandler: completionHandler
|
||||
aniListID: aniListID,
|
||||
malID: malID,
|
||||
isFiller: isFiller,
|
||||
aniListID: aniListID, malID: malID, isFiller: isFiller, completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct EpisodeCell: View {
|
|||
let episodeIndex: Int
|
||||
let episode: String
|
||||
let episodeID: Int
|
||||
let malID: Int? = nil
|
||||
let progress: Double
|
||||
let itemID: Int
|
||||
let totalEpisodes: Int?
|
||||
|
|
@ -49,6 +50,7 @@ struct EpisodeCell: View {
|
|||
@State private var dragState: DragState = .inactive
|
||||
|
||||
@State private var retryAttempts: Int = 0
|
||||
private var malIDFromParent: Int? { malID }
|
||||
private let maxRetryAttempts: Int = 3
|
||||
private let initialBackoffDelay: TimeInterval = 1.0
|
||||
|
||||
|
|
@ -702,7 +704,10 @@ private extension EpisodeCell {
|
|||
season: 1,
|
||||
episode: episodeID + 1,
|
||||
subtitleURL: subtitleURL,
|
||||
showPosterURL: showPosterImageURL
|
||||
showPosterURL: showPosterImageURL,
|
||||
aniListID: itemID,
|
||||
malID: malIDFromParent,
|
||||
isFiller: isFiller
|
||||
) { success, message in
|
||||
if success {
|
||||
Logger.shared.log("Started download for Episode \(self.episodeID + 1): \(self.episode)", type: "Download")
|
||||
|
|
|
|||
|
|
@ -711,7 +711,8 @@ struct MediaInfoView: View {
|
|||
},
|
||||
tmdbID: tmdbID,
|
||||
seasonNumber: season,
|
||||
fillerEpisodes: jikanFillerSet
|
||||
fillerEpisodes: jikanFillerSet,
|
||||
malID: matchedMalID
|
||||
)
|
||||
.disabled(isFetchingEpisode)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue