aboutsummaryrefslogtreecommitdiffstats
path: root/sys/nixpkgs/pkgs/ytc/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sys/nixpkgs/pkgs/ytc/src/main.rs188
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)
+}