// yt - A fully featured command line YouTube client
//
// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// 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 <https://www.gnu.org/licenses/gpl-3.0.txt>.
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<Command>,
/// 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<PathBuf>,
/// 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<SelectCommand>,
},
/// 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<String>,
#[arg(short, long, default_value = "6")]
/// How many processes to spawn at the same time
concurrent_processes: usize,
},
/// Manipulate subscription
#[command(visible_alias = "subs")]
Subscriptions {
#[command(subcommand)]
cmd: SubscriptionCommand,
},
}
fn byte_parser(s: &str) -> Result<u64, Error> {
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::<String>();
let number: u64 = s
.chars()
.take_while(|x| x.is_numeric())
.collect::<String>()
.parse()?;
let extension = s.chars().skip_while(|x| x.is_numeric()).collect::<String>();
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<String>,
/// 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<PathBuf>,
/// Remove any previous subscriptions
#[arg(short, long)]
force: bool,
},
/// List all subscriptions
List {
/// Only show the URLs
#[arg(short, long)]
url: bool,
},
}
#[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<i64>,
/// 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,
},
}