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
+51 -6
View File
@@ -974,9 +974,16 @@ final class PlayerModel: ObservableObject {
skipOffer = nil
}
let parts = source?.url.absoluteString.split(separator: ":") ?? []
let episode = parts.count == 3 && parts[0] == "miruro" ? Int(parts[2]) : nil
let generation = metadataLifecycle.replace(identity: source?.url.absoluteString, episode: episode)
guard let source, let episode else { return }
let episode = parts.count == 3 && (parts[0] == "miruro" || parts[0] == "anilist") ? Int(parts[2]) : nil
let providerIntervals: [ProviderInterval]
if case .resolved(let resolved, _) = source, parts.first == "anilist" {
providerIntervals = resolved.intervals
} else {
providerIntervals = []
}
let generation = metadataLifecycle.replace(identity: source?.url.absoluteString, episode: episode,
providerIntervals: providerIntervals)
guard let source, let episode, parts[0] == "miruro" else { return }
let identity = source.url.absoluteString
let mediaID = String(parts[1])
let resolver = miruroMetadataResolver
@@ -2142,14 +2149,20 @@ final class PlayerModel: ObservableObject {
private struct MetadataLifecycle {
private(set) var identity: String?
private(set) var metadata: MiruroMetadata?
private var providerIntervals: [MiruroInterval] = []
private var episode: Int?
private var generation: UInt64 = 0
mutating func replace(identity: String?, episode: Int?) -> UInt64 {
mutating func replace(identity: String?, episode: Int?, providerIntervals: [ProviderInterval] = []) -> UInt64 {
generation &+= 1
self.identity = identity
self.episode = episode
metadata = nil
self.providerIntervals = episode.map { episode in
providerIntervals.map {
MiruroInterval(kind: $0.kind == .intro ? .intro : .outro, start: $0.start, end: $0.end, episode: episode)
}
} ?? []
return generation
}
@@ -2160,8 +2173,14 @@ final class PlayerModel: ObservableObject {
}
func visibleIntervals(duration: Double) -> [MiruroInterval] {
guard let episode, let metadata else { return [] }
return metadata.visibleIntervals(episode: episode, duration: duration)
guard let episode else { return [] }
if !providerIntervals.isEmpty {
guard duration.isFinite, duration > 0 else { return [] }
return providerIntervals.filter {
$0.start.isFinite && $0.end.isFinite && $0.start >= 0 && $0.end > $0.start && $0.end <= duration
}
}
return metadata?.visibleIntervals(episode: episode, duration: duration) ?? []
}
}
@@ -2208,6 +2227,32 @@ final class PlayerModel: ObservableObject {
precondition(!lifecycle.accept(metadata, identity: a, generation: second), "YouTube rejects earlier metadata")
_ = lifecycle.replace(identity: nil, episode: nil)
precondition(lifecycle.metadata == nil, "Close clears metadata")
let providerRows: [ProviderInterval] = [
.init(kind: .intro, start: 0.25, end: 1.75),
.init(kind: .outro, start: 90.5, end: 100),
.init(kind: .intro, start: -1, end: 1),
.init(kind: .outro, start: 10, end: 10),
.init(kind: .intro, start: .nan, end: 3),
.init(kind: .outro, start: 2, end: .infinity),
]
let providerGeneration = lifecycle.replace(identity: "anilist:1:1", episode: 1, providerIntervals: providerRows)
let expected = [
MiruroInterval(kind: .intro, start: 0.25, end: 1.75, episode: 1),
MiruroInterval(kind: .outro, start: 90.5, end: 100, episode: 1),
]
for duration in [0.0, -1, .nan, .infinity] {
precondition(lifecycle.visibleIntervals(duration: duration).isEmpty, "Unknown/nonfinite provider duration hides bands")
}
precondition(lifecycle.visibleIntervals(duration: 100) == expected, "Provider fractions and exact-duration end survive mapping")
precondition(lifecycle.visibleIntervals(duration: 95) == [expected[0]], "Provider ranges outside actual duration are rejected, not clipped")
precondition(lifecycle.visibleIntervals(duration: 100) == expected, "Provider raw rows survive duration changes")
_ = lifecycle.replace(identity: "anilist:1:2", episode: 2)
precondition(lifecycle.visibleIntervals(duration: 100).isEmpty, "Missing provider timing clears preceding episode")
_ = lifecycle.replace(identity: "anilist:1:1", episode: 1)
precondition(!lifecycle.accept(metadata, identity: "anilist:1:1", generation: providerGeneration),
"Provider replacement retains same-identity generation protection")
_ = lifecycle.replace(identity: nil, episode: nil)
precondition(lifecycle.visibleIntervals(duration: 100).isEmpty, "Close clears provider bands")
}
#endif