Live-stream recording, recent-additions feed, per-platform impersonate profiles

Three roadmap items together since they touch overlapping code:

Live-stream recording
- New `live: bool` argument on Downloader::start. When true, attaches
  `--live-from-start --wait-for-video 30`, suppresses
  `--break-on-existing` (every recording is a unique file), and
  prepends a UTC timestamp suffix to the output filename so
  re-recordings of the same broadcaster don't collide.
- The job label gains a "🔴 LIVE" marker so a long-running record is
  obviously different from a VOD pull.
- Both UIs gain a "🔴 Live stream" checkbox in the download panel,
  hidden when Music mode is selected. Works for Twitch *and* YouTube
  Live since yt-dlp recognizes both from the URL.
- Web `POST /api/download` accepts a `live: bool` body field; all
  other callers (scheduled re-check, right-click "Check for new
  videos") pass false.

Recent-additions feed
- `library::Video` gains `mtime_unix: Option<u64>` — populated from
  the metadata we already read for `file_size`, so no extra fs hit.
- New `SidebarView::Recent` (desktop) + `showRecent` (web) sort the
  whole library by mtime descending and cap at 100 entries.
- Sidebar entry "🕒 Recent additions" appears only when the library
  has any dated content, so a fresh install doesn't show an empty view.

Per-platform impersonation
- New `Platform::impersonate_target()`:
  - Twitch → None (skip impersonation; their OAuth/Helix dislikes
    mismatched TLS fingerprints).
  - TikTok → "Chrome-Android-131" (mobile profile matches their
    first-party app surface).
  - Everything else → "Chrome-146:Macos-26".
- `Downloader::apply_impersonation` now takes the platform and routes
  through the override; `repair()` (always YouTube-implicit) and
  `start_music()` (uses classify_url on the URL) wire through too.
- `/api/preview` does the same classification before dispatching to
  yt-dlp.
- 3 new tests confirm Twitch → None, TikTok → mobile, YouTube → desktop.

44 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-05-25 00:17:59 -07:00
parent c0be4bb533
commit b24ef4be67
5 changed files with 245 additions and 42 deletions

View file

@ -72,6 +72,10 @@ pub struct Video {
pub has_chapters: bool,
/// Size of the video file on disk; `None` if the video file is missing.
pub file_size: Option<u64>,
/// Filesystem mtime of the video file as a UNIX timestamp (seconds).
/// Used by the activity feed to surface recent additions. `None` if the
/// video file is missing or the system clock returned an error.
pub mtime_unix: Option<u64>,
/// Upload date as `YYYYMMDD` (yt-dlp's native format from info.json).
/// `None` if the info.json sidecar is missing or lacks the field.
pub upload_date: Option<String>,
@ -404,9 +408,14 @@ fn enrich(raws: Vec<RawVideo>) -> Vec<Video> {
(dur, chap, date)
})
.unwrap_or((None, false, None));
let file_size = raw.video_path.as_ref()
.and_then(|p| std::fs::metadata(p).ok())
.map(|m| m.len());
let metadata = raw.video_path.as_ref()
.and_then(|p| std::fs::metadata(p).ok());
let file_size = metadata.as_ref().map(|m| m.len());
let mtime_unix = metadata
.as_ref()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs());
Video {
id: raw.id,
title: raw.title,
@ -420,6 +429,7 @@ fn enrich(raws: Vec<RawVideo>) -> Vec<Video> {
duration_secs,
has_chapters,
file_size,
mtime_unix,
upload_date,
}
}).collect();