feat: support multiple video selection

This commit is contained in:
2026-10-22 14:01:47 +02:00
parent acfb3be1e7
commit 2d30120072
12 changed files with 883 additions and 169 deletions
+82 -14
View File
@@ -1,8 +1,9 @@
use std::{
collections::VecDeque,
collections::{BTreeSet, VecDeque},
ffi::OsStr,
fs::{self, OpenOptions},
io::{self, Read},
path::Path,
path::PathBuf,
process::{Child, Command, Stdio},
sync::mpsc::{self, Receiver, TryRecvError},
thread,
@@ -914,20 +915,87 @@ pub struct PlayerLaunch {
}
impl PlayerLaunch {
pub fn start(id: &str, player: &OsStr, progress_file: &Path) -> io::Result<Self> {
let url = if valid_video_id(id) {
format!("https://www.youtube.com/watch?v={id}")
} else {
tubular::miruro::Identity::from_key(id)
.and_then(|identity| identity.watch_url())
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?
pub fn start(
videos: &[Video],
player: &OsStr,
state: &mut crate::video_state::State,
) -> io::Result<Self> {
if videos.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"No videos selected",
));
}
let playback_url = |id: &str| {
if valid_video_id(id) {
Ok(format!("https://www.youtube.com/watch?v={id}"))
} else {
tubular::miruro::Identity::from_key(id)
.and_then(|identity| identity.watch_url())
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))
}
};
crate::video_state::register_identity(progress_file, id)?;
let mut command = isolated_command(player);
command
.args(["--new-instance", "--url", &url])
.arg("--progress-file")
.arg(progress_file);
command.arg("--new-instance");
if let [video] = videos {
let url = playback_url(&video.id)?;
let report = state.progress_file(video)?;
crate::video_state::register_identity(&report, &video.id)?;
command
.args(["--url", &url])
.arg("--progress-file")
.arg(report);
return Self::launch(command);
}
let mut ids = BTreeSet::new();
let urls = videos
.iter()
.map(|video| {
if !ids.insert(video.id.as_str()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Duplicate queue identity",
));
}
playback_url(&video.id)
})
.collect::<io::Result<Vec<_>>>()?;
let reports = state.progress_files(videos)?;
#[derive(Serialize)]
struct QueueItem {
url: String,
progress_file: PathBuf,
}
#[derive(Serialize)]
struct Queue {
version: u8,
items: Vec<QueueItem>,
}
let items = videos
.iter()
.zip(urls)
.zip(reports)
.map(|((video, url), report)| {
crate::video_state::register_identity(&report, &video.id)?;
let parent = fs::canonicalize(report.parent().expect("session directory"))?;
Ok(QueueItem {
url,
progress_file: parent.join("progress.jsonl"),
})
})
.collect::<io::Result<Vec<_>>>()?;
let manifest = items[0].progress_file.with_file_name("queue.json");
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let file = options.open(&manifest)?;
serde_json::to_writer(&file, &Queue { version: 1, items })?;
file.sync_all()?;
command.arg("--queue-file").arg(manifest);
Self::launch(command)
}