fix: retain Zoko opening and ending metadata through playback
This commit is contained in:
@@ -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://") }!
|
||||
|
||||
Reference in New Issue
Block a user