Watch Later — user-curated antidote to algorithm feeds + MED-8 SB size cap

Cobb 2026-05-23: 'trending on youtube is cancer. only braindead zombies
will want what is trending on youtube.' So no Trending entry. Instead:

Watch Later — pure user-curated. Pin a video from any listing context
menu ('Add to Watch Later'); it persists to watch_later.json under
addon_data with full metadata {id, name, channel, duration, thumbnail}
so re-rendering the list doesn't need to re-fetch from rustypipe.
Newest first, dedupe on id, cap WATCH_LATER_MAX=500.

New ops + actions:
- _watch_later_directory (action=watch_later) — renders saved videos
  with _add_video_items(in_watch_later=True). Each item gets a
  'Remove from Watch Later' context entry; the Add entry is suppressed.
- _wl_add_action (action=wl_add) — RunPlugin-style handler that gets
  the id from the URL, calls sidecar 'resolve' for fresh metadata
  (falls back to id-only if resolve fails), saves into watch_later.json,
  notification toast.
- _wl_remove_action (action=wl_remove) — symmetric remove. Triggers
  Container.Refresh so the item disappears immediately from the list.
- Root menu gains a 'Watch Later (N)' entry, always present, with the
  count when N>0.
- _add_video_items now accepts in_watch_later=bool and adds either Add
  or Remove to the context menu accordingly.

MED-8 from the audit: SponsorBlock response capped at 1 MiB before
JSON parse. Normal SponsorBlock responses are tens of KB; a degenerate
prefix collision or malicious mirror returning gigabytes would
otherwise be deserialized into memory before we filter. resp.bytes()
+ length check + serde_json::from_slice.

Verified live via JSON-RPC (browse-only, Leia not interrupted):
- Empty WL → notification path
- Addons.ExecuteAddon wl_add for LTT 2T8x5antlnc → watch_later.json
  has full metadata block
- watch_later dir lists 'The Internet was WRONG: Trump Phone is
  Shipping  ·  Linus Tech Tips  ·  14:08'
- Root menu shows 'Watch Later  (1)' alongside Search + Play by URL +
  Recent searches.

Saved feedback memory at memory/feedback_no_youtube_trending.md so
future me doesn't propose Trending again.

Addon v0.0.15.
This commit is contained in:
Kayos 2026-05-23 12:38:32 -07:00
parent f1c7264e75
commit 503dbef5df
3 changed files with 192 additions and 12 deletions

View file

@ -63,7 +63,20 @@ pub(crate) async fn fetch(id: &str, categories: &[String]) -> anyhow::Result<ser
anyhow::bail!("sponsorblock http {}", resp.status());
}
let body: Vec<ApiResponse> = resp.json().await?;
// Cap the response body at 1 MiB. A normal prefix collision returns
// tens of KB; a degenerate response (or a hostile API mirror) returning
// gigabytes would otherwise be deserialized straight into memory before
// we filter to our target video_id.
const SPONSORBLOCK_MAX_BYTES: usize = 1 * 1024 * 1024;
let bytes = resp.bytes().await?;
if bytes.len() > SPONSORBLOCK_MAX_BYTES {
anyhow::bail!(
"sponsorblock response too large: {} bytes (cap {})",
bytes.len(),
SPONSORBLOCK_MAX_BYTES
);
}
let body: Vec<ApiResponse> = serde_json::from_slice(&bytes)?;
// Filter to the exact video id (the API returns all videos sharing the prefix).
let segments: Vec<&ApiSegment> = body