aboutsummaryrefslogtreecommitdiffstats
path: root/src/videos
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-10-14 12:32:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-10-14 12:34:56 +0200
commit145a776039248a9460e9473e4bc9ef3d533b60c1 (patch)
tree7b2a948ae1f08335eba477c26bf1d5e83cdac24b /src/videos
parentfix(downloader): Don't display changed cache size on first run (diff)
downloadyt-145a776039248a9460e9473e4bc9ef3d533b60c1.zip
feat(videos): Provide a consistent display for the `Video` struct
Before, `Video`s where colourized differently, just because the colourization was not standardized. It now is.
Diffstat (limited to 'src/videos')
-rw-r--r--src/videos/display/format_video.rs166
-rw-r--r--src/videos/display/mod.rs314
-rw-r--r--src/videos/mod.rs14
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());