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:
@@ -145,6 +145,15 @@ struct ConfigurationChecks {
|
||||
try Data("[playback]\nsponsorblock_autoskip = \(value)\n".utf8).write(to: sponsor)
|
||||
try requireLoadError(at: sponsor, "non-boolean sponsor autoskip", containing: ["sponsorblock_autoskip"])
|
||||
}
|
||||
for enabled in [true, false] {
|
||||
try Data("[playback]\nopening_ending_autoskip = \(enabled)\n".utf8).write(to: sponsor)
|
||||
try require(try PlayerConfiguration.load(from: sponsor) == PlayerConfiguration(openingEndingAutoSkip: enabled),
|
||||
"opening/ending autoskip is independent of SponsorBlock")
|
||||
}
|
||||
for value in ["\"true\"", "1", "[]"] {
|
||||
try Data("[playback]\nopening_ending_autoskip = \(value)\n".utf8).write(to: sponsor)
|
||||
try requireLoadError(at: sponsor, "non-boolean opening/ending autoskip", containing: ["opening_ending_autoskip"])
|
||||
}
|
||||
let miruro = root.appendingPathComponent("miruro.toml")
|
||||
for (settings, video, captions) in [
|
||||
("video_provider = \"new-Host_2\"", "new-Host_2", ""),
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user