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
+14 -9
View File
@@ -13,6 +13,7 @@ binary = str(Path(sys.argv[1]).resolve())
fixtures = Path(sys.argv[2]).resolve()
seen = []
errors = []
zoko_only = "--zoko-intervals" in sys.argv[3:]
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *_):
@@ -117,17 +118,21 @@ try:
"title": "Local resolved fixture", "subtitles": [{"url": base + "/english.jpeg", "format": "vtt", "language": "en",
"label": "English", "default": True, "headers": {"X-Resource": "subtitle-private"}}]}))
source.chmod(0o600)
subprocess.run([binary, str(source), str(progress)], check=True, timeout=100)
records = [json.loads(line) for line in progress.read_text().splitlines()]
assert records and all(record["version"] == 2 and record["media_identity"] ==
{"provider": "miruro", "media_id": "fixture-ID", "episode": 1} for record in records), "stable episode progress identity"
assert any(record["completion_eligible"] for record in records), "acknowledged video progress eligible"
assert "stream-private" not in progress.read_text() and "token=" not in progress.read_text(), "progress has no transport secrets"
subprocess.run([binary, str(source), "--zoko-intervals"] if zoko_only else
[binary, str(source), str(progress)], check=True, timeout=100)
if not zoko_only:
records = [json.loads(line) for line in progress.read_text().splitlines()]
assert records and all(record["version"] == 2 and record["media_identity"] ==
{"provider": "miruro", "media_id": "fixture-ID", "episode": 1} for record in records), "stable episode progress identity"
assert any(record["completion_eligible"] for record in records), "acknowledged video progress eligible"
assert "stream-private" not in progress.read_text() and "token=" not in progress.read_text(), "progress has no transport secrets"
assert not errors, errors
assert {"/master.m3u8", "/variant.m3u8", "/english.jpeg"}.issubset(seen), "master, nested playlist and external VTT fetched"
assert {"/alternate/master.m3u8", "/alternate/english.jpeg"}.issubset(seen), "alternate video host and independent caption provider fetched"
assert {"/master.m3u8", "/variant.m3u8"}.issubset(seen), "master and nested playlist fetched"
if not zoko_only:
assert {"/english.jpeg", "/alternate/master.m3u8", "/alternate/english.jpeg"}.issubset(seen), "external VTT, alternate host and independent captions fetched"
assert any(path.endswith(".ts") for path in seen), "media segment fetched"
print("PASS: model HLS/VTT playback, independent resource headers, tokens, stable progress")
print("PASS: local Zoko metadata native playback and source preparation" if zoko_only else
"PASS: model HLS/VTT playback, independent resource headers, tokens, stable progress")
finally:
server.shutdown()
server.server_close()
+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://") }!
+62 -3
View File
@@ -15,7 +15,10 @@ struct ZokoCheck {
throw Failure(message: "Incorrect provider error category")
}
static func page(_ config: [String: Any]) throws -> String {
var data = Array(try JSONSerialization.data(withJSONObject: config))
try page(data: JSONSerialization.data(withJSONObject: config))
}
static func page(data: Data) -> String {
var data = Array(data)
let key = Array("otaku-embed-v1".utf8)
for i in data.indices { data[i] ^= key[i % key.count] }
return "<script>window.__P = '\(Data(data).base64EncodedString())';</script>"
@@ -35,7 +38,60 @@ struct ZokoCheck {
}
var config: [String: Any] = ["src": "https://hls.dramahot.top/master.m3u8?token=fixture",
"subtitles": [["src": "https://hls.dramahot.top/en.vtt", "lang": "en", "label": "English", "default": true]]]
let intro = ProviderInterval(kind: .intro, start: 49.25, end: 144.75)
let outro = ProviderInterval(kind: .outro, start: 1357.125, end: 1451.5)
config["skip"] = ["intro": ["start": intro.start, "end": intro.end],
"outro": ["start": outro.start, "end": outro.end]]
let source = try ZokoResolver.parse(page: page(config), mediaID: "1", episode: 1)
try require(source.intervals == [intro, outro], "fractional provider boundaries retain seconds and kinds")
let replaced = source.replacingSubtitles([])
try require(replaced.intervals == source.intervals && replaced.subtitles.isEmpty &&
replaced.identity == source.identity && replaced.requirements.zoko,
"subtitle replacement preserves provider intervals and playback requirements")
let malformedMetadata: [Any] = [NSNull(), true, "bad", [], [:], ["intro": NSNull(), "outro": "bad"]]
for metadata in malformedMetadata {
var malformed = config
malformed["skip"] = metadata
let parsed = try ZokoResolver.parse(page: page(malformed), mediaID: "1", episode: 1)
try require(parsed.intervals.isEmpty && parsed.url == source.url && parsed.subtitles.count == 1,
"missing or malformed skip metadata does not block valid playback")
}
var absent = config
absent.removeValue(forKey: "skip")
let withoutTimings = try ZokoResolver.parse(page: page(absent), mediaID: "1", episode: 1)
try require(withoutTimings.intervals.isEmpty && withoutTimings.url == source.url,
"provider timings are optional")
let invalidTimings: [Any] = [
NSNull(), true, "bad", [], [:], ["start": 1], ["end": 2],
["start": true, "end": 144], ["start": 49, "end": false],
["start": "49", "end": 144], ["start": 49, "end": "144"],
["start": -1, "end": 144], ["start": 49, "end": -1],
["start": 49, "end": 49], ["start": 144, "end": 49]
]
for invalid in invalidTimings {
for badKind in ["intro", "outro"] {
var malformed = config
var skip = config["skip"] as! [String: Any]
skip[badKind] = invalid
malformed["skip"] = skip
let parsed = try ZokoResolver.parse(page: page(malformed), mediaID: "1", episode: 1)
try require(parsed.intervals == (badKind == "intro" ? [outro] : [intro]) && parsed.url == source.url,
"each malformed interval is discarded independently without blocking playback")
}
}
for timing in [#"{"start":1e999,"end":144}"#, #"{"start":49,"end":1e999}"#] {
let data = Data("""
{"src":"https://hls.dramahot.top/master.m3u8","subtitles":[],
"skip":{"intro":\(timing),"outro":{"start":1357.125,"end":1451.5}}}
""".utf8)
let parsed = try ZokoResolver.parse(page: page(data: data), mediaID: "1", episode: 1)
try require(parsed.intervals == [outro], "nonfinite numeric interval never blocks a valid sibling or playback")
}
var zeroStart = config
zeroStart["skip"] = ["intro": ["start": 0, "end": 1.5]]
let zeroSource = try ZokoResolver.parse(page: page(zeroStart), mediaID: "1", episode: 1)
try require(zeroSource.intervals == [ProviderInterval(kind: .intro, start: 0, end: 1.5)],
"zero-based intervals remain valid")
let mapped = try await ZokoResolver.resolve(mediaID: "97940", episode: 1, fetchResponse: { request in
let data: Data
let mime: String
@@ -46,13 +102,16 @@ struct ZokoCheck {
mime = "application/json"
} else {
try require(request.url?.absoluteString == "https://zokoanime.video/stream/mal/34572/1/sub", "provider lookup uses the exact mapped MAL ID, not AniList ID")
data = Data(try page(["src": "https://hls.dramahot.top/master.m3u8", "subtitles": []]).utf8)
data = Data(try page(["src": "https://hls.dramahot.top/master.m3u8", "subtitles": [],
"skip": ["intro": ["start": 49.25, "end": 144.75]]]).utf8)
mime = "text/html"
}
return (data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": mime])!)
})
try require(mapped.identity == "anilist:97940:1" && mapped.title == "Black Clover · Episode 1 (sub)",
"mapped playback preserves AniList identity and catalog title")
try require(mapped.intervals == [intro] && mapped.requirements.zoko,
"catalog title enrichment preserves provider intervals and transport requirements")
for bad in [
#"{"data":{"Media":{"id":34572,"idMal":97940}}}"#,
#"{"data":{"Media":{"id":97940,"idMal":0}}}"#,
@@ -116,7 +175,7 @@ struct ZokoCheck {
progress = PlaybackProgress(defaults: defaults)
try require(progress.position(for: identity) == 123 && progress.position(for: URL(string: "miruro:1:1")!) == 456,
"AniList resume persists separately from Miruro")
print("PASS: AniList URL/identity, static codec, unavailable/protection/malformed categories, bounded resources/subtitles, narrow CDN policy and isolated resume")
print("PASS: AniList URL/identity, static codec, optional independent fractional skip metadata, source transformations, unavailable/protection/malformed categories, bounded resources/subtitles, narrow CDN policy and isolated resume")
}
// No NSApplication, window or audio playback. The production player opens and