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:
parent
c0be4bb533
commit
b24ef4be67
5 changed files with 245 additions and 42 deletions
|
|
@ -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-"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue