diff options
Diffstat (limited to 'sys/nixpkgs/pkgs/ytc/src/main.rs')
-rw-r--r-- | sys/nixpkgs/pkgs/ytc/src/main.rs | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/sys/nixpkgs/pkgs/ytc/src/main.rs b/sys/nixpkgs/pkgs/ytc/src/main.rs new file mode 100644 index 00000000..5c7849b8 --- /dev/null +++ b/sys/nixpkgs/pkgs/ytc/src/main.rs @@ -0,0 +1,188 @@ +use std::{ + env, + fs::{self, canonicalize}, + io::{stderr, stdout}, + os::unix::fs::symlink, + path::PathBuf, + process::Command as StdCmd, +}; + +use anyhow::{bail, Context, Result}; +use clap::{Parser, Subcommand}; +use downloader::Downloader; +use log::debug; +use serde::Deserialize; + +mod downloader; + +const STATUS_PATH: &str = "ytcc/running"; + +/// A helper for downloading and playing youtube videos +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + /// The subcommand to execute + pub subcommand: Command, +} +#[derive(Subcommand, Debug)] +pub enum Command { + #[clap(value_parser)] + /// Work based of ytcc ids + Id { + #[clap(value_parser)] + /// A list of ids to play + ids: Vec<u32>, + }, + #[clap(value_parser)] + /// Work based of raw youtube urls + Url { + #[clap(value_parser)] + /// A list of urls to play + urls: Vec<String>, + }, +} + +struct PlayThing { + url: String, + id: Option<u32>, +} + +#[derive(Deserialize)] +struct YtccListData { + url: String, + #[allow(unused)] + title: String, + #[allow(unused)] + description: String, + #[allow(unused)] + publish_date: String, + #[allow(unused)] + watch_date: Option<String>, + #[allow(unused)] + duration: String, + #[allow(unused)] + thumbnail_url: String, + #[allow(unused)] + extractor_hash: String, + id: u32, + #[allow(unused)] + playlists: Vec<YtccPlaylistData>, +} +#[derive(Deserialize)] +struct YtccPlaylistData { + #[allow(unused)] + name: String, + #[allow(unused)] + url: String, + #[allow(unused)] + reverse: bool, +} + +fn main() -> Result<()> { + let args = Args::parse(); + cli_log::init_cli_log!(); + + let playspec: Vec<PlayThing> = match args.subcommand { + Command::Id { ids } => { + let mut output = Vec::with_capacity(ids.len()); + for id in ids { + debug!("Adding {}", id); + let mut ytcc = StdCmd::new("ytcc"); + ytcc.args([ + "--output", + "json", + "list", + "--attributes", + "url", + "--ids", + id.to_string().as_str(), + ]); + let json = serde_json::from_slice::<Vec<YtccListData>>( + &ytcc.output().context("Failed to get url from id")?.stdout, + ) + .context("Failed to deserialize json output")?; + + if json.len() == 0 { + bail!("Could not find a video with id: {}", id); + } + assert_eq!(json.len(), 1); + let json = json.first().expect("Has only one element"); + + debug!("Id resolved to: '{}'", &json.url); + + output.push(PlayThing { + url: json.url.clone(), + id: Some(json.id), + }) + } + output + } + Command::Url { urls } => urls + .into_iter() + .map(|url| PlayThing { url, id: None }) + .collect(), + }; + + debug!("Initializing downloader"); + let mut downloader = Downloader::new(playspec)?; + + while let Some((path, id)) = downloader.next() { + debug!("Next path to play is: '{}'", path.display()); + let mut info_json = canonicalize(&path).context("Failed to canoncialize path")?; + info_json.set_extension("info.json"); + + if status_path()?.is_symlink() { + fs::remove_file(status_path()?).context("Failed to delete old status file")?; + } else { + bail!( + "The status path ('{}') is not a symlink!", + status_path()?.display() + ); + } + + symlink(info_json, status_path()?).context("Failed to symlink")?; + + let mut mpv = StdCmd::new("mpv"); + mpv.stdout(stdout()); + mpv.stderr(stderr()); + mpv.args(["--speed=2.7", "--volume=75"]); + mpv.arg(&path); + + let status = mpv.status().context("Failed to run mpv")?; + if let Some(code) = status.code() { + if let Some(id) = id { + if code == 0 { + println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id); + fs::remove_file(&path)?; + let mut ytcc = StdCmd::new("ytcc"); + ytcc.stdout(stdout()); + ytcc.stderr(stderr()); + ytcc.args(["mark"]); + ytcc.arg(id.to_string()); + let status = ytcc.status().context("Failed to run ytcc")?; + if let Some(code) = status.code() { + if code != 0 { + bail!("Ytcc failed with status: {}", code); + } + } + } + } + debug!("Mpv exited with: {}", code); + } + } + downloader.drop()?; + + Ok(()) +} + +fn status_path() -> Result<PathBuf> { + let out: PathBuf = format!( + "{}/{}", + env::var("XDG_RUNTIME_DIR").expect("This should always exist"), + STATUS_PATH + ) + .into(); + fs::create_dir_all(&out.parent().expect("Parent should exist"))?; + Ok(out) +} |