// 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::{collections::HashMap, fs, sync::Arc}; use anyhow::{bail, Context, Result}; use app::App; use bytes::Bytes; use cache::invalidate; use clap::Parser; use cli::{CacheCommand, CheckCommand, SelectCommand, SubscriptionCommand, VideosCommand}; use config::Config; use log::info; use select::cmds::handle_select_cmd; use storage::video_database::getters::get_video_by_hash; use tokio::{ fs::File, io::{stdin, BufReader}, task::JoinHandle, }; use url::Url; use yt_dlp::wrapper::info_json::InfoJson; use crate::{cli::Command, storage::subscriptions::get_subscriptions}; pub mod app; pub mod cli; pub mod cache; pub mod comments; pub mod config; pub mod constants; pub mod download; pub mod select; pub mod status; pub mod storage; pub mod subscribe; pub mod update; pub mod videos; pub mod watch; #[tokio::main] async fn main() -> Result<()> { let args = cli::CliArgs::parse(); // The default verbosity is 1 (Warn) let verbosity: u8 = args.verbosity + 1; stderrlog::new() .module(module_path!()) .modules(&["yt_dlp".to_owned(), "libmpv2".to_owned()]) .quiet(args.quiet) .show_module_names(false) .color(stderrlog::ColorChoice::Auto) .verbosity(verbosity as usize) .timestamp(stderrlog::Timestamp::Off) .init() .expect("Let's just hope that this does not panic"); info!("Using verbosity level: '{} ({})'", verbosity, { match verbosity { 0 => "Error", 1 => "Warn", 2 => "Info", 3 => "Debug", 4.. => "Trace", } }); let app = { let config = Config::from_config_file(args.db_path, args.config_path)?; App::new(config).await? }; match args.command.unwrap_or(Command::default()) { Command::Download { force, max_cache_size, } => { let max_cache_size = max_cache_size.unwrap_or(app.config.download.max_cache_size.as_u64()); info!("Max cache size: '{}'", Bytes::new(max_cache_size)); if force { invalidate(&app, true).await?; } download::Downloader::new() .consume(Arc::new(app), max_cache_size) .await?; } Command::Select { cmd } => { let cmd = cmd.unwrap_or(SelectCommand::default()); match cmd { SelectCommand::File { done, use_last_selection, } => select::select(&app, done, use_last_selection).await?, _ => handle_select_cmd(&app, cmd, None).await?, } } Command::Sedowa {} => { select::select(&app, false, false).await?; let max_cache_size = app.config.download.max_cache_size; info!("Max cache size: '{}'", max_cache_size); let arc_app = Arc::new(app); let arc_app_clone = Arc::clone(&arc_app); let download: JoinHandle> = tokio::spawn(async move { download::Downloader::new() .consume(arc_app_clone, max_cache_size.as_u64()) .await?; Ok(()) }); watch::watch(&arc_app).await?; download.await??; } Command::Videos { cmd } => match cmd { VideosCommand::List { search_query, limit, } => { videos::query(&app, limit, search_query) .await .context("Failed to query videos")?; } VideosCommand::Info { hash } => { let video = get_video_by_hash(&app, &hash.realize(&app).await?).await?; dbg!(video); } }, Command::Update { max_backlog, subscriptions, } => { let all_subs = get_subscriptions(&app).await?; for sub in &subscriptions { if !all_subs.0.contains_key(sub) { bail!( "Your specified subscription to update '{}' is not a subscription!", sub ) } } let max_backlog = max_backlog.unwrap_or(app.config.update.max_backlog); update::update(&app, max_backlog, subscriptions, verbosity).await?; } Command::Subscriptions { cmd } => match cmd { SubscriptionCommand::Add { name, url } => { subscribe::subscribe(&app, name, url) .await .context("Failed to add a subscription")?; } SubscriptionCommand::Remove { name } => { subscribe::unsubscribe(&app, name) .await .context("Failed to remove a subscription")?; } SubscriptionCommand::List {} => { let all_subs = get_subscriptions(&app).await?; for (key, val) in all_subs.0 { println!("{}: '{}'", key, val.url); } } SubscriptionCommand::Export {} => { let all_subs = get_subscriptions(&app).await?; for val in all_subs.0.values() { println!("{}", val.url); } } SubscriptionCommand::Import { file, force } => { if let Some(file) = file { let f = File::open(file).await?; subscribe::import(&app, BufReader::new(f), force).await? } else { subscribe::import(&app, BufReader::new(stdin()), force).await? }; } }, Command::Watch {} => watch::watch(&app).await?, Command::Status {} => status::show(&app).await?, Command::Config {} => status::config(&app)?, Command::Database { command } => match command { CacheCommand::Invalidate { hard } => cache::invalidate(&app, hard).await?, CacheCommand::Maintain { all } => cache::maintain(&app, all).await?, }, Command::Check { command } => match command { CheckCommand::InfoJson { path } => { let string = fs::read_to_string(&path) .with_context(|| format!("Failed to read '{}' to string!", path.display()))?; let _: InfoJson = serde_json::from_str(&string).context("Failed to deserialize value")?; } CheckCommand::UpdateInfoJson { path } => { let string = fs::read_to_string(&path) .with_context(|| format!("Failed to read '{}' to string!", path.display()))?; let _: HashMap = serde_json::from_str(&string).context("Failed to deserialize value")?; } }, Command::Comments {} => { comments::comments(&app).await?; } Command::Description {} => { todo!() // description::description(&app).await?; } } Ok(()) }