feat: make sponsored blocks skippable

This commit is contained in:
2026-10-25 21:35:46 +01:00
parent 7052fe29af
commit a604810319
7 changed files with 170 additions and 43 deletions
+63 -9
View File
@@ -134,23 +134,28 @@ struct ResolvedPlaybackCheck {
let suite = "terebi.sponsor-preference." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
func launch(_ configured: Bool = false) -> PlayerModel {
PlayerModel(defaults: UserDefaults(suiteName: suite)!, configuration: .init(sponsorBlock: configured))
func launch(_ configured: Bool = false, autoSkip: Bool = false) -> PlayerModel {
PlayerModel(defaults: UserDefaults(suiteName: suite)!,
configuration: .init(sponsorBlock: configured, sponsorBlockAutoSkip: autoSkip))
}
let first = launch()
require(!first.sponsorBlockEnabled, "unconfigured SponsorBlock defaults to disabled")
require(!first.sponsorBlockAutoSkip, "sponsors are not skipped without opt-in")
first.setSponsorBlockAutoSkip(true)
first.setSponsorBlockEnabled(true)
first.shutdown()
let second = launch()
require(second.sponsorBlockEnabled, "enabled preference survives a fresh model/defaults instance")
require(second.sponsorBlockAutoSkip, "autoskip preference survives restart")
second.setSponsorBlockAutoSkip(false)
second.setSponsorBlockEnabled(false)
second.shutdown()
let third = launch(true)
require(!third.sponsorBlockEnabled, "saved disabled preference survives restart and overrides config")
let third = launch(true, autoSkip: true)
require(!third.sponsorBlockEnabled && !third.sponsorBlockAutoSkip, "saved disabled preferences override config")
third.shutdown()
defaults.removePersistentDomain(forName: suite)
let configured = launch(true)
require(configured.sponsorBlockEnabled, "config supplies the initial choice without a saved preference")
let configured = launch(true, autoSkip: true)
require(configured.sponsorBlockEnabled && configured.sponsorBlockAutoSkip, "config supplies unsaved choices")
configured.shutdown()
print("PASS: SponsorBlock preference persists enable/disable across launches, with config fallback")
}
@@ -167,6 +172,7 @@ struct ResolvedPlaybackCheck {
var extraction: CheckedContinuation<YouTubeVideo, Never>?
var held: [CheckedContinuation<[SponsorSegment], Never>] = []
var extractionCount = 0
let clock = Clock()
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),
@@ -177,7 +183,7 @@ struct ResolvedPlaybackCheck {
}, youtubeTitleResolver: { _ in nil }, sponsorResolver: { id in
if id == "BBBBBBBBBBB" { throw URLError(.timedOut) }
return await withCheckedContinuation { held.append($0) }
})
}, skipClock: { clock.now })
defer { model.shutdown() }
model.open(url)
try await until("playback extraction held with SponsorBlock disabled", model: model) { extraction != nil }
@@ -199,7 +205,34 @@ struct ResolvedPlaybackCheck {
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")
clock.now += 60
require(model.skipSegment(), "sponsor skip stays available beyond the opening/ending timeout")
try await until("manual sponsor skip reaches range end while paused", model: model) {
!model.state.seek_pending && abs(model.state.position - 1.5) < 0.1
}
require(!model.effectivePlaying, "manual sponsor skip preserves paused intent")
model.setSponsorBlockAutoSkip(true)
require(model.seekRelative(0.75 - model.state.position), "deliberately revisit sponsor with autoskip enabled")
try await until("intentional seek into sponsor settles", model: model) {
!model.state.seek_pending && abs(model.state.position - 0.75) < 0.1
}
require(model.setPlayback(playing: true), "play deliberately selected sponsor")
try await until("deliberately selected sponsor plays", model: model) { model.state.position >= 1 }
require(model.state.position < 1.5, "autoskip does not fight a seek into the sponsor")
require(model.setPlayback(playing: false), "pause before seeking just before sponsor")
try await until("sponsor paused again", model: model) { model.state.status == PLAYER_PAUSED }
model.isSeeking = true
model.seekPosition = 0.25
model.finishSeeking()
try await until("slider seek just before sponsor settles", model: model) {
!model.state.seek_pending && abs(model.state.position - 0.25) < 0.1
}
require(model.setPlayback(playing: true), "play from just before sponsor")
try await until("sponsor entered after nearby slider seek", model: model) { model.state.position >= 0.9 }
require(model.state.position < 1.5, "autoskip does not fight a nearby slider seek")
require(model.skipButtonTitle == "Skip Sponsor", "suppressed autoskip keeps the manual button")
require(model.setPlayback(playing: false), "pause before metadata lifecycle checks")
try await until("sponsor lifecycle playback paused", model: model) { model.state.status == PLAYER_PAUSED }
model.setSponsorBlockEnabled(false)
require(model.sponsoredRanges.isEmpty, "disabling immediately clears paint")
model.setSponsorBlockEnabled(true)
@@ -247,7 +280,28 @@ struct ResolvedPlaybackCheck {
!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")
print("PASS: persistent manual sponsor skip, deliberate-seek protection, opt-in, quality reuse, cancellation and failure isolation")
try await sponsorAutoSkip(source: source)
}
static func sponsorAutoSkip(source: ResolvedSource) async throws {
let suite = "terebi.sponsor-autoskip." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let model = PlayerModel(defaults: defaults,
configuration: .init(sponsorBlock: true, sponsorBlockAutoSkip: true),
youtubeResolver: { url, _ in
YouTubeVideo(title: "Autoskip fixture", video: .init(url: source.url, headers: source.headers),
audio: nil, duration: 0, isLive: false, webpageURL: url, height: 720, availableHeights: [720], chapters: [])
}, youtubeTitleResolver: { _ in nil }, sponsorResolver: { _ in fixtureSponsors })
defer { model.shutdown() }
model.open(URL(string: "https://www.youtube.com/watch?v=CCCCCCCCCCC")!)
try await until("natural sponsor entry queues exact-end autoskip", model: model) { model.seekPosition == 1.5 }
try await until("native player acknowledges autoskip and keeps playing", model: model) {
!model.state.seek_pending && model.state.position >= 1.5 && model.state.status == PLAYER_PLAYING
}
require(model.effectivePlaying, "autoskip preserves playing intent")
print("PASS: natural sponsor entry automatically seeks to range end in the native player")
}
static var fixtureChapters: [YouTubeChapter] {