fix: clip metadata intervals to playable duration
This commit is contained in:
@@ -167,15 +167,24 @@ struct MiruroMetadataChecks {
|
||||
for duration in [0, -1, Double.nan, .infinity] {
|
||||
try require(MiruroMetadata(title: nil, intervals: [valid]).visibleIntervals(episode: 1, duration: duration).isEmpty, "wait for finite positive actual duration")
|
||||
}
|
||||
let invalid = [interval(.intro, -1, 10), interval(.intro, 10, 10), interval(.intro, 20, 10),
|
||||
interval(.intro, .nan, 10), interval(.intro, 0, .infinity), interval(.intro, 0, 101),
|
||||
let invalid = [interval(.intro, -10, 0), interval(.intro, 100, 110),
|
||||
interval(.intro, 10, 10), interval(.intro, 20, 10),
|
||||
interval(.intro, .nan, 10), interval(.intro, 0, .infinity),
|
||||
interval(episode: 2), interval(votes: -1), interval(votes: .nan),
|
||||
interval(provider: "SomeCrunchyrollProvider"), interval(length: 106)]
|
||||
try require(MiruroMetadata(title: nil, intervals: invalid).visibleIntervals(episode: 1, duration: 100).isEmpty, "bounds, episode, votes, provenance and reference-length rejection")
|
||||
let metadata = MiruroMetadata(title: nil, intervals: [valid])
|
||||
try require(metadata.visibleIntervals(episode: 1, duration: 100) == [valid], "end equal to duration allowed")
|
||||
try require(metadata.visibleIntervals(episode: 1, duration: 99).isEmpty, "duration shrink revalidates without trimming")
|
||||
try require(metadata.visibleIntervals(episode: 1, duration: 120) == [valid], "duration growth revalidates without scaling")
|
||||
try require(metadata.visibleIntervals(episode: 1, duration: 99) == [interval(.intro, 10, 99)], "duration shrink clips the visible interval")
|
||||
try require(metadata.visibleIntervals(episode: 1, duration: 120) == [valid] && metadata.intervals == [valid], "duration growth restores unchanged raw timestamps")
|
||||
let overlapping = MiruroMetadata(title: nil, intervals: [
|
||||
interval(.intro, -5.5, 10.25), interval(.outro, 90.75, 110.5)])
|
||||
try require(overlapping.visibleIntervals(episode: 1, duration: 100) == [
|
||||
interval(.intro, 0, 10.25), interval(.outro, 90.75, 100)], "partial overlaps clip at both playback boundaries")
|
||||
try require(overlapping.visibleIntervals(episode: 1, duration: 120) == [
|
||||
interval(.intro, 0, 10.25), interval(.outro, 90.75, 110.5)], "growth recomputes clipping from raw timestamps")
|
||||
try require(MiruroMetadata(title: nil, intervals: [interval(.intro, -10, 110)])
|
||||
.visibleIntervals(episode: 1, duration: 100) == [interval(.intro, 0, 100)], "interval spanning the video clips to the playable range")
|
||||
let first = interval(.intro, 5, 15, votes: 4), tied = interval(.intro, 20, 30, votes: 4)
|
||||
let ending = interval(.outro, 12, 40, votes: 2)
|
||||
let ranked = MiruroMetadata(title: nil, intervals: [interval(), first, tied, ending, interval(.outro, 50, 60, votes: 1)])
|
||||
|
||||
@@ -4,8 +4,8 @@ import CoreFoundation
|
||||
struct MiruroInterval: Sendable, Equatable {
|
||||
enum Kind: Sendable { case intro, outro }
|
||||
let kind: Kind
|
||||
let start: Double
|
||||
let end: Double
|
||||
var start: Double
|
||||
var end: Double
|
||||
let episode: Int
|
||||
var rawKind: String = ""
|
||||
var provider: String? = nil
|
||||
@@ -20,13 +20,16 @@ struct MiruroMetadata: Sendable {
|
||||
func visibleIntervals(episode: Int, duration: Double) -> [MiruroInterval] {
|
||||
guard duration.isFinite, duration > 0 else { return [] }
|
||||
var intro: MiruroInterval?, outro: MiruroInterval?
|
||||
for row in intervals {
|
||||
for var row in intervals {
|
||||
guard row.episode == episode, row.start.isFinite, row.end.isFinite,
|
||||
row.start >= 0, row.end > row.start, row.end <= duration,
|
||||
row.end > row.start,
|
||||
!(row.provider?.range(of: "crunchyroll", options: .caseInsensitive) != nil),
|
||||
(row.votes ?? 0).isFinite, (row.votes ?? 0) >= 0 else { continue }
|
||||
if let length = row.referenceLength, length.isFinite, length > 0,
|
||||
abs(length - duration) / duration > 0.05 { continue }
|
||||
row.start = max(0, row.start)
|
||||
row.end = min(duration, row.end)
|
||||
guard row.end > row.start else { continue }
|
||||
// Strictly greater preserves input order on ties, equivalent to stable descending votes.
|
||||
switch row.kind {
|
||||
case .intro:
|
||||
|
||||
@@ -1614,8 +1614,10 @@ final class PlayerModel: ObservableObject {
|
||||
precondition(lifecycle.accept(metadata, identity: a, generation: first), "Current metadata is accepted")
|
||||
precondition(lifecycle.visibleIntervals(duration: 0).isEmpty, "Unknown duration hides bands")
|
||||
precondition(lifecycle.visibleIntervals(duration: 100).count == 2, "Actual duration permits exact-end bands")
|
||||
precondition(lifecycle.visibleIntervals(duration: 95).count == 1, "Duration shrink revalidates raw bands")
|
||||
precondition(lifecycle.visibleIntervals(duration: 100).count == 2, "Duration growth restores raw bands")
|
||||
precondition(lifecycle.visibleIntervals(duration: 95) == [
|
||||
metadata.intervals[0], MiruroInterval(kind: .outro, start: 90, end: 95, episode: 1)
|
||||
], "Duration shrink clips raw bands")
|
||||
precondition(lifecycle.visibleIntervals(duration: 100) == metadata.intervals, "Duration growth restores raw bands")
|
||||
_ = lifecycle.replace(identity: b, episode: 2)
|
||||
precondition(lifecycle.metadata == nil, "Replacement clears title and intervals")
|
||||
precondition(!lifecycle.accept(metadata, identity: a, generation: first), "Different-identity result is stale")
|
||||
|
||||
@@ -29,7 +29,7 @@ Create a private reporting-session directory (`0700`) and supply a **nonexistent
|
||||
|
||||
The native resolver requests watch-route data and safely follows bounded same-origin, same-ID, same-episode canonicalization envelopes, including HTTP-200 redirects. It accepts JSON and newline-delimited `text/sveltekit-data`, decoding only the needed response-local tables and episode promise. Bodies and traversal are bounded; unrelated source promises are not used for playback.
|
||||
|
||||
Titles come from returned series/selected-episode metadata. Only `op`/`mixed-op` and `ed`/`mixed-ed` become opening/ending bands. Bands retain fractional seconds, require positive player-reported duration, and reject nonfinite, negative, reversed, wrong-episode or out-of-bounds timings; an end equal to duration is valid. Duration changes revalidate raw intervals without trimming, shifting or scaling. Optional provenance/votes/reference-length checks follow the documented frontend evidence; missing fields are not invented. Highest-vote eligible opening and ending win, with response order breaking ties. Overlaps are not merged.
|
||||
Titles come from returned series/selected-episode metadata. Only `op`/`mixed-op` and `ed`/`mixed-ed` become opening/ending bands. Bands retain fractional seconds, require finite positive player-reported duration, and reject nonfinite timestamps, originally reversed/zero-length intervals and wrong-episode timings. Display intervals are clipped to `[0, duration]`; intervals with no positive-length overlap are hidden. Raw timestamps remain unchanged, and duration changes recompute clipping without shifting or scaling. Optional provenance/votes/reference-length checks remain in force; a finite positive reference length differing from actual duration by more than 5% rejects the interval, and missing fields are not invented. Highest-vote eligible opening and ending win, with response order breaking ties. Overlaps are not merged.
|
||||
|
||||
The controls mark interval boundaries with small rounded gaps in the slider track, without visible labels or boxes. Buffer rendering shares the segmented track; accessibility descriptions retain opening/ending names and times. Neither automatic seeking nor skip buttons are added. Bounds do not prove synchronization across cuts. Metadata tasks are bound to identity and lifecycle generation; replacement clears intervals and rejects stale results, including A → B → A.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user