feat: support Miruro provider tracks, proxies, and native embeds

This commit is contained in:
2026-10-22 19:04:30 +02:00
parent 80d9e705e5
commit 8df500da83
15 changed files with 1201 additions and 203 deletions
+32 -7
View File
@@ -125,14 +125,17 @@ final class PlayerModel: ObservableObject {
@Published private(set) var miruroCaptionProvider: String
@Published private(set) var selectedMiruroVideoProvider: String?
@Published private(set) var selectedMiruroCaptionProvider: String?
private var pendingMiruro: (mediaID: String, episode: Int, sources: MiruroSources)?
private var miruroSources: MiruroSources? {
if let pendingMiruro { return pendingMiruro.sources }
if case .resolved(_, let providers) = currentMedia { return providers }
return nil
}
var miruroVideoProviders: [String] { miruroSources?.videoProviders ?? [] }
var miruroCaptionProviders: [String] { miruroSources?.captionProviders ?? [] }
var canChangeMiruroProvider: Bool {
castDevice == nil && miruroSources != nil && canTogglePlayback &&
castDevice == nil && miruroSources != nil &&
(pendingMiruro != nil ? canOpen && !resolvingStream : canTogglePlayback) &&
!isSeeking && !transition.seekPending && !state.seek_pending
}
@Published private(set) var subtitleTracks: [ResolvedSubtitle] = []
@@ -421,8 +424,9 @@ final class PlayerModel: ObservableObject {
source = .youtube(video, height: nil)
case .miruro(let mediaID, let episode):
let providers = try await miruroResolver(mediaID, episode)
let resolved = try providers.select(videoProvider: miruroVideoProvider,
let selected = try providers.select(videoProvider: miruroVideoProvider,
captionProvider: miruroCaptionProvider).source
let resolved = try await MiruroTransport.prepare(selected)
guard resolved.identity == "miruro:\(mediaID):\(episode)" else { throw CocoaError(.fileReadCorruptFile) }
url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines))!
title = resolved.title
@@ -551,7 +555,8 @@ final class PlayerModel: ObservableObject {
let track = subtitleTracks[index]
subtitleTask = Task { [weak self] in
do {
let document = try await WebVTTDocument.load(url: track.url, headers: track.headers)
let prepared = try await MiruroTransport.subtitle(track)
let document = try await WebVTTDocument.load(url: prepared.url, headers: prepared.headers)
guard !Task.isCancelled, let self else { return }
self.subtitleDocument = document
self.updateSubtitle()
@@ -598,8 +603,13 @@ final class PlayerModel: ObservableObject {
func setMiruroVideoProvider(_ provider: String) {
guard canChangeMiruroProvider, let handle,
provider.isEmpty || miruroVideoProviders.contains(provider),
provider != miruroVideoProvider,
provider.isEmpty || miruroVideoProviders.contains(provider) else { return }
if let pendingMiruro {
miruroVideoProvider = provider
resolveMiruro(mediaID: pendingMiruro.mediaID, episode: pendingMiruro.episode)
return
}
guard provider != miruroVideoProvider,
case .resolved(let source, _) = currentMedia else { return }
let identity = source.identity.split(separator: ":")
guard identity.count == 3, let episode = Int(identity[2]) else { return }
@@ -611,13 +621,18 @@ final class PlayerModel: ObservableObject {
func setMiruroCaptionProvider(_ provider: String) {
guard canChangeMiruroProvider, let providers = miruroSources,
provider.isEmpty || miruroCaptionProviders.contains(provider) else { return }
if pendingMiruro != nil {
miruroCaptionProvider = provider
return
}
guard case .resolved(let current, _) = currentMedia else { return }
do {
let selection = try providers.select(videoProvider: selectedMiruroVideoProvider,
captionProvider: provider)
let language = selectedSubtitle.flatMap { subtitleTracks.indices.contains($0) ? subtitleTracks[$0].language : nil }
miruroCaptionProvider = provider
selectedMiruroCaptionProvider = selection.captionProvider
currentMedia = .resolved(selection.source, providers: providers)
currentMedia = .resolved(current.replacingSubtitles(selection.source.subtitles), providers: providers)
subtitleTracks = selection.source.subtitles
let index = language.flatMap { language in subtitleTracks.firstIndex { $0.language == language } ??
subtitleTracks.firstIndex(where: \.isDefault) }
@@ -647,10 +662,12 @@ final class PlayerModel: ObservableObject {
let videoProvider = miruroVideoProvider
let captionProvider = miruroCaptionProvider
let task = Task { [weak self] in
var catalog: MiruroSources?
do {
let providers = try await resolver(mediaID, episode)
catalog = providers
let selection = try providers.select(videoProvider: videoProvider, captionProvider: captionProvider)
let source = selection.source
let source = try await MiruroTransport.prepare(selection.source)
guard source.identity == "miruro:\(mediaID):\(episode)" else {
throw CocoaError(.fileReadCorruptFile)
}
@@ -662,6 +679,9 @@ final class PlayerModel: ObservableObject {
guard !Task.isCancelled, let self else { return }
self.objectWillChange.send()
guard self.transition.finishResolution(request) != nil else { return }
if let catalog, self.currentMedia?.url.absoluteString != "miruro:\(mediaID):\(episode)" {
self.pendingMiruro = (mediaID, episode, catalog)
}
self.failStreamChange(change, error: error)
}
}
@@ -712,6 +732,10 @@ final class PlayerModel: ObservableObject {
}
private func cancelStreamResolution() {
if pendingMiruro != nil {
objectWillChange.send()
pendingMiruro = nil
}
guard transition.isResolving else { return }
objectWillChange.send()
transition.cancelResolution()
@@ -872,6 +896,7 @@ final class PlayerModel: ObservableObject {
queuePlaybackRevision = queue.currentID == nil ? nil : player_get_state(handle).revision
queueCompletionHandled = false
currentMedia = source
pendingMiruro = nil
if case .resolved(_, let providers) = source,
let selection = try? providers?.select(videoProvider: miruroVideoProvider,
captionProvider: miruroCaptionProvider) {