feat: show optional SponsorBlock ranges on playback timeline

This commit is contained in:
2026-10-25 03:01:40 +01:00
parent edb17f76e3
commit b04ed7fdc8
9 changed files with 666 additions and 24 deletions
+128 -6
View File
@@ -46,6 +46,13 @@ struct ResolvedPlaybackCheck {
try await enqueueLifecycle(source: source)
return
}
if CommandLine.arguments.contains("--sponsors-only") {
try await chapterLifecycle(source: source, file: file)
try await sponsorLifecycle(source: source)
try await streamQueueLifecycle(source: source)
try await enqueueLifecycle(source: source)
return
}
let suite = "terebi.resolved-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
@@ -114,12 +121,108 @@ struct ResolvedPlaybackCheck {
try await dubOnlySelection(source: source)
try await queueMetadataLifecycle(source: source)
try await chapterLifecycle(source: source, file: file)
try await sponsorLifecycle(source: source)
try await streamQueueLifecycle(source: source)
try await enqueueLifecycle(source: source)
try await sidebarQueueLifecycle(source: source)
}
}
static var fixtureSponsors: [SponsorSegment] {
[.init(start: 0.5, end: 1.5, videoDuration: 0)]
}
static func sponsorLifecycle(source: ResolvedSource) async throws {
let suite = "terebi.sponsor-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let url = URL(string: "https://www.youtube.com/watch?v=AAAAAAAAAAA")!
var extraction: CheckedContinuation<YouTubeVideo, Never>?
var held: [CheckedContinuation<[SponsorSegment], Never>] = []
var extractionCount = 0
let model = PlayerModel(defaults: defaults, youtubeResolver: { url, height in
extractionCount += 1
let video = YouTubeVideo(title: "Sponsor fixture", video: .init(url: source.url, headers: source.headers),
audio: nil, duration: 0, isLive: false, webpageURL: url, height: height ?? 1080,
availableHeights: [1080, 720], chapters: fixtureChapters)
if extractionCount == 1 { return await withCheckedContinuation { extraction = $0 } }
return video
}, youtubeTitleResolver: { _ in nil }, sponsorResolver: { id in
if id == "BBBBBBBBBBB" { throw URLError(.timedOut) }
return await withCheckedContinuation { held.append($0) }
})
defer { model.shutdown() }
model.open(url)
try await until("playback extraction held with SponsorBlock disabled", model: model) { extraction != nil }
require(held.isEmpty, "opted-out playback makes no SponsorBlock request")
model.setSponsorBlockEnabled(true)
try await until("SponsorBlock starts independently of held playback extraction", model: model) { held.count == 1 }
held[0].resume(returning: fixtureSponsors)
extraction!.resume(returning: YouTubeVideo(title: "Sponsor fixture",
video: .init(url: source.url, headers: source.headers), audio: nil, duration: 0, isLive: false,
webpageURL: url, height: 1080, availableHeights: [1080, 720], chapters: fixtureChapters))
extraction = nil
try await until("sponsor metadata appears against native measured duration", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.sponsoredRanges == [0.5...1.5]
}
requireYouTubeChapters(model, [1.25, 2.5], "sponsor paint crossing chapter boundary")
require(model.setPlayback(playing: false), "pause sponsored playback")
try await until("sponsor playback paused", model: model) { model.state.status == PLAYER_PAUSED }
require(model.seekRelative(0.75 - model.state.position), "seek within sponsor range")
try await until("sponsor seek acknowledged without automatic skipping", model: model) {
!model.state.seek_pending && abs(model.state.position - 0.75) < 0.1
}
require(model.segmentToSkip == nil, "sponsors never offer Miruro skip actions")
model.setSponsorBlockEnabled(false)
require(model.sponsoredRanges.isEmpty, "disabling immediately clears paint")
model.setSponsorBlockEnabled(true)
try await until("re-enabled lookup held", model: model) { held.count == 2 }
model.setYouTubeQuality(720)
try await until("quality changes without awaiting optional metadata", model: model) {
!model.isBusy && model.state.status == PLAYER_PAUSED && model.youtubeHeight == 720
}
require(held.count == 2, "quality replacement coalesces the in-flight current-video lookup")
held[1].resume(returning: fixtureSponsors)
try await until("pending metadata survives quality replacement", model: model) { model.sponsoredRanges == [0.5...1.5] }
model.setYouTubeQuality(1080)
try await until("completed metadata survives quality replacement", model: model) {
!model.isBusy && model.youtubeHeight == 1080 && model.sponsoredRanges == [0.5...1.5]
}
require(held.count == 2, "quality replacement reuses completed metadata")
model.open(url)
try await until("same-video reload has its own lookup", model: model) {
held.count == 3 && !model.isBusy && model.state.status == PLAYER_PLAYING
}
model.open(url)
try await until("second same-video reload has a new generation", model: model) {
held.count == 4 && !model.isBusy && model.state.status == PLAYER_PLAYING
}
held[2].resume(returning: [.init(start: 2, end: 3, videoDuration: 0)])
for _ in 0..<10 { await Task.yield() }
require(model.sponsoredRanges.isEmpty, "late same-video response cannot paint a newer load")
held[3].resume(returning: fixtureSponsors)
try await until("current reload accepts its own metadata", model: model) { model.sponsoredRanges == [0.5...1.5] }
model.open(url)
try await until("lookup pending before close", model: model) {
held.count == 5 && !model.isBusy && model.state.status == PLAYER_PLAYING
}
model.closeFile()
held[4].resume(returning: fixtureSponsors)
try await until("close clears sponsorship", model: model) { model.state.status == PLAYER_EMPTY }
require(model.sponsoredRanges.isEmpty, "closed player rejects late metadata")
model.open(URL(string: "https://www.youtube.com/watch?v=BBBBBBBBBBB")!)
try await until("SponsorBlock timeout never blocks native playback", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.has_audio
}
require(model.commandError == nil && model.sponsoredRanges.isEmpty, "optional failure leaves unmarked playback")
model.open(URL(string: "https://example.org/generic-video")!)
try await until("generic extractor site remains playable without SponsorBlock", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.has_audio
}
require(held.count == 5 && model.sponsoredRanges.isEmpty, "generic sites never send a SponsorBlock lookup")
print("PASS: SponsorBlock opt-in, independent lookup, crimson-only ranges, quality reuse, same-video cancellation, close, generic sites and timeout isolation")
}
static var fixtureChapters: [YouTubeChapter] {
[.init(start: 0, end: 1.25, title: "Opening"),
.init(start: 1.25, end: 2.5, title: "Ending")]
@@ -146,13 +249,14 @@ struct ResolvedPlaybackCheck {
let chapterless = URL(string: "https://www.youtube.com/watch?v=BBBBBBBBBBB")!
var requests: [URL] = []
// The single-video Tubular launch uses this progress-file initialization plus openStream.
let model = PlayerModel(defaults: defaults, progressFile: report.path, youtubeResolver: { url, height in
let model = PlayerModel(defaults: defaults, configuration: .init(sponsorBlock: true),
progressFile: report.path, youtubeResolver: { url, height in
requests.append(url)
return YouTubeVideo(title: url == chaptered ? "Chaptered launch" : "Chapterless launch",
video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: url, height: height ?? 1080, availableHeights: [1080, 720],
chapters: url == chaptered ? fixtureChapters + [.init(start: 3, end: 500, title: nil)] : [])
}, youtubeTitleResolver: { _ in nil },
}, youtubeTitleResolver: { _ in nil }, sponsorResolver: { id in id == "AAAAAAAAAAA" ? fixtureSponsors : [] },
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
defer { model.shutdown() }
require(model.progressInitializationError == nil, "single Tubular launch initializes progress reporting")
@@ -163,6 +267,10 @@ struct ResolvedPlaybackCheck {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position > 0.2
}
requireYouTubeChapters(model, expected, "direct openStream")
try await until("direct launch sponsorship metadata", model: model) {
url != chaptered || model.sponsoredRanges == [0.5...1.5]
}
require(model.sponsoredRanges == (url == chaptered ? [0.5...1.5] : []), "single launch sponsorship")
require(model.sliderSegmentTitle(at: 0.5) == (url == chaptered ? "Opening" : nil) &&
model.sliderSegmentTitle(at: 1.25) == (url == chaptered ? "Ending" : nil) &&
model.sliderSegmentTitle(at: 2.5) == nil,
@@ -185,6 +293,9 @@ struct ResolvedPlaybackCheck {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.position >= 0.7
}
requireYouTubeChapters(model, expected, "saved/recent open(url)")
try await until("saved-item sponsorship metadata", model: model) {
url != chaptered || model.sponsoredRanges == [0.5...1.5]
}
require(model.setPlayback(playing: false), "pause before chapter quality replacement")
try await until("chapter quality pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
let position = model.state.position
@@ -195,6 +306,7 @@ struct ResolvedPlaybackCheck {
abs(model.state.position - position) < 0.2
}
requireYouTubeChapters(model, expected, "chaptered/chapterless quality replacement")
require(model.sponsoredRanges == (url == chaptered ? [0.5...1.5] : []), "quality replacement retains paint")
}
model.open(chaptered)
try await until("chaptered media replaces chapterless media", model: model) {
@@ -207,6 +319,7 @@ struct ResolvedPlaybackCheck {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.state.has_video && model.state.has_audio
}
require(model.sliderBoundaries.isEmpty, "resolved media does not inherit YouTube chapters")
require(model.sponsoredRanges.isEmpty, "resolved replacement clears sponsorship")
model.closeFile()
try await until("resolved replacement closes", model: model) { model.state.status == PLAYER_EMPTY }
require(model.sliderBoundaries.isEmpty, "empty player has no chapter boundaries")
@@ -537,13 +650,14 @@ struct ResolvedPlaybackCheck {
video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: youtubeURL, height: nil, availableHeights: [],
chapters: fixtureChapters)
let model = PlayerModel(defaults: defaults, streamQueue: queue, youtubeResolver: { _, _ in
let model = PlayerModel(defaults: defaults, configuration: .init(sponsorBlock: true),
streamQueue: queue, youtubeResolver: { _, _ in
youtubeRequests += 1
if youtubeRequests == 1 {
return await withCheckedContinuation { stale = $0 }
}
return video
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in
}, youtubeTitleResolver: { _ in nil }, sponsorResolver: { _ in fixtureSponsors }, miruroResolver: { _, _ in
episodeRequests += 1
return try providerSources(source)
}, miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
@@ -573,6 +687,7 @@ struct ResolvedPlaybackCheck {
}
require(youtubeRequests == 2 && model.queue.currentID == firstID, "manual previous preserves queue and resolves current item")
requireYouTubeChapters(model, [1.25, 2.5], "batch queue previous")
try await until("batch launch sponsor paint", model: model) { model.sponsoredRanges == [0.5...1.5] }
require(model.setPlayback(playing: false), "pause first queued stream")
try await until("queue pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
require(model.seekRelative(max(0, model.duration - 0.3) - model.state.position), "seek first stream near natural end")
@@ -585,6 +700,7 @@ struct ResolvedPlaybackCheck {
!model.miruroVideoProviders.isEmpty && episodeRequests == 2
}
require(model.sliderBoundaries.isEmpty, "natural queue advance clears YouTube chapters for Miruro")
require(model.sponsoredRanges.isEmpty, "natural advance clears sponsor paint for Miruro")
require(model.setPlayback(playing: false), "pause second stream for restoration")
try await until("second paused", model: model) { model.state.status == PLAYER_PAUSED }
require(model.seekRelative(0.8 - model.state.position), "save an acknowledged second-stream bookmark")
@@ -642,14 +758,15 @@ struct ResolvedPlaybackCheck {
let suite = "terebi.enqueue-check." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let model = PlayerModel(defaults: defaults, progressFile: originalReport.path,
let model = PlayerModel(defaults: defaults, configuration: .init(sponsorBlock: true), progressFile: originalReport.path,
youtubeResolver: { url, _ in
requests += 1
return YouTubeVideo(title: url.absoluteString,
video: .init(url: source.url, headers: source.headers), audio: nil,
duration: 0, isLive: false, webpageURL: url, height: nil, availableHeights: [],
chapters: url == first ? fixtureChapters : [])
}, youtubeTitleResolver: { _ in nil }, miruroResolver: { _, _ in episodeRequests += 1; return try providerSources(source) },
}, youtubeTitleResolver: { _ in nil }, sponsorResolver: { id in id == "AAAAAAAAAAA" ? fixtureSponsors : [] },
miruroResolver: { _, _ in episodeRequests += 1; return try providerSources(source) },
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) })
defer { model.shutdown() }
func deliver(_ items: [(URL, URL)]) throws {
@@ -664,6 +781,7 @@ struct ResolvedPlaybackCheck {
model.state.status == PLAYER_PLAYING && !model.isBusy && model.state.position > 0.2
}
requireYouTubeChapters(model, [1.25, 2.5], "single stream before enqueue")
try await until("initial single launch sponsor paint", model: model) { model.sponsoredRanges == [0.5...1.5] }
require(model.setPlayback(playing: false), "pause original single-video playback")
try await until("standalone pause acknowledged", model: model) { model.state.status == PLAYER_PAUSED }
let selected = model.queue.currentID
@@ -676,6 +794,7 @@ struct ResolvedPlaybackCheck {
model.fileName == first.absoluteString && requests == 1 && episodeRequests == 0,
"paused enqueue preserves original media, selection and decoder timeline; new items resolve lazily in supplied order")
requireYouTubeChapters(model, [1.25, 2.5], "paused enqueue preserves current chapters")
require(model.sponsoredRanges == [0.5...1.5], "paused enqueue preserves current sponsor paint")
require(model.setPlayback(playing: true), "resume original playback")
try await until("original playing again", model: model) { model.state.status == PLAYER_PLAYING }
let playingRevision = model.state.revision
@@ -685,6 +804,7 @@ struct ResolvedPlaybackCheck {
model.state.status == PLAYER_PLAYING,
"repeated native delivery appends while playing without reopening current media")
requireYouTubeChapters(model, [1.25, 2.5], "playing enqueue preserves current chapters")
require(model.sponsoredRanges == [0.5...1.5], "playing enqueue preserves current sponsor paint")
try deliver([(third, directory.appendingPathComponent("conflict.jsonl"))])
require(model.commandError != nil && model.queue.items.count == 4 && model.queue.currentID == selected,
"conflicting identity rejects complete enqueue without changing playback")
@@ -710,11 +830,13 @@ struct ResolvedPlaybackCheck {
model.queue.currentURL == second
}
require(model.sliderBoundaries.isEmpty, "enqueued Miruro advance clears current chapters")
require(model.sponsoredRanges.isEmpty, "enqueue advance to Miruro clears sponsor paint")
model.advanceQueue(1)
try await until("enqueued YouTube item decodes", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.fileName == third.absoluteString
}
requireYouTubeChapters(model, [], "chapterless enqueued YouTube advance")
require(model.sponsoredRanges.isEmpty, "enqueued no-result video has no sponsor paint")
model.advanceQueue(-1)
try await until("enqueued Miruro report revisits", model: model) {
!model.isBusy && model.state.status == PLAYER_PLAYING && model.queue.currentURL == second