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:
parent
6f174f703b
commit
3031b5b0e5
7 changed files with 219 additions and 20 deletions
16
src/web.rs
16
src/web.rs
|
|
@ -371,6 +371,9 @@ struct SettingsPayload {
|
|||
/// YouTube player clients (comma-separated). Empty = yt-dlp defaults.
|
||||
#[serde(default)]
|
||||
youtube_player_clients: String,
|
||||
/// SponsorBlock mode: "off" / "mark" / "remove".
|
||||
#[serde(default)]
|
||||
sponsorblock_mode: String,
|
||||
/// Global [convert] settings, round-tripped on GET + POST.
|
||||
#[serde(default)]
|
||||
convert_mode: String,
|
||||
|
|
@ -1369,6 +1372,7 @@ async fn get_settings(State(state): State<Arc<WebState>>) -> impl IntoResponse {
|
|||
let use_pot_provider = cfg.backup.use_pot_provider;
|
||||
let subs = cfg.subtitles.clone();
|
||||
let player_clients_out = cfg.backup.youtube_player_clients.clone();
|
||||
let sponsorblock_out = cfg.backup.sponsorblock_mode.clone();
|
||||
let convert = cfg.convert.clone();
|
||||
drop(cfg);
|
||||
|
||||
|
|
@ -1407,6 +1411,7 @@ async fn get_settings(State(state): State<Arc<WebState>>) -> impl IntoResponse {
|
|||
subtitle_format: subs.format,
|
||||
subtitle_langs: subs.langs,
|
||||
youtube_player_clients: player_clients_out.clone(),
|
||||
sponsorblock_mode: sponsorblock_out.clone(),
|
||||
convert_mode: convert.mode.clone(),
|
||||
convert_crf: convert.crf,
|
||||
convert_preset: convert.preset.clone(),
|
||||
|
|
@ -1452,6 +1457,7 @@ async fn post_settings(
|
|||
cfg.subtitles.format = body.subtitle_format.trim().to_string();
|
||||
cfg.subtitles.langs = body.subtitle_langs.trim().to_string();
|
||||
cfg.backup.youtube_player_clients = body.youtube_player_clients.trim().to_string();
|
||||
cfg.backup.sponsorblock_mode = body.sponsorblock_mode.trim().to_string();
|
||||
// Global format-conversion defaults.
|
||||
cfg.convert.mode = body.convert_mode.trim().to_string();
|
||||
cfg.convert.crf = body.convert_crf;
|
||||
|
|
@ -1473,6 +1479,7 @@ async fn post_settings(
|
|||
let use_pot_provider = cfg.backup.use_pot_provider;
|
||||
let subs = cfg.subtitles.clone();
|
||||
let player_clients_out = cfg.backup.youtube_player_clients.clone();
|
||||
let sponsorblock_out = cfg.backup.sponsorblock_mode.clone();
|
||||
let convert = cfg.convert.clone();
|
||||
drop(cfg);
|
||||
|
||||
|
|
@ -1486,6 +1493,7 @@ async fn post_settings(
|
|||
dl.use_pot_provider = use_pot_provider;
|
||||
dl.subtitle_defaults = subs.clone();
|
||||
dl.youtube_player_clients = player_clients_out.clone();
|
||||
dl.sponsorblock_mode = sponsorblock_out.clone();
|
||||
dl.convert_defaults = convert.clone();
|
||||
}
|
||||
|
||||
|
|
@ -1542,6 +1550,7 @@ async fn post_settings(
|
|||
subtitle_format: subs.format,
|
||||
subtitle_langs: subs.langs,
|
||||
youtube_player_clients: player_clients_out.clone(),
|
||||
sponsorblock_mode: sponsorblock_out.clone(),
|
||||
convert_mode: convert.mode.clone(),
|
||||
convert_crf: convert.crf,
|
||||
convert_preset: convert.preset.clone(),
|
||||
|
|
@ -1832,6 +1841,10 @@ async fn get_comments(
|
|||
let time = c.get("_time_text").and_then(|v| v.as_str())
|
||||
.or_else(|| c.get("time_text").and_then(|v| v.as_str()))
|
||||
.map(String::from);
|
||||
// Absolute post time (unix seconds) when yt-dlp captured it —
|
||||
// drives "newest" sorting and the new-since-last-visit highlight.
|
||||
let timestamp = c.get("timestamp").and_then(|v| v.as_i64());
|
||||
let is_uploader = c.get("author_is_uploader").and_then(|v| v.as_bool());
|
||||
Some(serde_json::json!({
|
||||
"id": id,
|
||||
"author": author,
|
||||
|
|
@ -1839,6 +1852,8 @@ async fn get_comments(
|
|||
"likes": likes,
|
||||
"parent": parent,
|
||||
"time": time,
|
||||
"timestamp": timestamp,
|
||||
"is_uploader": is_uploader,
|
||||
}))
|
||||
}).collect::<Vec<_>>()
|
||||
})
|
||||
|
|
@ -2528,6 +2543,7 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
|||
);
|
||||
downloader.subtitle_defaults = config.subtitles.clone();
|
||||
downloader.youtube_player_clients = config.backup.youtube_player_clients.clone();
|
||||
downloader.sponsorblock_mode = config.backup.sponsorblock_mode.clone();
|
||||
downloader.convert_defaults = config.convert.clone();
|
||||
let music_root = downloader.music_root();
|
||||
let _ = std::fs::create_dir_all(&music_root);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue