fix: retain Zoko opening and ending metadata through playback

This commit is contained in:
2026-10-25 23:01:38 +01:00
parent d8fb648c90
commit 41aa0abd56
11 changed files with 341 additions and 24 deletions
+149
View File
@@ -44,6 +44,11 @@ struct ResolvedPlaybackCheck {
} else {
source = try ResolvedSource.load(from: file)
}
if CommandLine.arguments.contains("--zoko-intervals") {
try await transportProtocol(source: source)
try await zokoIntervalLifecycle(source: source)
return
}
if CommandLine.arguments.contains("--enqueue-only") {
try await enqueueLifecycle(source: source)
return
@@ -128,9 +133,151 @@ struct ResolvedPlaybackCheck {
try await streamQueueLifecycle(source: source)
try await enqueueLifecycle(source: source)
try await sidebarQueueLifecycle(source: source)
try await zokoIntervalLifecycle(source: source)
}
}
static func zokoIntervalLifecycle(source: ResolvedSource) async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("terebi.zoko-interval-check." + UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let suite = "terebi.zoko-interval-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
func url(_ episode: Int) -> URL {
URL(string: "https://zokoanime.video/stream/ani/1/\(episode)/sub")!
}
let rows: [ProviderInterval] = [
.init(kind: .intro, start: 0.5, end: 1.5), .init(kind: .outro, start: 2.25, end: 3.25),
]
let expected = [
MiruroInterval(kind: .intro, start: 0.5, end: 1.5, episode: 1),
MiruroInterval(kind: .outro, start: 2.25, end: 3.25, episode: 1),
]
func resolved(_ episode: Int) -> ResolvedSource {
var result = ResolvedSource(url: source.url, headers: source.headers, identity: "anilist:1:\(episode)",
title: "Zoko interval fixture \(episode)", subtitles: [])
result.intervals = episode == 2 ? [] : rows
if episode == 3 { result.intervals[1] = .init(kind: .outro, start: 2.25, end: 500) }
return result
}
var requests: [Int: Int] = [:]
var held: CheckedContinuation<ResolvedSource, Never>?
var heldMetadata: CheckedContinuation<MiruroMetadata, Never>?
var staleCompleted = false
var metadataRequests = 0
let clock = Clock()
let model = PlayerModel(defaults: defaults, progressFile: directory.appendingPathComponent("direct.jsonl").path,
miruroResolver: { _, _ in try providerSources(source) },
zokoResolver: { _, episode in
requests[episode, default: 0] += 1
if episode == 4 {
let result = await withCheckedContinuation { held = $0 }
staleCompleted = true
return result
}
return resolved(episode)
}, miruroMetadataResolver: { _, _ in
metadataRequests += 1
return await withCheckedContinuation { heldMetadata = $0 }
}, skipClock: { clock.now })
defer { model.shutdown() }
model.openStream(url(1).absoluteString)
require(model.visibleIntervals.isEmpty, "direct launch hides bands until native media is acknowledged")
try await until("direct Zoko native decode and provider bands", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position > 0.2 &&
model.visibleIntervals == expected
}
require(model.state.has_audio && model.state.has_video && model.state.duration >= 3.25,
"local fixture supplies real video/audio and measured duration")
require(model.sliderBoundaries == [0.5, 1.5, 2.25, 3.25] &&
model.sliderSegmentTitle(at: 0.75) == "Opening" && model.sliderSegmentTitle(at: 2.5) == "Ending",
"provider bands reuse existing slider labels and fractional boundaries")
require(model.setPlayback(playing: false), "pause provider playback")
try await until("provider pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
require(model.seekRelative(0.8 - model.state.position), "save provider bookmark inside opening")
try await until("provider bookmark and skip offer acknowledged", model: model) {
!model.state.seek_pending && abs(model.state.position - 0.8) < 0.1 && model.segmentToSkip == expected[0]
}
require(model.skipButtonTitle == "Skip Opening", "provider timing feeds unchanged manual skip policy")
model.closeFile()
require(model.visibleIntervals.isEmpty && model.sliderBoundaries.isEmpty, "close clears provider bands immediately")
try await until("provider playback closed", model: model) { model.state.status == PLAYER_EMPTY }
require(model.savedPosition(for: URL(string: "anilist:1:1")!).map { abs($0 - 0.8) < 0.1 } == true,
"provider bookmark is saved under stable episode identity")
let queue = StreamQueue(entries: (1...3).map { episode in
.init(url: url(episode), identity: "anilist:1:\(episode)",
progressFile: directory.appendingPathComponent("queue-\(episode).jsonl"), label: "Episode \(episode)")
})
let reopened = PlayerModel(defaults: UserDefaults(suiteName: suite)!, streamQueue: queue,
zokoResolver: { _, episode in
requests[episode, default: 0] += 1
return resolved(episode)
}, miruroMetadataResolver: { _, _ in
metadataRequests += 1
return MiruroMetadata(title: nil, intervals: [])
}, skipClock: { clock.now })
defer { reopened.shutdown() }
reopened.openStreamQueue(queue)
try await until("fresh model reopens saved Zoko episode with newly resolved bands", model: reopened) {
!reopened.isBusy && reopened.state.status == PLAYER_PLAYING && reopened.state.position >= 0.7 &&
reopened.visibleIntervals == expected
}
require(requests[1] == 2 && requests[2] == nil, "saved queued reopen resolves only selected episode afresh")
reopened.advanceQueue(1)
require(reopened.visibleIntervals.isEmpty && reopened.segmentToSkip == nil, "queue replacement clears opening offer and bands")
try await until("queued episode without timing still decodes", model: reopened) {
!reopened.isBusy && reopened.state.status == PLAYER_PLAYING && reopened.fileName == resolved(2).title
}
require(reopened.visibleIntervals.isEmpty && reopened.sliderBoundaries.isEmpty, "missing timing never inherits previous episode")
reopened.advanceQueue(1)
try await until("out-of-duration provider range does not block queued playback", model: reopened) {
!reopened.isBusy && reopened.state.status == PLAYER_PLAYING && reopened.fileName == resolved(3).title
}
require(reopened.visibleIntervals == [MiruroInterval(kind: .intro, start: 0.5, end: 1.5, episode: 3)],
"actual native duration rejects oversized provider ending without clipping or losing valid opening")
reopened.advanceQueue(-2)
try await until("queue revisit restores fresh first-episode bands", model: reopened) {
!reopened.isBusy && reopened.state.status == PLAYER_PLAYING && reopened.visibleIntervals == expected
}
require(requests[1] == 3, "queue revisit resolves again instead of retaining stale timing")
reopened.closeFile()
try await until("queued provider playback closed", model: reopened) { reopened.state.status == PLAYER_EMPTY }
model.openStream(url(4).absoluteString)
try await until("stale provider resolution held", model: model) { held != nil }
model.openStream(url(2).absoluteString)
held!.resume(returning: resolved(4))
held = nil
try await until("late canceled provider resolution cannot replace new episode", model: model) {
staleCompleted && !model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == resolved(2).title
}
require(model.visibleIntervals.isEmpty && model.sliderBoundaries.isEmpty, "late source timing cannot paint replacement episode")
model.closeFile()
try await until("empty player before sidebar provider launch", model: model) { model.state.status == PLAYER_EMPTY }
try await model.addStreamToQueue(url(1).absoluteString)
try await until("cold sidebar provider launch preserves prepared source timing", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.visibleIntervals == expected
}
require(metadataRequests == 0, "AniList paths never request Miruro timing metadata")
model.openStream("https://barelystarted.miruro.tv/watch/fixture-ID/episode?ep=1")
require(model.visibleIntervals.isEmpty, "switching away from provider timing clears bands immediately")
try await until("Miruro metadata lookup held", model: model) { heldMetadata != nil }
model.openStream(url(1).absoluteString)
heldMetadata!.resume(returning: MiruroMetadata(title: "Stale Miruro title",
intervals: [.init(kind: .intro, start: 0.1, end: 0.4, episode: 1)]))
heldMetadata = nil
try await until("replacement Zoko episode keeps its own provider metadata", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.visibleIntervals == expected
}
for _ in 0..<10 { await Task.yield() }
require(metadataRequests == 1 && model.fileName == resolved(1).title && model.visibleIntervals == expected,
"canceled Miruro enrichment cannot rename or paint provider replacement")
print("PASS: Zoko direct/sidebar launch, fractional bands, manual skip, saved fresh-model reopen, queue/revisit, missing/out-of-duration timing and stale source/metadata rejection")
}
static func sponsorPreferenceCheck() {
let suite = "terebi.sponsor-preference." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
@@ -432,9 +579,11 @@ struct ResolvedPlaybackCheck {
var input = ResolvedSource(url: base + "/transport-master.m3u8?token=private", headers:
"Referer: https://transport.example/\r\nX-Secret: private\r\n",
identity: source.identity, title: source.title, subtitles: [])
input.intervals = [.init(kind: .intro, start: 0.25, end: 1.75)]
input.requirements = .init(proxy: .init(rotate: false, segments: true),
segmentOrigin: base, playlistQuery: ["skip": "fixture"])
let prepared = try await MiruroTransport.prepare(input, deployment: deployment)
require(prepared.intervals == input.intervals, "transport source reconstruction preserves provider intervals")
let (master, _) = try await URLSession.shared.data(from: URL(string: prepared.url)!)
let nested = String(data: master, encoding: .utf8)!.components(separatedBy: .newlines)
.first { $0.hasPrefix("http://") }!