feat: make sponsored blocks skippable
This commit is contained in:
@@ -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] {
|
||||
|
||||
Reference in New Issue
Block a user