diff options
author | Soispha <soispha@vhack.eu> | 2024-01-20 12:03:46 +0100 |
---|---|---|
committer | Soispha <soispha@vhack.eu> | 2024-01-20 12:03:46 +0100 |
commit | 089c08af03849fb2883a17726462ca0d8ce8b51f (patch) | |
tree | d5d9285c6beab0babd16fe533b621ed56280bdd4 /sys/nixpkgs/pkgs/yt | |
parent | feat(hm/conf/nvim/plgs/treesitter): Add custom parser for `yts` (diff) | |
download | nixos-config-089c08af03849fb2883a17726462ca0d8ce8b51f.tar.gz nixos-config-089c08af03849fb2883a17726462ca0d8ce8b51f.zip |
feat(sys/nixpkgs/pkgs/yt): Merge ytc and the rewritten ytc
Diffstat (limited to '')
20 files changed, 869 insertions, 295 deletions
diff --git a/sys/nixpkgs/pkgs/yt/.env b/sys/nixpkgs/pkgs/yt/.env new file mode 100755 index 00000000..8018a738 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/.env @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +PATH="$(pwd)/target/release/:$(pwd)/target/debug/:$PATH" diff --git a/sys/nixpkgs/pkgs/ytc/.gitignore b/sys/nixpkgs/pkgs/yt/.gitignore index c84fa049..c84fa049 100644 --- a/sys/nixpkgs/pkgs/ytc/.gitignore +++ b/sys/nixpkgs/pkgs/yt/.gitignore diff --git a/sys/nixpkgs/pkgs/ytc/Cargo.lock b/sys/nixpkgs/pkgs/yt/Cargo.lock index 630580c1..1f5501d6 100644 --- a/sys/nixpkgs/pkgs/ytc/Cargo.lock +++ b/sys/nixpkgs/pkgs/yt/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", @@ -85,9 +85,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bumpalo" @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.13" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -211,6 +211,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9544f10105d33957765016b8a9baea7e689bf1f0f2f32c2fa2f568770c38d2b3" [[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -240,6 +249,16 @@ dependencies = [ ] [[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -247,24 +266,24 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" @@ -288,10 +307,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -325,11 +350,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -344,18 +369,18 @@ checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "serde" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", @@ -424,12 +449,53 @@ dependencies = [ ] [[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -437,9 +503,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -447,9 +513,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", @@ -462,9 +528,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -472,9 +538,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", @@ -485,9 +551,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "windows-core" @@ -622,7 +688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] -name = "ytc" +name = "yt" version = "0.1.0" dependencies = [ "anyhow", @@ -632,4 +698,5 @@ dependencies = [ "serde", "serde_json", "tempfile", + "url", ] diff --git a/sys/nixpkgs/pkgs/yt/Cargo.toml b/sys/nixpkgs/pkgs/yt/Cargo.toml new file mode 100644 index 00000000..faad6699 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "yt" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.79" +clap = { version = "4.4.13", features = ["derive"] } +cli-log = "2.0.0" +log = "0.4.20" +serde = { version = "1.0.195", features = ["derive"] } +serde_json = "1.0.111" +tempfile = "3.9.0" +url = "2.5.0" + +# This is here to be able to tell nix which binary to build +[features] +yts = [] +ytc = [] +yt = [] +default = ["yt"] + +[[bin]] +name = "yts" +required-features = ["yts"] + +[[bin]] +name = "ytc" +required-features = ["ytc"] + +[[bin]] +name = "yt" +required-features = ["yt"] diff --git a/sys/nixpkgs/pkgs/yt/default.nix b/sys/nixpkgs/pkgs/yt/default.nix new file mode 100644 index 00000000..63bce5c3 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/default.nix @@ -0,0 +1,51 @@ +[ + ( + final: prev: { + yt = import ./yt.nix { + inherit + (prev) + lib + makeWrapper + rustPlatform + # dependencies + + ytcc + yt-dlp + mpv + ; + }; + } + ) + ( + final: prev: { + yts = import ./yts.nix { + inherit + (prev) + lib + makeWrapper + rustPlatform + # dependencies + + ytcc + ; + }; + } + ) + ( + final: prev: { + ytc = import ./ytc.nix { + inherit + (prev) + lib + makeWrapper + rustPlatform + # dependencies + + ytcc + yt-dlp + mpv + ; + }; + } + ) +] diff --git a/sys/nixpkgs/pkgs/yt/src/bin/yt/main.rs b/sys/nixpkgs/pkgs/yt/src/bin/yt/main.rs new file mode 100644 index 00000000..54d89daa --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/bin/yt/main.rs @@ -0,0 +1,110 @@ +use anyhow::{bail, Context, Result}; +use std::{ + env, + io::{BufRead, BufReader, BufWriter, Write}, + process::Command as StdCmd, +}; +use tempfile::NamedTempFile; +use yt::{ + constants::HELP_STR, + downloader::{Downloadable, Downloader}, + ytcc_drop, Duration, Line, LineCommand, YtccListData, +}; + +fn main() -> Result<()> { + cli_log::init_cli_log!(); + + let json_map = { + let mut ytcc = StdCmd::new("ytcc"); + ytcc.args([ + "--output", + "json", + "list", + "--order-by", + "publish_date", + "desc", + ]); + + serde_json::from_slice::<Vec<YtccListData>>( + &ytcc.output().context("Failed to json from ytcc")?.stdout, + ) + .context("Failed to deserialize json output")? + }; + + let temp_file = NamedTempFile::new().context("Failed to get tempfile")?; + let mut edit_file = BufWriter::new(&temp_file); + + json_map + .iter() + .map(|line| { + format!( + r#"pick {} "{}" "{}" "{}" "{}" "{}"{}"#, + line.id, + line.title.replace('"', "'"), + line.publish_date, + line.playlists + .iter() + .map(|p| p.name.replace('"', "'")) + .collect::<Vec<String>>() + .join(", "), + Duration::from(line.duration.trim()), + line.url.replace('"', "'"), + "\n" + ) + }) + .for_each(|line| { + edit_file + .write(line.as_bytes()) + .expect("This write should not fail"); + }); + + edit_file.write(HELP_STR.as_bytes())?; + edit_file.flush().context("Failed to flush edit file")?; + + let read_file = temp_file.reopen()?; + + let mut nvim = StdCmd::new("nvim"); + nvim.arg(temp_file.path()); + + let status = nvim.status().context("Falied to run nvim")?; + if !status.success() { + bail!("nvim exited with error status: {}", status) + } + + let mut watching = Vec::new(); + let reader = BufReader::new(&read_file); + for line in reader.lines() { + let line = line.context("Failed to read line")?; + + if line.starts_with("#") { + // comment + continue; + } else if line.trim().len() == 0 { + // empty line + continue; + } + + let line = Line::from(line.as_str()); + match line.cmd { + LineCommand::Pick => (), + LineCommand::Drop => { + ytcc_drop(line.id).with_context(|| format!("Failed to drop: {}", line.id))? + } + LineCommand::Watch => watching.push(Downloadable { + id: Some(line.id), + url: line.url, + }), + } + } + + if watching.len() == 0 { + return Ok(()); + } + + let downloader = Downloader::new(watching).context("Failed to construct downloader")?; + downloader + .consume() + .context("Failed to consume downloader")?; + + Ok(()) +} diff --git a/sys/nixpkgs/pkgs/yt/src/bin/ytc/args.rs b/sys/nixpkgs/pkgs/yt/src/bin/ytc/args.rs new file mode 100644 index 00000000..8b2d6a61 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/bin/ytc/args.rs @@ -0,0 +1,26 @@ +use clap::{Parser, Subcommand}; +/// 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>, + }, +} diff --git a/sys/nixpkgs/pkgs/yt/src/bin/ytc/main.rs b/sys/nixpkgs/pkgs/yt/src/bin/ytc/main.rs new file mode 100644 index 00000000..437df803 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/bin/ytc/main.rs @@ -0,0 +1,75 @@ +use std::{env, process::Command as StdCmd}; + +use anyhow::{bail, Context, Result}; +use clap::Parser; +use log::debug; +use url::Url; +use yt::{ + downloader::{Downloadable, Downloader}, + YtccListData, +}; + +use crate::args::{Args, Command}; + +mod args; + +fn main() -> Result<()> { + let args = Args::parse(); + cli_log::init_cli_log!(); + + let playspec: Vec<Downloadable> = 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(Downloadable { + url: Url::parse(&json.url)?, + id: Some(json.id), + }) + } + output + } + Command::Url { urls } => { + let mut output = Vec::with_capacity(urls.len()); + for url in urls { + output.push(Downloadable { + url: Url::parse(&url).context("Failed to parse url")?, + id: None, + }) + } + output + } + }; + + debug!("Initializing downloader"); + let downloader = Downloader::new(playspec)?; + + downloader + .consume() + .context("Failed to consume downloader")?; + + Ok(()) +} diff --git a/sys/nixpkgs/pkgs/yt/src/bin/yts/args.rs b/sys/nixpkgs/pkgs/yt/src/bin/yts/args.rs new file mode 100644 index 00000000..56989421 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/bin/yts/args.rs @@ -0,0 +1,41 @@ +use clap::{Parser, Subcommand}; +/// A helper for selecting which videos to download from ytcc to ytc +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + /// subcommand to execute + pub subcommand: Option<Command>, +} + +#[derive(Subcommand, Debug)] +pub enum Command { + #[clap(value_parser)] + /// Which ordering to use + Order { + #[command(subcommand)] + command: OrderCommand, + }, +} + +#[derive(Subcommand, Debug)] +pub enum OrderCommand { + #[clap(value_parser)] + /// Order by date + #[group(required = true)] + Date { + #[arg(value_parser)] + /// Order descending + desc: bool, + #[clap(value_parser)] + /// Order ascending + asc: bool, + }, + #[clap(value_parser)] + /// Pass a raw SQL 'ORDER BY' value + Raw { + #[clap(value_parser)] + /// The raw value(s) to pass + value: Vec<String>, + }, +} diff --git a/sys/nixpkgs/pkgs/yt/src/bin/yts/main.rs b/sys/nixpkgs/pkgs/yt/src/bin/yts/main.rs new file mode 100644 index 00000000..788ecab2 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/bin/yts/main.rs @@ -0,0 +1,109 @@ +use anyhow::{bail, Context, Result}; +use clap::Parser; +use std::{ + env, + io::{BufRead, BufReader, Write}, + process::Command as StdCmd, +}; +use tempfile::NamedTempFile; +use yt::{constants::HELP_STR, ytcc_drop, Line, LineCommand, YtccListData}; + +use crate::args::{Args, Command, OrderCommand}; + +mod args; + +fn main() -> Result<()> { + let args = Args::parse(); + cli_log::init_cli_log!(); + + let ordering = match args.subcommand.unwrap_or(Command::Order { + command: OrderCommand::Date { + desc: true, + asc: false, + }, + }) { + Command::Order { command } => match command { + OrderCommand::Date { desc, asc } => { + if desc { + vec!["--order-by".into(), "publish_date".into(), "desc".into()] + } else if asc { + vec!["--order-by".into(), "publish_date".into(), "asc".into()] + } else { + vec!["--order-by".into(), "publish_date".into(), "desc".into()] + } + } + OrderCommand::Raw { value } => [vec!["--order-by".into()], value].concat(), + }, + }; + + let json_map = { + let mut ytcc = StdCmd::new("ytcc"); + ytcc.args(["--output", "json", "list"]); + ytcc.args(ordering); + + serde_json::from_slice::<Vec<YtccListData>>( + &ytcc.output().context("Failed to json from ytcc")?.stdout, + ) + .context("Failed to deserialize json output")? + }; + + let mut edit_file = NamedTempFile::new().context("Failed to get tempfile")?; + + let file: String = json_map + .iter() + .map(|line| { + format!( + "pick {} \"{}\" <{}> [{}]\n", + line.id, + line.title, + line.playlists + .iter() + .map(|p| &p.name[..]) + .collect::<Vec<&str>>() + .join(", "), + line.duration.trim() + ) + }) + .collect(); + + for line in file.lines() { + writeln!(&edit_file, "{}", line)?; + } + write!(&edit_file, "{}", HELP_STR)?; + edit_file.flush().context("Failed to flush edit file")?; + + let read_file = edit_file.reopen()?; + + let mut nvim = StdCmd::new("nvim"); + nvim.arg(edit_file.path()); + + let status = nvim.status().context("Falied to run nvim")?; + if !status.success() { + bail!("Nvim exited with error status: {}", status) + } + + let mut watching = Vec::new(); + let reader = BufReader::new(&read_file); + for line in reader.lines() { + let line = line.context("Failed to read line")?; + + if line.starts_with("#") { + continue; + } else if line.trim().len() == 0 { + // empty line + continue; + } + + let line = Line::from(line.as_str()); + match line.cmd { + LineCommand::Pick => (), + LineCommand::Drop => { + ytcc_drop(line.id).with_context(|| format!("Failed to drop: {}", line.id))? + } + LineCommand::Watch => watching.push(line.id), + } + } + + dbg!(&watching); + Ok(()) +} diff --git a/sys/nixpkgs/pkgs/yt/src/constants.rs b/sys/nixpkgs/pkgs/yt/src/constants.rs new file mode 100644 index 00000000..23e1d9b9 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/constants.rs @@ -0,0 +1,36 @@ +use std::{env, fs, path::PathBuf}; + +pub const HELP_STR: &'static str = include_str!("./help.str"); + +pub const YT_DLP_FLAGS: [&str; 12] = [ + "--format", + "bestvideo[height<=?1080]+bestaudio/best", + "--embed-chapters", + "--progress", + "--write-comments", + "--extractor-args", + "youtube:max_comments=150,all,100;comment_sort=top", + "--write-info-json", + "--sponsorblock-mark", + "default", + "--sponsorblock-remove", + "sponsor", +]; +pub const MPV_FLAGS: [&str; 2] = ["--speed=2.7", "--volume=75"]; + +pub const CONCURRENT: u32 = 5; + +pub const DOWNLOAD_DIR: &str = "/tmp/ytcc"; + +const STATUS_PATH: &str = "ytcc/running"; + +pub fn status_path() -> anyhow::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) +} diff --git a/sys/nixpkgs/pkgs/ytc/src/downloader.rs b/sys/nixpkgs/pkgs/yt/src/downloader.rs index dddebe05..1733500a 100644 --- a/sys/nixpkgs/pkgs/ytc/src/downloader.rs +++ b/sys/nixpkgs/pkgs/yt/src/downloader.rs @@ -1,7 +1,8 @@ use std::{ - fs, + fs::{self, canonicalize}, io::{stderr, stdout, Read}, mem, + os::unix::fs::symlink, path::PathBuf, process::Command, sync::mpsc::{self, Receiver, Sender}, @@ -9,40 +10,28 @@ use std::{ }; use anyhow::{bail, Context, Result}; -use log::debug; +use log::{debug, warn}; +use url::Url; -use crate::PlayThing; +use crate::constants::{status_path, CONCURRENT, DOWNLOAD_DIR, MPV_FLAGS, YT_DLP_FLAGS}; -const YT_DLP_FLAGS: [&str; 12] = [ - "--format", - "bestvideo[height<=?1080]+bestaudio/best", - "--embed-chapters", - "--progress", - "--write-comments", - "--extractor-args", - "youtube:max_comments=150,all,100;comment_sort=top", - "--write-info-json", - "--sponsorblock-mark", - "default", - "--sponsorblock-remove", - "sponsor", -]; - -const CONCURRENT: u32 = 5; - -const DOWNLOAD_DIR: &str = "/tmp/ytcc"; +#[derive(Debug)] +pub struct Downloadable { + pub url: Url, + pub id: Option<u32>, +} pub struct Downloader { sent: usize, download_thread: JoinHandle<Result<()>>, orx: Receiver<(PathBuf, Option<u32>)>, - itx: Option<Sender<PlayThing>>, - playspec: Vec<PlayThing>, + itx: Option<Sender<Downloadable>>, + playspec: Vec<Downloadable>, } impl Downloader { - pub fn new(mut playspec: Vec<PlayThing>) -> anyhow::Result<Downloader> { - let (itx, irx): (Sender<PlayThing>, Receiver<PlayThing>) = mpsc::channel(); + pub fn new(mut playspec: Vec<Downloadable>) -> anyhow::Result<Downloader> { + let (itx, irx): (Sender<Downloadable>, Receiver<Downloadable>) = mpsc::channel(); let (otx, orx) = mpsc::channel(); let jh = thread::spawn(move || -> Result<()> { while let Some(pt) = irx.recv().ok() { @@ -91,7 +80,10 @@ impl Downloader { debug!("Will add 1"); self.add(1).ok()?; } else { - debug!("Will drop sender"); + debug!( + "Done sending videos to be downloaded, downoladed: {} videos", + self.sent + ); let itx = mem::take(&mut self.itx); drop(itx) } @@ -99,7 +91,7 @@ impl Downloader { Some(ok) } Err(err) => { - debug!("Recieved error while listening: {}", err); + debug!("Received error while listening: {}", err); None } } @@ -110,9 +102,63 @@ impl Downloader { Err(err) => panic!("Can't join thread: '{:#?}'", err), } } + + pub fn consume(mut self) -> anyhow::Result<()> { + while let Some((path, id)) = self.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 if !status_path()?.exists() { + debug!( + "The status path at '{}' does not exists", + status_path()?.display() + ); + } else { + bail!( + "The status path ('{}') is not a symlink but exists!", + status_path()?.display() + ); + } + + symlink(info_json, status_path()?).context("Failed to symlink")?; + + let mut mpv = Command::new("mpv"); + mpv.stdout(stdout()); + mpv.stderr(stderr()); + mpv.args(MPV_FLAGS); + mpv.arg(&path); + + let status = mpv.status().context("Failed to run mpv")?; + if status.success() { + fs::remove_file(&path)?; + if let Some(id) = id { + println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id); + let mut ytcc = std::process::Command::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: '{}'", status); + } else { + warn!("mpv exited with: '{}'", status); + } + } + self.drop()?; + Ok(()) + } } -fn download_url(url: &str) -> Result<PathBuf> { +fn download_url(url: &Url) -> Result<PathBuf> { let output_file = tempfile::NamedTempFile::new().context("Failed to create tempfile")?; output_file .as_file() @@ -130,17 +176,17 @@ fn download_url(url: &str) -> Result<PathBuf> { yt_dlp.args([ "--output", "%(channel)s/%(title)s.%(ext)s", - url, + url.as_str(), "--print-to-file", "after_move:filepath", ]); yt_dlp.arg(output_file.path().as_os_str()); + let status = yt_dlp.status().context("Failed to run yt-dlp")?; - if let Some(code) = status.code() { - if code != 0 { - bail!("yt_dlp execution failed with error: '{}'", status); - } + if !status.success() { + bail!("yt_dlp execution failed with error: '{}'", status); } + let mut path = String::new(); output_file .as_file() diff --git a/sys/nixpkgs/pkgs/yt/src/help.str b/sys/nixpkgs/pkgs/yt/src/help.str new file mode 100644 index 00000000..e5b21fce --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/help.str @@ -0,0 +1,7 @@ +# Commands: +# w, watch <id> = watch id +# d, drop <id> = mark id as watched +# p, pick <id> = leave id as is; This is a noop +# +# These lines can be re-ordered; they are executed from top to bottom. +# vim: filetype=yts conceallevel=2 concealcursor=nc colorcolumn= diff --git a/sys/nixpkgs/pkgs/yt/src/lib.rs b/sys/nixpkgs/pkgs/yt/src/lib.rs new file mode 100644 index 00000000..a08b32db --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/src/lib.rs @@ -0,0 +1,140 @@ +use anyhow::{bail, Context}; +use serde::Deserialize; +use url::Url; + +pub mod constants; +pub mod downloader; + +#[derive(Deserialize)] +pub struct YtccListData { + pub url: String, + pub title: String, + pub description: String, + pub publish_date: String, + pub watch_date: Option<String>, + pub duration: String, + pub thumbnail_url: String, + pub extractor_hash: String, + pub id: u32, + pub playlists: Vec<YtccPlaylistData>, +} +#[derive(Deserialize)] +pub struct YtccPlaylistData { + pub name: String, + pub url: String, + pub reverse: bool, +} + +pub enum LineCommand { + Pick, + Drop, + Watch, +} + +impl std::str::FromStr for LineCommand { + type Err = anyhow::Error; + fn from_str(v: &str) -> Result<Self, <Self as std::str::FromStr>::Err> { + match v { + "pick" | "p" => Ok(Self::Pick), + "drop" | "d" => Ok(Self::Drop), + "watch" | "w" => Ok(Self::Watch), + other => bail!("'{}' is not a recognized command!", other), + } + } +} + +pub struct Line { + pub cmd: LineCommand, + pub id: u32, + pub url: Url, +} + +/// We expect that each line is correctly formatted, and simply use default ones if they are not +impl From<&str> for Line { + fn from(v: &str) -> Self { + let buf: Vec<_> = v.split_whitespace().collect(); + let url: Url = Url::parse( + buf.last() + .expect("This should always exists") + .trim_matches('"'), + ) + .expect("This parsing should work,as the url is generated"); + + Line { + cmd: buf + .get(0) + .unwrap_or(&"pick") + .parse() + .unwrap_or(LineCommand::Pick), + id: buf.get(1).unwrap_or(&"0").parse().unwrap_or(0), + url, + } + } +} + +pub struct Duration { + time: u32, +} + +impl From<&str> for Duration { + fn from(v: &str) -> Self { + let buf: Vec<_> = v.split(':').take(2).collect(); + Self { + time: (buf[0] + .parse::<u32>() + .expect("Should be a number") + * 60) + + buf[1] + .parse::<u32>() + .expect("Should be a number"), + } + } +} + +impl std::fmt::Display for Duration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + const SECOND: u32 = 1; + const MINUTE: u32 = 60 * SECOND; + const HOUR: u32 = 60 * MINUTE; + + let base_hour = self.time - (self.time % HOUR); + let base_min = (self.time % HOUR) - ((self.time % HOUR) % MINUTE); + let base_sec = ((self.time % HOUR) % MINUTE) - (((self.time % HOUR) % MINUTE) % SECOND); + + let h = base_hour / HOUR; + let m = base_min / MINUTE; + let s = base_sec / SECOND; + + if self.time == 0 { + write!(f, "[No Duration]") + } else if h > 0 { + write!(f, "[{h}h {m}m]") + } else { + write!(f, "[{m}m {s}s]") + } + } +} +#[cfg(test)] +mod test { + use crate::Duration; + + #[test] + fn test_display_duration_1h() { + let dur = Duration { time: 60 * 60 }; + assert_eq!("[1h 0m]".to_owned(), dur.to_string()); + } + #[test] + fn test_display_duration_30min() { + let dur = Duration { time: 60 * 30 }; + assert_eq!("[30m 0s]".to_owned(), dur.to_string()); + } +} + +pub fn ytcc_drop(id: u32) -> anyhow::Result<()> { + let mut ytcc = std::process::Command::new("ytcc"); + ytcc.args(["mark", &format!("{}", id)]); + if !ytcc.status().context("Failed to run ytcc")?.success() { + bail!("`ytcc mark {}` failed to execute", id) + } + Ok(()) +} diff --git a/sys/nixpkgs/pkgs/yt/yt.nix b/sys/nixpkgs/pkgs/yt/yt.nix new file mode 100644 index 00000000..184b7820 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/yt.nix @@ -0,0 +1,27 @@ +{ + lib, + rustPlatform, + ytcc, + yt-dlp, + mpv, + makeWrapper, +}: +rustPlatform.buildRustPackage { + pname = "yt"; + version = "0.1.0"; + + src = ./.; + cargoHash = "sha256-AlrixMGbBa+VJZDzplQZwFV9Mp3STjCJUD6zV/JmUyA="; + + buildNoDefaultFeatures = true; + buildFeatures = ["yt"]; + + nativeBuildInputs = [ + makeWrapper + ]; + + postInstall = '' + wrapProgram $out/bin/yt \ + --set PATH ${lib.makeBinPath [mpv yt-dlp ytcc]} + ''; +} diff --git a/sys/nixpkgs/pkgs/ytc/package.nix b/sys/nixpkgs/pkgs/yt/ytc.nix index 5428ee18..8967ea35 100644 --- a/sys/nixpkgs/pkgs/ytc/package.nix +++ b/sys/nixpkgs/pkgs/yt/ytc.nix @@ -11,7 +11,10 @@ rustPlatform.buildRustPackage { version = "0.1.0"; src = ./.; - cargoHash = "sha256-KogHeuKKYhhpfSg+ImPCO4RwxWMOhSBXa3OjwCBZxEE="; + cargoHash = "sha256-X3bw6p09RttYR/6unu4rGawbmrBuDV8QNzaQlg1cBrs="; + + buildNoDefaultFeatures = true; + buildFeatures = ["ytc"]; nativeBuildInputs = [ makeWrapper diff --git a/sys/nixpkgs/pkgs/yt/yts.nix b/sys/nixpkgs/pkgs/yt/yts.nix new file mode 100644 index 00000000..fa00dc48 --- /dev/null +++ b/sys/nixpkgs/pkgs/yt/yts.nix @@ -0,0 +1,25 @@ +{ + lib, + rustPlatform, + ytcc, + makeWrapper, +}: +rustPlatform.buildRustPackage { + pname = "yts"; + version = "0.1.0"; + + src = ./.; + cargoHash = "sha256-vtf58M+qTEbgqSMFgaB67mhI4cEXtlQFMTbgmWFoxX4="; + + buildNoDefaultFeatures = true; + buildFeatures = ["yts"]; + + nativeBuildInputs = [ + makeWrapper + ]; + + postInstall = '' + wrapProgram $out/bin/yts \ + --set PATH ${lib.makeBinPath [ytcc]} + ''; +} diff --git a/sys/nixpkgs/pkgs/ytc/Cargo.toml b/sys/nixpkgs/pkgs/ytc/Cargo.toml deleted file mode 100644 index f958b9c6..00000000 --- a/sys/nixpkgs/pkgs/ytc/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "ytc" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = "1.0.79" -clap = { version = "4.4.13", features = ["derive"] } -cli-log = "2.0.0" -log = "0.4.20" -serde = { version = "1.0.194", features = ["derive"] } -serde_json = "1.0.111" -tempfile = "3.9.0" diff --git a/sys/nixpkgs/pkgs/ytc/default.nix b/sys/nixpkgs/pkgs/ytc/default.nix deleted file mode 100644 index d437b84a..00000000 --- a/sys/nixpkgs/pkgs/ytc/default.nix +++ /dev/null @@ -1,19 +0,0 @@ -[ - ( - final: prev: { - ytc = import ./package.nix { - inherit - (prev) - lib - makeWrapper - rustPlatform - # dependencies - - ytcc - yt-dlp - mpv - ; - }; - } - ) -] diff --git a/sys/nixpkgs/pkgs/ytc/src/main.rs b/sys/nixpkgs/pkgs/ytc/src/main.rs deleted file mode 100644 index 75084851..00000000 --- a/sys/nixpkgs/pkgs/ytc/src/main.rs +++ /dev/null @@ -1,193 +0,0 @@ -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 if !status_path()?.exists() { - debug!( - "The status path at '{}' does not exists", - status_path()?.display() - ); - } else { - bail!( - "The status path ('{}') is not a symlink but exists!", - 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 code == 0 { - fs::remove_file(&path)?; - if let Some(id) = id { - println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id); - 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) -} |