feat: open selected YouTube video in browser

This commit is contained in:
2026-10-17 16:54:23 +02:00
parent f8334716af
commit ba04cbe158
4 changed files with 94 additions and 0 deletions
+56
View File
@@ -805,6 +805,52 @@ fn failure(errors: &Receiver<io::Result<String>>, fallback: String) -> Option<Re
}
}
pub fn open_in_browser(id: &str) -> io::Result<Receiver<Result<(), String>>> {
if !valid_video_id(id) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid YouTube video ID",
));
}
let opener = if cfg!(target_os = "macos") {
"open"
} else {
"xdg-open"
};
let child = isolated_command(OsStr::new(opener))
.arg(format!("https://www.youtube.com/watch?v={id}"))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.map_err(|error| {
io::Error::new(
error.kind(),
format!("Cannot start browser opener: {error}"),
)
})?;
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
let result = child
.wait_with_output()
.map_err(|error| format!("Cannot wait for browser opener: {error}"))
.and_then(|output| {
if output.status.success() {
Ok(())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(format!(
"Browser opener exited with {}: {}",
output.status,
error.trim()
))
}
});
let _ = sender.send(result);
});
Ok(receiver)
}
pub struct PlayerLaunch {
player: Child,
player_errors: Receiver<io::Result<String>>,
@@ -1465,6 +1511,16 @@ mod tests {
}
}
#[test]
fn browser_open_rejects_unsafe_video_ids_before_spawning() {
for id in ["", "--help", "jNQXAC9IVRw&list=other", "日本語"] {
assert!(matches!(
open_in_browser(id),
Err(error) if error.kind() == io::ErrorKind::InvalidInput
));
}
}
#[test]
fn publication_dates_prefer_exact_metadata_and_handle_utc_boundaries_and_invalid_dates() {
for feed in [false, true] {