about summary refs log tree commit diff stats
path: root/src/videos/display/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/videos/display/mod.rs')
-rw-r--r--src/videos/display/mod.rs314
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
+    }
+}