From 8c2a18df48da41d6a8ada989aa53036f9b9d2e48 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 25 Oct 2026 03:41:38 +0100 Subject: [PATCH] fix: persist SponsorBlock preference between launches --- Checks/resolved-playback.swift | 27 +++++++++++++++++++++++++++ README.md | 6 +++--- Sources/App/TerebiApp.swift | 5 ++++- TODO.md | 3 +++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Checks/resolved-playback.swift b/Checks/resolved-playback.swift index 25da8d1..9efb174 100644 --- a/Checks/resolved-playback.swift +++ b/Checks/resolved-playback.swift @@ -30,6 +30,8 @@ struct ResolvedPlaybackCheck { } _ = NSApplication.shared NSApp.setActivationPolicy(.prohibited) + sponsorPreferenceCheck() + if CommandLine.arguments[1] == "--sponsor-preference" { return } let input = CommandLine.arguments[1] let file = URL(fileURLWithPath: input) let preference = CommandLine.arguments.first(where: { $0.hasPrefix("--provider=") }).map { String($0.dropFirst(11)) } ?? "" @@ -128,6 +130,31 @@ struct ResolvedPlaybackCheck { } } + static func sponsorPreferenceCheck() { + 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)) + } + let first = launch() + require(!first.sponsorBlockEnabled, "unconfigured SponsorBlock defaults to disabled") + first.setSponsorBlockEnabled(true) + first.shutdown() + let second = launch() + require(second.sponsorBlockEnabled, "enabled preference survives a fresh model/defaults instance") + second.setSponsorBlockEnabled(false) + second.shutdown() + let third = launch(true) + require(!third.sponsorBlockEnabled, "saved disabled preference survives restart and overrides config") + third.shutdown() + defaults.removePersistentDomain(forName: suite) + let configured = launch(true) + require(configured.sponsorBlockEnabled, "config supplies the initial choice without a saved preference") + configured.shutdown() + print("PASS: SponsorBlock preference persists enable/disable across launches, with config fallback") + } + static var fixtureSponsors: [SponsorSegment] { [.init(start: 0.5, end: 1.5, videoDuration: 0)] } diff --git a/README.md b/README.md index fc85cd1..c714db6 100644 --- a/README.md +++ b/README.md @@ -79,20 +79,20 @@ The optional config file is `~/.config/terebi/config.toml`, or `$XDG_CONFIG_HOME terebi --default-config ``` -Copy the settings you want to change into the config file; omitted settings keep their defaults. Settings load at startup, so restart the player after editing. Changes made through menus are session-only. +Copy the settings you want to change into the config file; omitted settings keep their defaults. Settings load at startup, so restart the player after editing. Menu changes are session-only except SponsorBlock, whose saved preference overrides its startup config setting. The queue shortcut can be rebound with `show_queue` and `hide_queue` under `[keys]`; give both actions the same chord to keep a single toggle. ### SponsorBlock -SponsorBlock is **off by default**. Enable **Playback → SponsorBlock → Show Sponsored Ranges** for the session, or opt in at startup: +SponsorBlock is **off by default**. **Playback → SponsorBlock → Show Sponsored Ranges** saves your choice across launches. A saved choice overrides the config setting; without one, the startup setting applies: ```toml [playback] sponsorblock = true ``` -Use `false` (the generated default) or omit the setting to stay opted out; non-boolean values are rejected. The optional lookup runs independently of playback, only for canonical YouTube videos. Failure leaves playback unchanged with no sponsored markers. +Use `false` (the generated default) or omit the setting to default to disabled when no preference is saved; non-boolean values are rejected. The optional lookup runs independently of playback, only for canonical YouTube videos. Failure leaves playback unchanged with no sponsored markers. Enabling it adds a connection to `sponsor.ajay.app`. Requests use a bucket identified by the first four hexadecimal characters of the video ID's SHA-256 hash, reducing disclosure compared with sending the full ID; this is **not anonymous**. Terebi sends no cookies, account credentials or API key and makes no submissions, votes or view reports. diff --git a/Sources/App/TerebiApp.swift b/Sources/App/TerebiApp.swift index 23cf561..253e0b5 100644 --- a/Sources/App/TerebiApp.swift +++ b/Sources/App/TerebiApp.swift @@ -76,6 +76,7 @@ final class PlayerModel: ObservableObject { private var metadataLifecycle = MetadataLifecycle() private var metadataTask: Task? @Published private(set) var sponsorBlockEnabled: Bool + private let preferenceDefaults: UserDefaults private var sponsorVideoID: String? private var sponsorGeneration: UInt64 = 0 private var sponsorTask: Task? @@ -265,7 +266,8 @@ final class PlayerModel: ObservableObject { compactTitleProgress = configuration.compactTitleProgress automaticYouTubeQuality = configuration.automaticQuality showsBufferInfo = configuration.showBufferInfo - sponsorBlockEnabled = configuration.sponsorBlock + preferenceDefaults = defaults + sponsorBlockEnabled = (defaults.object(forKey: "sponsorBlockEnabled") as? Bool) ?? configuration.sponsorBlock miruroVideoProvider = configuration.miruroVideoProvider miruroCaptionProvider = configuration.miruroCaptionProvider compactWindow = CompactWindowController(snappingEnabled: configuration.snapToEdges, defaults: defaults) @@ -842,6 +844,7 @@ final class PlayerModel: ObservableObject { func setSponsorBlockEnabled(_ enabled: Bool) { guard sponsorBlockEnabled != enabled else { return } sponsorBlockEnabled = enabled + preferenceDefaults.set(enabled, forKey: "sponsorBlockEnabled") replaceSponsorMetadata(videoID: sponsorVideoID) } diff --git a/TODO.md b/TODO.md index e69de29..861c598 100644 --- a/TODO.md +++ b/TODO.md @@ -0,0 +1,3 @@ +- [ ] make sponsored blocks skippable: + - display the skip button used for openings and endings but for sponsored blocks it shouldn't disappear after a timeout + - add a setting for autoskipping (shouldn't activate if seeking onto the sponsored segment (or just before it) to avoid fighting user intention) \ No newline at end of file