feat: resolve AniList episodes through ZokoAnime

This commit is contained in:
2026-10-25 15:46:35 +01:00
parent 8c2a18df48
commit ab61d83c69
16 changed files with 492 additions and 21 deletions
+48 -1
View File
@@ -241,6 +241,7 @@ final class PlayerModel: ObservableObject {
private let youtubeTitleResolver: @MainActor (URL) async throws -> String?
private let sponsorResolver: @MainActor (String) async throws -> [SponsorSegment]
private let miruroResolver: @MainActor (String, Int) async throws -> MiruroSources
private let zokoResolver: @MainActor (String, Int) async throws -> ResolvedSource
private let miruroMetadataResolver: @MainActor (String, Int) async throws -> MiruroMetadata
private var reportingErrorMessage: String {
reportingErrorBuffer.withUnsafeBufferPointer { String(cString: $0.baseAddress!) }
@@ -252,6 +253,7 @@ final class PlayerModel: ObservableObject {
youtubeTitleResolver: @escaping @MainActor (URL) async throws -> String? = { try await YouTubeResolver.resolveTitle($0) },
sponsorResolver: @escaping @MainActor (String) async throws -> [SponsorSegment] = { try await SponsorBlockResolver.resolve(videoID: $0) },
miruroResolver: @escaping @MainActor (String, Int) async throws -> MiruroSources = { try await MiruroResolver.resolve(mediaID: $0, episode: $1) },
zokoResolver: @escaping @MainActor (String, Int) async throws -> ResolvedSource = { try await ZokoResolver.resolve(mediaID: $0, episode: $1) },
miruroMetadataResolver: @escaping @MainActor (String, Int) async throws -> MiruroMetadata = { try await MiruroMetadataResolver.resolve(mediaID: $0, episode: $1) },
qualityClock: @escaping () -> Double = { ProcessInfo.processInfo.systemUptime },
skipClock: @escaping () -> Double = { ProcessInfo.processInfo.systemUptime }) {
@@ -261,6 +263,7 @@ final class PlayerModel: ObservableObject {
self.youtubeTitleResolver = youtubeTitleResolver
self.sponsorResolver = sponsorResolver
self.miruroResolver = miruroResolver
self.zokoResolver = zokoResolver
self.miruroMetadataResolver = miruroMetadataResolver
keyBindings = configuration.keys
compactTitleProgress = configuration.compactTitleProgress
@@ -441,7 +444,7 @@ final class PlayerModel: ObservableObject {
let url: URL
switch stream {
case .youtube(let canonical): url = canonical
case .miruro: url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines))!
case .miruro, .anilist: url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines))!
}
queue.replaceStreams([(url: url, label: url.absoluteString)])
resolveStream(stream, change: .open)
@@ -461,6 +464,12 @@ final class PlayerModel: ObservableObject {
url = canonical
title = video.title
source = .youtube(video, height: nil)
case .anilist(let mediaID, let episode):
let resolved = try await MiruroTransport.prepare(zokoResolver(mediaID, episode))
guard resolved.identity == "anilist:\(mediaID):\(episode)" else { throw CocoaError(.fileReadCorruptFile) }
url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines))!
title = resolved.title
source = .resolved(resolved)
case .miruro(let mediaID, let episode):
let providers = try await miruroResolver(mediaID, episode)
let selected = try providers.select(videoProvider: miruroVideoProvider,
@@ -522,6 +531,7 @@ final class PlayerModel: ObservableObject {
switch stream {
case .youtube(let url): return url.absoluteString
case .miruro(let id, let episode): return "miruro:\(id):\(episode)"
case .anilist(let id, let episode): return "anilist:\(id):\(episode)"
}
}
return item.url.absoluteString
@@ -562,6 +572,7 @@ final class PlayerModel: ObservableObject {
switch stream {
case .youtube(let url): resolveYouTube(url, height: nil, change: change)
case .miruro(let mediaID, let episode): resolveMiruro(mediaID: mediaID, episode: episode, change: change)
case .anilist(let mediaID, let episode): resolveZoko(mediaID: mediaID, episode: episode, change: change)
}
}
@@ -682,6 +693,40 @@ final class PlayerModel: ObservableObject {
}
}
private func resolveZoko(mediaID: String, episode: Int, change: MediaChange) {
guard let handle else { return }
if castDevice != nil { stopCasting(resume: false) }
cancelStreamResolution()
automaticQuality = AutomaticQuality()
guard player_pause(handle) else {
commandError = "Could not pause the current media to open the stream. Please try again."
revealControls()
return
}
objectWillChange.send()
let request = transition.beginResolution(change)
systemPlayback?.update()
commandError = nil
revealControls()
let resolver = zokoResolver
let task = Task { [weak self] in
do {
let source = try await MiruroTransport.prepare(resolver(mediaID, episode))
guard source.identity == "anilist:\(mediaID):\(episode)" else { throw ZokoError.malformed }
guard !Task.isCancelled, let self else { return }
self.objectWillChange.send()
guard let change = self.transition.finishResolution(request) else { return }
self.openMedia(.resolved(source), change: change)
} catch {
guard !Task.isCancelled, let self else { return }
self.objectWillChange.send()
guard self.transition.finishResolution(request) != nil else { return }
self.failStreamChange(change, error: error)
}
}
transition.attachResolution(task, request: request)
}
private func resolveMiruro(mediaID: String, episode: Int, change: MediaChange = .open) {
guard let handle else { return }
if castDevice != nil { stopCasting(resume: false) }
@@ -826,6 +871,8 @@ final class PlayerModel: ObservableObject {
title = try await youtube(url)
case .miruro(let id, let episode):
title = try await miruro(id, episode).title
case .anilist(let id, let episode):
title = try await ZokoResolver.title(mediaID: id, episode: episode)
}
} catch {
// Optional enrichment must not surface as a playback error or retry every poll.