feat: show optional SponsorBlock ranges on playback timeline
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user