feat: add opening/ending autoskip and correct sponsor hover anchors

Sponsor hover labels used the overlapping chapter midpoint; retain chapter-title lookup but anchor the label to the sponsored range midpoint.
This commit is contained in:
2026-10-25 22:12:08 +01:00
parent a604810319
commit 599e2f548a
6 changed files with 143 additions and 27 deletions
+67 -7
View File
@@ -119,6 +119,7 @@ struct ResolvedPlaybackCheck {
if !input.hasPrefix("https://") {
try await transportProtocol(source: source)
model.shutdown()
try await automaticSegmentSkips(file: file)
try await providerLifecycle(source: source)
try await dubOnlySelection(source: source)
try await queueMetadataLifecycle(source: source)
@@ -134,30 +135,37 @@ struct ResolvedPlaybackCheck {
let suite = "terebi.sponsor-preference." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
func launch(_ configured: Bool = false, autoSkip: Bool = false) -> PlayerModel {
func launch(_ configured: Bool = false, autoSkip: Bool = false, openingEnding: Bool = false) -> PlayerModel {
PlayerModel(defaults: UserDefaults(suiteName: suite)!,
configuration: .init(sponsorBlock: configured, sponsorBlockAutoSkip: autoSkip))
configuration: .init(sponsorBlock: configured, sponsorBlockAutoSkip: autoSkip,
openingEndingAutoSkip: openingEnding))
}
let first = launch()
require(!first.sponsorBlockEnabled, "unconfigured SponsorBlock defaults to disabled")
require(!first.sponsorBlockAutoSkip, "sponsors are not skipped without opt-in")
require(!first.openingEndingAutoSkip, "openings and endings are not skipped without opt-in")
first.setOpeningEndingAutoSkip(true)
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")
require(second.openingEndingAutoSkip, "opening/ending autoskip preference survives restart")
second.setOpeningEndingAutoSkip(false)
second.setSponsorBlockAutoSkip(false)
second.setSponsorBlockEnabled(false)
second.shutdown()
let third = launch(true, autoSkip: true)
require(!third.sponsorBlockEnabled && !third.sponsorBlockAutoSkip, "saved disabled preferences override config")
let third = launch(true, autoSkip: true, openingEnding: true)
require(!third.sponsorBlockEnabled && !third.sponsorBlockAutoSkip && !third.openingEndingAutoSkip,
"saved disabled skip preferences override config")
third.shutdown()
defaults.removePersistentDomain(forName: suite)
let configured = launch(true, autoSkip: true)
require(configured.sponsorBlockEnabled && configured.sponsorBlockAutoSkip, "config supplies unsaved choices")
let configured = launch(true, autoSkip: true, openingEnding: true)
require(configured.sponsorBlockEnabled && configured.sponsorBlockAutoSkip && configured.openingEndingAutoSkip,
"config supplies unsaved skip choices")
configured.shutdown()
print("PASS: SponsorBlock preference persists enable/disable across launches, with config fallback")
print("PASS: sponsor and opening/ending skip preferences persist across launches, with config fallback")
}
static var fixtureSponsors: [SponsorSegment] {
@@ -1142,4 +1150,56 @@ struct ResolvedPlaybackCheck {
require(model.effectivePlaying, "opening skip preserves playing intent")
print("PASS: timed opening/ending offers, dismissal, pending seeks, exact-end skips and reopen reset")
}
static func automaticSegmentSkips(file: URL) async throws {
for seekTarget: Double? in [nil, 0.75, 0.25] {
let suite = "terebi.segment-autoskip." + UUID().uuidString
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
let model = PlayerModel(defaults: defaults, configuration: .init(openingEndingAutoSkip: true),
miruroMetadataResolver: { _, _ in MiruroMetadata(title: nil, intervals: []) }, skipClock: { 100 })
defer { model.shutdown() }
model.openResolvedSource(file)
try await until("automatic segment fixture plays", model: model) {
model.state.status == PLAYER_PLAYING && model.canSeek && !model.isBusy
}
let opening = MiruroInterval(kind: .intro, start: 0.5, end: 1.5, episode: 1)
let ending = MiruroInterval(kind: .outro, start: 2, end: model.duration, episode: 1)
if let seekTarget {
require(model.setPlayback(playing: false), "pause before intentional opening seek")
try await until("automatic segment fixture pauses", model: model) { model.state.status == PLAYER_PAUSED }
if seekTarget == 0.25 {
model.isSeeking = true
model.seekPosition = seekTarget
model.finishSeeking()
} else {
require(model.seekRelative(seekTarget - model.state.position), "seek into opening")
}
try await until("intentional opening seek settles", model: model) {
!model.state.seek_pending && abs(model.state.position - seekTarget) < 0.1
}
}
model.supplyMetadataForCheck(.init(title: nil, intervals: [opening, ending]), episode: 1)
if let seekTarget {
require(abs(model.seekPosition - seekTarget) < 0.1 && model.state.status == PLAYER_PAUSED,
"late metadata does not autoskip paused playback")
require(model.setPlayback(playing: true), "resume deliberately selected opening")
try await until("deliberately selected opening plays", model: model) { model.state.position >= 1 }
require(model.state.position < opening.end && model.skipButtonTitle == "Skip Opening",
"opening autoskip respects seeking into or just before it and keeps the manual button")
continue
}
try await until("opening autoskip queues its exact end", model: model) { model.seekPosition == opening.end }
try await until("native player acknowledges opening autoskip", model: model) {
!model.state.seek_pending && model.state.position >= opening.end && model.state.status == PLAYER_PLAYING
}
try await until("ending autoskip queues exact media end before playing through ending", model: model) {
model.seekPosition == ending.end && model.state.position < ending.end - 0.5
}
try await until("native player acknowledges ending autoskip", model: model) {
!model.state.seek_pending && abs(model.state.position - ending.end) < 0.1
}
}
print("PASS: native opening/ending autoskip, paused/late metadata and deliberate relative/slider seek protection")
}
}