// 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 chrono::DateTime; use format_video::FormatVideo; use owo_colors::OwoColorize; use url::Url; use crate::{ app::App, select::selection_file::duration::Duration, storage::video_database::{getters::get_video_opts, Video}, }; use anyhow::{Context, Result}; pub mod format_video; macro_rules! get { ($value:expr, $key:ident, $name:expr, $code:tt) => { if let Some(value) = &$value.$key { $code(value) } else { concat!("[No ", $name, "]").to_owned() } }; } /// This is identical to a [`FormattedVideo`], but has colorized fields. pub struct ColorizedFormattedVideo(FormattedVideo); impl FormattedVideo { pub fn colorize(self) -> ColorizedFormattedVideo { let Self { cache_path, description, duration, extractor_hash, last_status_change, parent_subscription_name, priority, publish_date, status, status_change, thumbnail_url, title, url, video_options, } = self; ColorizedFormattedVideo(Self { cache_path: cache_path.blue().bold().to_string(), description, duration: duration.cyan().bold().to_string(), extractor_hash: extractor_hash.bright_purple().italic().to_string(), last_status_change: last_status_change.bright_cyan().to_string(), parent_subscription_name: parent_subscription_name.bright_magenta().to_string(), priority, publish_date: publish_date.bright_white().bold().to_string(), status: status.red().bold().to_string(), status_change, thumbnail_url, title: title.green().bold().to_string(), url: url.italic().to_string(), video_options: video_options.bright_green().to_string(), }) } } /// This is a version of [`Video`] that has all the fields of the original [`Video`] structure /// turned to [`String`]s to facilitate displaying it. /// /// This structure provides a way to display a [`Video`] in a coherent way, as it enforces to /// always use the same colors for one field. #[derive(Debug)] pub struct FormattedVideo { cache_path: String, description: String, duration: String, extractor_hash: String, last_status_change: String, parent_subscription_name: String, priority: String, publish_date: String, status: String, status_change: String, thumbnail_url: String, title: String, url: String, /// This string contains the video options (speed, subtitle_languages, etc.). /// It already starts with an extra whitespace, when these are not empty. video_options: String, } impl Video { pub async fn to_formatted_video_owned(self, app: &App) -> Result { Self::to_formatted_video(&self, app).await } pub async fn to_formatted_video(&self, app: &App) -> Result { fn date_from_stamp(stamp: i64) -> String { DateTime::from_timestamp(stamp, 0) .expect("The timestamps should always be valid") .format("%Y-%m-%d") .to_string() } let cache_path: String = get!( self, cache_path, "Cache Path", (|value: &PathBuf| value.to_string_lossy().to_string()) ); let description = get!( self, description, "Description", (|value: &str| value.to_owned()) ); let duration = Duration::from(self.duration); let extractor_hash = self .extractor_hash .into_short_hash(app) .await .with_context(|| { format!( "Failed to format extractor hash, whilst formatting video: '{}'", self.title ) })?; let last_status_change = date_from_stamp(self.last_status_change); let parent_subscription_name = get!( self, parent_subscription_name, "author", (|sub: &str| sub.replace('"', "'")) ); let priority = self.priority; let publish_date = get!( self, publish_date, "release date", (|date: &i64| date_from_stamp(*date)) ); // TODO: We might support `.trim()`ing that, as the extra whitespace could be bad in the // selection file. <2024-10-07> let status = self.status.as_command(); let status_change = self.status_change; let thumbnail_url = get!( self, thumbnail_url, "thumbnail URL", (|url: &Url| url.to_string()) ); let title = self.title.replace(['"', '„', '”'], "'"); let url = self.url.as_str().replace('"', "\\\""); let video_options = { let opts = get_video_opts(app, &self.extractor_hash) .await .with_context(|| { format!("Failed to get video options for video: '{}'", self.title) })? .to_cli_flags(app); let opts_white = if !opts.is_empty() { " " } else { "" }; format!("{}{}", opts_white, opts) }; Ok(FormattedVideo { cache_path, description, duration: duration.to_string(), extractor_hash: extractor_hash.to_string(), last_status_change, parent_subscription_name, priority: priority.to_string(), publish_date, status: status.to_string(), status_change: status_change.to_string(), thumbnail_url, title, url, video_options, }) } } impl<'a> FormatVideo for &'a FormattedVideo { type Output = &'a str; fn cache_path(&self) -> Self::Output { &self.cache_path } fn description(&self) -> Self::Output { &self.description } fn duration(&self) -> Self::Output { &self.duration } fn extractor_hash(&self) -> Self::Output { &self.extractor_hash } fn last_status_change(&self) -> Self::Output { &self.last_status_change } fn parent_subscription_name(&self) -> Self::Output { &self.parent_subscription_name } fn priority(&self) -> Self::Output { &self.priority } fn publish_date(&self) -> Self::Output { &self.publish_date } fn status(&self) -> Self::Output { &self.status } fn status_change(&self) -> Self::Output { &self.status_change } fn thumbnail_url(&self) -> Self::Output { &self.thumbnail_url } fn title(&self) -> Self::Output { &self.title } fn url(&self) -> Self::Output { &self.url } fn video_options(&self) -> Self::Output { &self.video_options } } impl<'a> FormatVideo for &'a ColorizedFormattedVideo { type Output = &'a str; fn cache_path(&self) -> Self::Output { &self.0.cache_path } fn description(&self) -> Self::Output { &self.0.description } fn duration(&self) -> Self::Output { &self.0.duration } fn extractor_hash(&self) -> Self::Output { &self.0.extractor_hash } fn last_status_change(&self) -> Self::Output { &self.0.last_status_change } fn parent_subscription_name(&self) -> Self::Output { &self.0.parent_subscription_name } fn priority(&self) -> Self::Output { &self.0.priority } fn publish_date(&self) -> Self::Output { &self.0.publish_date } fn status(&self) -> Self::Output { &self.0.status } fn status_change(&self) -> Self::Output { &self.0.status_change } fn thumbnail_url(&self) -> Self::Output { &self.0.thumbnail_url } fn title(&self) -> Self::Output { &self.0.title } fn url(&self) -> Self::Output { &self.0.url } fn video_options(&self) -> Self::Output { &self.0.video_options } }