// yt - A fully featured command line YouTube client // // Copyright (C) 2024 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Yt. // // You should have received a copy of the License along with this program. // If not, see . use std::path::PathBuf; use anyhow::{bail, Error}; use chrono::NaiveDate; use clap::{ArgAction, Args, Parser, Subcommand}; use url::Url; use crate::{ constants, select::selection_file::duration::Duration, storage::video_database::extractor_hash::LazyExtractorHash, }; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] /// An command line interface to select, download and watch videos pub struct CliArgs { #[command(subcommand)] /// The subcommand to execute [default: select] pub command: Option, /// Increase message verbosity #[arg(long="verbose", short = 'v', action = ArgAction::Count)] pub verbosity: u8, /// Set the path to the videos.db. Otherwise use the default location #[arg(long, short)] pub db_path: Option, /// Silence all output #[arg(long, short = 'q')] pub quiet: bool, } #[derive(Subcommand, Debug)] pub enum Command { /// Download and cache URLs Download { /// Forcefully re-download all cached videos (i.e. delete the cache path, then download). #[arg(short, long)] force: bool, /// The maximum size the download dir should have. Beware that the value must be given in /// bytes. #[arg(short, long, default_value = "3 GiB", value_parser = byte_parser)] max_cache_size: u64, }, /// Watch the already cached (and selected) videos Watch {}, /// Show, which videos have been selected to be watched (and their cache status) Status {}, /// Perform various tests Check { #[command(subcommand)] command: CheckCommand, }, /// Display the comments of the currently playing video Comments {}, /// Display the description of the currently playing video Description {}, /// Manipulate the video cache in the database #[command(visible_alias = "db")] Database { #[command(subcommand)] command: CacheCommand, }, /// Change the state of videos in the database (the default) Select { #[command(subcommand)] cmd: Option, }, /// Update the video database Update { #[arg(short, long, default_value = "20")] /// The number of videos to updating max_backlog: u32, #[arg(short, long)] /// The subscriptions to update (can be given multiple times) subscriptions: Vec, }, /// Manipulate subscription #[command(visible_alias = "subs")] Subscriptions { #[command(subcommand)] cmd: SubscriptionCommand, }, } fn byte_parser(s: &str) -> Result { const B: u64 = 1; const KIB: u64 = 1024 * B; const MIB: u64 = 1024 * KIB; const GIB: u64 = 1024 * MIB; const KB: u64 = 1000 * B; const MB: u64 = 1000 * KB; const GB: u64 = 1000 * MB; let s = s .chars() .filter(|elem| !elem.is_whitespace()) .collect::(); let number: u64 = s .chars() .take_while(|x| x.is_numeric()) .collect::() .parse()?; let extension = s.chars().skip_while(|x| x.is_numeric()).collect::(); let output = match extension.to_lowercase().as_str() { "" => number, "b" => number * B, "kib" => number * KIB, "mib" => number * MIB, "gib" => number * GIB, "kb" => number * KB, "mb" => number * MB, "gb" => number * GB, other => bail!( "Your extension '{}' is not yet supported. Only KB,MB,GB or KiB,MiB,GiB are supported", other ), }; Ok(output) } impl Default for Command { fn default() -> Self { Self::Select { cmd: Some(SelectCommand::default()), } } } #[derive(Subcommand, Clone, Debug)] pub enum SubscriptionCommand { /// Subscribe to an URL Add { #[arg(short, long)] /// The human readable name of the subscription name: Option, /// The URL to listen to url: Url, }, /// Unsubscribe from an URL Remove { /// The human readable name of the subscription name: String, }, /// Import a bunch of URLs as subscriptions. Import { /// The file containing the URLs. Will use Stdin otherwise. file: Option, /// Remove any previous subscriptions #[arg(short, long)] force: bool, }, /// Write all subscriptions in an format understood by `import` Export {}, /// List all subscriptions List {}, } #[derive(Clone, Debug, Args)] #[command(infer_subcommands = true)] /// Mark the video given by the hash to be watched pub struct SharedSelectionCommandArgs { /// The ordering priority (higher means more at the top) #[arg(short, long)] pub priority: Option, /// The short extractor hash pub hash: LazyExtractorHash, pub title: String, pub date: NaiveDate, pub publisher: String, pub duration: Duration, pub url: Url, } #[derive(Subcommand, Clone, Debug)] #[command(infer_subcommands = true)] // NOTE: Keep this in sync with the [`constants::HELP_STR`] constant. <2024-08-20> pub enum SelectCommand { /// Open a `git rebase` like file to select the videos to watch (the default) File { /// Include done (watched, dropped) videos #[arg(long, short)] done: bool, }, Watch { #[command(flatten)] shared: SharedSelectionCommandArgs, /// The subtitles to download (e.g. 'en,de,sv') #[arg(short = 'l', long, default_value = constants::DEFAULT_SUBTITLE_LANGS)] subtitle_langs: String, /// The speed to set mpv to // NOTE: KEEP THIS IN SYNC WITH THE `DEFAULT_MPV_PLAYBACK_SPEED` in `constants.rs` <2024-08-20> #[arg(short, long, default_value = "2.7")] speed: f64, }, /// Mark the video given by the hash to be dropped Drop { #[command(flatten)] shared: SharedSelectionCommandArgs, }, /// Open the video URL in Firefox's `timesinks.youtube` profile Url { #[command(flatten)] shared: SharedSelectionCommandArgs, }, /// Reset the videos status to 'Pick' Pick { #[command(flatten)] shared: SharedSelectionCommandArgs, }, } impl Default for SelectCommand { fn default() -> Self { Self::File { done: false } } } #[derive(Subcommand, Clone, Debug)] pub enum CheckCommand { /// Check if the given info.json is deserializable InfoJson { path: PathBuf }, /// Check if the given update info.json is deserializable UpdateInfoJson { path: PathBuf }, } #[derive(Subcommand, Clone, Copy, Debug)] pub enum CacheCommand { /// Invalidate all cache entries Invalidate { /// Also delete the cache path #[arg(short, long)] hard: bool, }, /// Perform basic maintenance operations on the database. /// This helps recovering from invalid db states after a crash (or force exit via CTRL+C). /// /// 1. Check every path for validity (removing all invalid cache entries) /// 2. Reset all `status_change` bits of videos to false. #[command(verbatim_doc_comment)] Maintain { /// Check every video (otherwise only the videos to be watched are checked) #[arg(short, long)] all: bool, }, }