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

@ -142,6 +142,26 @@ impl Platform {
pub fn is_audio_first(self) -> bool {
matches!(self, Self::Bandcamp | Self::SoundCloud)
}
/// yt-dlp `--impersonate` target tuned for each source. Returns `None`
/// when impersonation should be skipped entirely (e.g. Twitch's OAuth
/// flow can object to mismatched TLS fingerprints).
///
/// Targets follow yt-dlp's format. TikTok matches the patterns it
/// expects from its first-party mobile app so a desktop fingerprint
/// doesn't trip its bot scoring; everything else gets a recent desktop
/// Chrome.
pub fn impersonate_target(self) -> Option<&'static str> {
match self {
// Twitch's auth surface dislikes TLS-fingerprint impersonation;
// omit the flag so curl_cffi doesn't break OAuth/Helix calls.
Self::Twitch => None,
// TikTok's API is mobile-first — pretend to be Chrome on Android.
Self::TikTok => Some("Chrome-Android-131"),
// Default: recent desktop Chrome on macOS.
_ => Some("Chrome-146:Macos-26"),
}
}
}
/// What kind of URL we're looking at — drives the yt-dlp `-o` template.
@ -460,4 +480,22 @@ mod tests {
assert_eq!(platform_root(cr, Platform::TikTok), Path::new("/foo/tiktok"));
assert_eq!(platform_root(cr, Platform::Twitch), Path::new("/foo/twitch"));
}
#[test]
fn twitch_skips_impersonation() {
// TLS fingerprint impersonation can interfere with Twitch's OAuth/Helix.
assert!(Platform::Twitch.impersonate_target().is_none());
}
#[test]
fn tiktok_uses_mobile_profile() {
let t = Platform::TikTok.impersonate_target().unwrap();
assert!(t.to_lowercase().contains("android"));
}
#[test]
fn youtube_uses_desktop_chrome() {
let t = Platform::YouTube.impersonate_target().unwrap();
assert!(t.starts_with("Chrome-"));
}
}