fix: persist SponsorBlock preference between launches

This commit is contained in:
2026-10-25 03:41:38 +01:00
parent b04ed7fdc8
commit 8c2a18df48
4 changed files with 37 additions and 4 deletions
+27
View File
@@ -30,6 +30,8 @@ struct ResolvedPlaybackCheck {
} }
_ = NSApplication.shared _ = NSApplication.shared
NSApp.setActivationPolicy(.prohibited) NSApp.setActivationPolicy(.prohibited)
sponsorPreferenceCheck()
if CommandLine.arguments[1] == "--sponsor-preference" { return }
let input = CommandLine.arguments[1] let input = CommandLine.arguments[1]
let file = URL(fileURLWithPath: input) let file = URL(fileURLWithPath: input)
let preference = CommandLine.arguments.first(where: { $0.hasPrefix("--provider=") }).map { String($0.dropFirst(11)) } ?? "" 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] { static var fixtureSponsors: [SponsorSegment] {
[.init(start: 0.5, end: 1.5, videoDuration: 0)] [.init(start: 0.5, end: 1.5, videoDuration: 0)]
} }
+3 -3
View File
@@ -79,20 +79,20 @@ The optional config file is `~/.config/terebi/config.toml`, or `$XDG_CONFIG_HOME
terebi --default-config 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. 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
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 ```toml
[playback] [playback]
sponsorblock = true 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. 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.
+4 -1
View File
@@ -76,6 +76,7 @@ final class PlayerModel: ObservableObject {
private var metadataLifecycle = MetadataLifecycle() private var metadataLifecycle = MetadataLifecycle()
private var metadataTask: Task<Void, Never>? private var metadataTask: Task<Void, Never>?
@Published private(set) var sponsorBlockEnabled: Bool @Published private(set) var sponsorBlockEnabled: Bool
private let preferenceDefaults: UserDefaults
private var sponsorVideoID: String? private var sponsorVideoID: String?
private var sponsorGeneration: UInt64 = 0 private var sponsorGeneration: UInt64 = 0
private var sponsorTask: Task<Void, Never>? private var sponsorTask: Task<Void, Never>?
@@ -265,7 +266,8 @@ final class PlayerModel: ObservableObject {
compactTitleProgress = configuration.compactTitleProgress compactTitleProgress = configuration.compactTitleProgress
automaticYouTubeQuality = configuration.automaticQuality automaticYouTubeQuality = configuration.automaticQuality
showsBufferInfo = configuration.showBufferInfo showsBufferInfo = configuration.showBufferInfo
sponsorBlockEnabled = configuration.sponsorBlock preferenceDefaults = defaults
sponsorBlockEnabled = (defaults.object(forKey: "sponsorBlockEnabled") as? Bool) ?? configuration.sponsorBlock
miruroVideoProvider = configuration.miruroVideoProvider miruroVideoProvider = configuration.miruroVideoProvider
miruroCaptionProvider = configuration.miruroCaptionProvider miruroCaptionProvider = configuration.miruroCaptionProvider
compactWindow = CompactWindowController(snappingEnabled: configuration.snapToEdges, defaults: defaults) compactWindow = CompactWindowController(snappingEnabled: configuration.snapToEdges, defaults: defaults)
@@ -842,6 +844,7 @@ final class PlayerModel: ObservableObject {
func setSponsorBlockEnabled(_ enabled: Bool) { func setSponsorBlockEnabled(_ enabled: Bool) {
guard sponsorBlockEnabled != enabled else { return } guard sponsorBlockEnabled != enabled else { return }
sponsorBlockEnabled = enabled sponsorBlockEnabled = enabled
preferenceDefaults.set(enabled, forKey: "sponsorBlockEnabled")
replaceSponsorMetadata(videoID: sponsorVideoID) replaceSponsorMetadata(videoID: sponsorVideoID)
} }
+3
View File
@@ -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)