diff options
Diffstat (limited to 'src/videos/display/mod.rs')
-rw-r--r-- | src/videos/display/mod.rs | 314 |
1 files changed, 314 insertions, 0 deletions
diff --git a/src/videos/display/mod.rs b/src/videos/display/mod.rs new file mode 100644 index 0000000..d919dd2 --- /dev/null +++ b/src/videos/display/mod.rs @@ -0,0 +1,314 @@ +// 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 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<FormattedVideo> { + Self::to_formatted_video(&self, app).await + } + + pub async fn to_formatted_video(&self, app: &App) -> Result<FormattedVideo> { + 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 + } +} |