feat: subtitles while streaming, and fetched when a download lacks them
YouTube's HLS manifest carries a dozen audio renditions and no subtitles whatsoever — checked against a live master playlist: 16 EXT-X-MEDIA entries, all TYPE=AUDIO, zero SUBTITLES. So the track menu could only ever list audio while streaming, which is what it did. Subtitles are now fetched separately with yt-dlp, tidied through the existing VTT cleanup, and cached per video and language. They reach the player as blob URLs, which share the document's origin — a file:// or 127.0.0.1 track would be cross-origin to the page and need CORS the media pipeline cannot supply. The same path fills in a download saved before subtitles were switched on, without fetching the video again. YouTube serves identical auto-generated captions under both "en" and "en-orig", so byte-identical texts collapse to one entry rather than offering the same track twice, and tracks are labelled "English" rather than "en". The subtitle preference now defaults to English. "None" is a poor default for a setting whose whole purpose is captions: it silently means no subtitles are downloaded, fetched, or offered anywhere, and the Settings text now says so.
This commit is contained in:
@@ -135,6 +135,32 @@ pub fn build_args(
|
||||
args
|
||||
}
|
||||
|
||||
/// Arguments for fetching only the subtitles of a video.
|
||||
///
|
||||
/// YouTube's HLS manifest carries audio renditions but no subtitles whatsoever,
|
||||
/// so a streamed video has nothing to show unless the captions are fetched
|
||||
/// separately. Auto-generated captions are included: on most videos they are
|
||||
/// the only ones there are.
|
||||
pub fn subs_only_args(video_id: &str, out_template: &str, sub_langs: &str) -> Vec<String> {
|
||||
vec![
|
||||
"--skip-download".into(),
|
||||
"--no-playlist".into(),
|
||||
"--no-colors".into(),
|
||||
"--ignore-errors".into(),
|
||||
"--write-subs".into(),
|
||||
"--write-auto-subs".into(),
|
||||
"--sub-format".into(),
|
||||
"vtt".into(),
|
||||
"--convert-subs".into(),
|
||||
"vtt".into(),
|
||||
"--sub-langs".into(),
|
||||
sub_langs.to_string(),
|
||||
"-o".into(),
|
||||
out_template.into(),
|
||||
format!("https://www.youtube.com/watch?v={video_id}"),
|
||||
]
|
||||
}
|
||||
|
||||
/// The subtitle languages to request for a preference, or empty for none.
|
||||
///
|
||||
/// Only the language itself and YouTube's "-orig" variant; anything broader
|
||||
@@ -160,6 +186,29 @@ pub fn parse_final_path(line: &str) -> Option<String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn subs_only_args_take_auto_generated_captions() {
|
||||
let args = subs_only_args("abc", "/tmp/%(id)s.%(ext)s", "en,en-orig");
|
||||
// Most videos have no hand-written captions at all; without this the
|
||||
// fetch comes back empty.
|
||||
assert!(args.iter().any(|a| a == "--write-auto-subs"));
|
||||
assert!(args.iter().any(|a| a == "--write-subs"));
|
||||
assert!(args.iter().any(|a| a == "--skip-download"));
|
||||
// Named exactly: a wildcard drags in dozens of machine translations.
|
||||
let langs = args.iter().position(|a| a == "--sub-langs").unwrap();
|
||||
assert_eq!(args[langs + 1], "en,en-orig");
|
||||
assert_eq!(args.last().unwrap(), "https://www.youtube.com/watch?v=abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subs_only_args_are_written_where_asked() {
|
||||
let args = subs_only_args("abc", "/cache/%(id)s.%(ext)s", "nl,nl-orig");
|
||||
let out = args.iter().position(|a| a == "-o").unwrap();
|
||||
assert_eq!(args[out + 1], "/cache/%(id)s.%(ext)s");
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_complete_line() {
|
||||
let p = parse_progress_line("FTPROG 1048576 10485760 524288.0 18").unwrap();
|
||||
|
||||
Reference in New Issue
Block a user