diff options
Diffstat (limited to 'src/videos')
-rw-r--r-- | src/videos/display/format_video.rs | 166 | ||||
-rw-r--r-- | src/videos/display/mod.rs | 314 | ||||
-rw-r--r-- | src/videos/mod.rs | 14 |
3 files changed, 490 insertions, 4 deletions
diff --git a/src/videos/display/format_video.rs b/src/videos/display/format_video.rs new file mode 100644 index 0000000..50646a1 --- /dev/null +++ b/src/videos/display/format_video.rs @@ -0,0 +1,166 @@ +// 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::fmt::Display; + +pub trait FormatVideo { + type Output; + + fn cache_path(&self) -> Self::Output; + fn description(&self) -> Self::Output; + fn duration(&self) -> Self::Output; + fn extractor_hash(&self) -> Self::Output; + fn last_status_change(&self) -> Self::Output; + fn parent_subscription_name(&self) -> Self::Output; + fn priority(&self) -> Self::Output; + fn publish_date(&self) -> Self::Output; + fn status(&self) -> Self::Output; + fn status_change(&self) -> Self::Output; + fn thumbnail_url(&self) -> Self::Output; + fn title(&self) -> Self::Output; + fn url(&self) -> Self::Output; + fn video_options(&self) -> Self::Output; + + fn to_parts( + &self, + ) -> ( + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + Self::Output, + ) { + let cache_path = self.cache_path(); + let description = self.description(); + let duration = self.duration(); + let extractor_hash = self.extractor_hash(); + let last_status_change = self.last_status_change(); + let parent_subscription_name = self.parent_subscription_name(); + let priority = self.priority(); + let publish_date = self.publish_date(); + let status = self.status(); + let status_change = self.status_change(); + let thumbnail_url = self.thumbnail_url(); + let title = self.title(); + let url = self.url(); + let video_options = self.video_options(); + + ( + cache_path, + description, + duration, + extractor_hash, + last_status_change, + parent_subscription_name, + priority, + publish_date, + status, + status_change, + thumbnail_url, + title, + url, + video_options, + ) + } + + fn to_info_display(&self) -> String + where + <Self as FormatVideo>::Output: Display, + { + let ( + 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.to_parts(); + + let status_change = if status_change.to_string().as_str() == "false" { + "currently not changing" + } else if status_change.to_string().as_str() == "true" { + "currently changing" + } else { + unreachable!("This is an formatted boolean"); + }; + + let string = format!( + "\ +{title} ({extractor_hash}) +| -> {cache_path} +| -> {duration} +| -> {parent_subscription_name} +| -> priority: {priority} +| -> {publish_date} +| -> status: {status} since {last_status_change} +| -> {status_change} +| -> {thumbnail_url} +| -> {url} +| -> options: {} +{description}\n", + video_options.to_string().trim() + ); + string + } + + fn to_line_display(&self) -> String + where + Self::Output: Display, + { + let f = format!( + "{} {} {} {} {} {}", + self.status(), + self.extractor_hash(), + self.title(), + self.publish_date(), + self.parent_subscription_name(), + self.duration() + ); + + f + } + + fn to_select_file_display(&self) -> String + where + Self::Output: Display, + { + let f = format!( + r#"{}{} {} "{}" "{}" "{}" "{}" "{}"{}"#, + self.status(), + self.video_options(), + self.extractor_hash(), + self.title(), + self.publish_date(), + self.parent_subscription_name(), + self.duration(), + self.url(), + '\n' + ); + + f + } +} 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 + } +} diff --git a/src/videos/mod.rs b/src/videos/mod.rs index 3876bd1..59baa8c 100644 --- a/src/videos/mod.rs +++ b/src/videos/mod.rs @@ -9,12 +9,15 @@ // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. use anyhow::Result; +use display::{format_video::FormatVideo, FormattedVideo}; use futures::{stream::FuturesUnordered, TryStreamExt}; use nucleo_matcher::{ pattern::{CaseMatching, Normalization, Pattern}, Matcher, }; +pub mod display; + use crate::{ app::App, storage::video_database::{getters::get_videos, VideoStatus}, @@ -25,7 +28,7 @@ pub async fn query(app: &App, limit: Option<usize>, search_query: Option<String> // turn one video to a color display, to pre-warm the hash shrinking cache if let Some(val) = all_videos.first() { - val.to_color_display(app).await?; + val.to_formatted_video(app).await?; } let limit = limit.unwrap_or(all_videos.len()); @@ -33,10 +36,13 @@ pub async fn query(app: &App, limit: Option<usize>, search_query: Option<String> let all_video_strings: Vec<String> = all_videos .into_iter() .take(limit) - .map(|vid| vid.to_color_display_owned(app)) + .map(|vid| vid.to_formatted_video_owned(app)) .collect::<FuturesUnordered<_>>() - .try_collect() - .await?; + .try_collect::<Vec<FormattedVideo>>() + .await? + .into_iter() + .map(|vid| (&vid.colorize()).to_line_display()) + .collect(); if let Some(query) = search_query { let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT.match_paths()); |