Enhance comment viewer + make SponsorBlock configurable (3.6 + parity)

Comment viewer (web UI): the flat tree gains search (highlight + keep
ancestor context), sort (threaded / top / newest / oldest), per-thread
collapse/expand with collapse-all/expand-all, an OP badge for the
uploader, and a "new since last visit" highlight + count (localStorage
per-video timestamp). API now returns each comment's `timestamp` and
`author_is_uploader`.

SponsorBlock: was a hardcoded `--sponsorblock-mark all`. Now a real
setting (off / mark / remove) with a per-channel override, threaded
through the full five touchpoints — config `[backup].sponsorblock_mode`
(default "mark", preserving prior behavior), DownloadOptions override,
a downloader resolver (apply_sponsorblock), and both UIs (egui Settings
+ channel dialog; web Settings modal + channel dialog + SettingsPayload).

Integration tests extended: settings round-trip asserts the sponsorblock
default + persistence; channel-options round-trip asserts the override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-07 03:53:49 -07:00
parent 6f174f703b
commit 3031b5b0e5
7 changed files with 219 additions and 20 deletions

View file

@ -207,6 +207,7 @@ fn settings_roundtrip_and_persist() {
let (code, body) = s.get("/api/settings");
assert_eq!(code, 200);
assert_eq!(field(&body, "convert_mode"), Some(""), "default convert off");
assert_eq!(field(&body, "sponsorblock_mode"), Some("mark"), "default sponsorblock = mark");
// Flip a few global settings.
let (code, _) = s.post("/api/settings", r#"{
@ -214,6 +215,7 @@ fn settings_roundtrip_and_persist() {
"max_concurrent":5,"use_bundled_ytdlp":false,"use_pot_provider":false,
"subtitles_enabled":true,"subtitles_auto":false,"subtitles_embed":true,
"subtitle_langs":"en","subtitle_format":"srt","youtube_player_clients":"tv,mweb",
"sponsorblock_mode":"remove",
"convert_mode":"h264-mp4","convert_crf":28,"convert_preset":"fast",
"convert_audio_format":"","convert_keep_original":true
}"#);
@ -224,12 +226,14 @@ fn settings_roundtrip_and_persist() {
assert_eq!(field(&body, "convert_mode"), Some("h264-mp4"));
assert_eq!(field(&body, "convert_crf"), Some("28"));
assert_eq!(field(&body, "youtube_player_clients"), Some("tv,mweb"));
assert_eq!(field(&body, "sponsorblock_mode"), Some("remove"));
assert_eq!(field(&body, "subtitle_format"), Some("srt"));
// …and so does config.toml on disk.
let cfg = std::fs::read_to_string(s.dir.join("config.toml")).unwrap();
assert!(cfg.contains("mode = \"h264-mp4\""), "config persisted convert mode:\n{cfg}");
assert!(cfg.contains("youtube_player_clients = \"tv,mweb\""), "config persisted clients");
assert!(cfg.contains("sponsorblock_mode = \"remove\""), "config persisted sponsorblock");
}
#[test]
@ -288,12 +292,13 @@ fn channel_options_roundtrip_and_clear() {
// Store an override.
let (code, _) = s.post(
"/api/channels/channels/SomeChannel/options",
r#"{"convert_mode":"audio","youtube_player_clients":"tv","subtitles_enabled":false}"#,
r#"{"convert_mode":"audio","youtube_player_clients":"tv","sponsorblock_mode":"off","subtitles_enabled":false}"#,
);
assert_eq!(code, 200);
let (_, opts) = s.get("/api/channels/channels/SomeChannel/options");
assert_eq!(field(&opts, "convert_mode"), Some("audio"));
assert_eq!(field(&opts, "youtube_player_clients"), Some("tv"));
assert_eq!(field(&opts, "sponsorblock_mode"), Some("off"));
// An all-default body hits the is_empty() delete path → back to defaults.
let (code, _) = s.post("/api/channels/channels/SomeChannel/options", r#"{}"#);