// 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::{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<Result<()>> = 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<Url, InfoJson> =
serde_json::from_str(&string).context("Failed to deserialize value")?;
}
},
Command::Comments {} => {
comments::comments(&app).await?;
}
Command::Description {} => {
todo!()
// description::description(&app).await?;
}
}
Ok(())
}