// 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::Context;
use bytes::Bytes;
use chrono::NaiveDate;
use clap::{ArgAction, Args, Parser, Subcommand};
use url::Url;
use crate::{
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. This overrides the default and the config file.
#[arg(long, short)]
pub db_path: Option<PathBuf>,
/// Set the path to the config.toml.
/// This overrides the default.
#[arg(long, short)]
pub config_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, value_parser = byte_parser)]
max_cache_size: Option<u64>,
},
/// Work with single videos
Videos {
#[command(subcommand)]
cmd: VideosCommand,
},
/// Watch the already cached (and selected) videos
Watch {},
/// Show, which videos have been selected to be watched (and their cache status)
Status {},
/// Show, the configuration options in effect
Config {},
/// 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)]
/// The number of videos to updating
max_backlog: Option<u32>,
#[arg(short, long)]
/// The subscriptions to update (can be given multiple times)
subscriptions: Vec<String>,
},
/// Manipulate subscription
#[command(visible_alias = "subs")]
Subscriptions {
#[command(subcommand)]
cmd: SubscriptionCommand,
},
}
fn byte_parser(input: &str) -> Result<u64, anyhow::Error> {
Ok(input
.parse::<Bytes>()
.with_context(|| format!("Failed to parse '{}' as bytes!", input))?
.as_u64())
}
impl Default for Command {
fn default() -> Self {
Self::Select {
cmd: Some(SelectCommand::default()),
}
}
}
#[derive(Subcommand, Clone, Debug)]
pub enum VideosCommand {
/// List the videos in the database
#[command(visible_alias = "ls")]
List {
/// An optional search query to limit the results
#[arg(action = ArgAction::Append)]
search_query: Option<String>,
/// The number of videos to show
#[arg(short, long)]
limit: Option<usize>,
},
/// Get detailed information about a video
Info {
/// The short hash of the video
hash: LazyExtractorHash,
},
}
#[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,
},
/// 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<i64>,
/// The subtitles to download (e.g. 'en,de,sv')
#[arg(short = 'l', long)]
pub subtitle_langs: Option<String>,
/// The speed to set mpv to
#[arg(short, long)]
pub speed: Option<f64>,
/// 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)]
// 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,
/// Use the last selection file (useful if you've spend time on it and want to get it again)
#[arg(long, short, conflicts_with = "done")]
use_last_selection: bool,
},
/// Mark the video given by the hash to be watched
#[command(visible_alias = "w")]
Watch {
#[command(flatten)]
shared: SharedSelectionCommandArgs,
},
/// Mark the video given by the hash to be dropped
#[command(visible_alias = "d")]
Drop {
#[command(flatten)]
shared: SharedSelectionCommandArgs,
},
/// Mark the video given by the hash as already watched
#[command(visible_alias = "wd")]
Watched {
#[command(flatten)]
shared: SharedSelectionCommandArgs,
},
/// Open the video URL in Firefox's `timesinks.youtube` profile
#[command(visible_alias = "u")]
Url {
#[command(flatten)]
shared: SharedSelectionCommandArgs,
},
/// Reset the videos status to 'Pick'
#[command(visible_alias = "p")]
Pick {
#[command(flatten)]
shared: SharedSelectionCommandArgs,
},
}
impl Default for SelectCommand {
fn default() -> Self {
Self::File {
done: false,
use_last_selection: 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,
},
}