about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-02-17 19:34:33 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-02-17 19:34:33 +0100
commit27fae0bcd380fdf7396c33678f4aa3fa2df192cf (patch)
tree70784d5b1aa231997faaf50773363bd38bdd1d23
parentfix(yt): Remove most of the references to the zero version `Video` struct (diff)
downloadyt-27fae0bcd380fdf7396c33678f4aa3fa2df192cf.zip
refactor(yt/videos/display): Streamline video formatting
The previous approach with a trait and two newtype wrappers was just too
complicated and really not worth it. Simply implementing the functions
directly makes the code more readable and simplifies the implementation.
Diffstat (limited to '')
-rw-r--r--yt/src/main.rs7
-rw-r--r--yt/src/select/cmds/add.rs6
-rw-r--r--yt/src/select/mod.rs42
-rw-r--r--yt/src/update/mod.rs7
-rw-r--r--yt/src/videos/display/format_video.rs175
-rw-r--r--yt/src/videos/display/mod.rs375
-rw-r--r--yt/src/videos/mod.rs18
7 files changed, 222 insertions, 408 deletions
diff --git a/yt/src/main.rs b/yt/src/main.rs
index acc6c7d..0178488 100644
--- a/yt/src/main.rs
+++ b/yt/src/main.rs
@@ -30,7 +30,6 @@ use tokio::{
     task::JoinHandle,
 };
 use url::Url;
-use videos::display::format_video::FormatVideo;
 use yt_dlp::wrapper::info_json::InfoJson;
 
 use crate::{cli::Command, storage::subscriptions};
@@ -147,12 +146,10 @@ async fn main() -> Result<()> {
 
                 print!(
                     "{}",
-                    (&video
-                        .to_formatted_video(&app)
+                    &video
+                        .to_info_display(&app)
                         .await
                         .context("Failed to format video")?
-                        .colorize(&app))
-                        .to_info_display()
                 );
             }
         },
diff --git a/yt/src/select/cmds/add.rs b/yt/src/select/cmds/add.rs
index 89d50c0..1e14995 100644
--- a/yt/src/select/cmds/add.rs
+++ b/yt/src/select/cmds/add.rs
@@ -6,7 +6,6 @@ use crate::{
     },
     unreachable::Unreachable,
     update::video_entry_to_video,
-    videos::display::format_video::FormatVideo,
 };
 
 use anyhow::{Context, Result, bail};
@@ -76,10 +75,7 @@ pub(super) async fn add(
             let video = video_entry_to_video(entry, None)?;
             add_video(app, video.clone()).await?;
 
-            println!(
-                "{}",
-                (&video.to_formatted_video(app).await?.colorize(app)).to_line_display()
-            );
+            println!("{}", &video.to_line_display(app).await?);
 
             Ok(())
         }
diff --git a/yt/src/select/mod.rs b/yt/src/select/mod.rs
index e7eb460..34262af 100644
--- a/yt/src/select/mod.rs
+++ b/yt/src/select/mod.rs
@@ -19,15 +19,14 @@ use crate::{
     app::App,
     cli::CliArgs,
     constants::HELP_STR,
-    storage::video_database::{VideoStatus, getters::get_videos},
+    storage::video_database::{Video, VideoStatus, getters::get_videos},
     unreachable::Unreachable,
-    videos::display::format_video::FormatVideo,
 };
 
 use anyhow::{Context, Result, bail};
 use clap::Parser;
 use cmds::handle_select_cmd;
-use futures::future::join_all;
+use futures::{TryStreamExt, stream::FuturesOrdered};
 use selection_file::process_line;
 use tempfile::Builder;
 use tokio::process::Command;
@@ -35,6 +34,10 @@ use tokio::process::Command;
 pub mod cmds;
 pub mod selection_file;
 
+async fn to_select_file_display_owned(video: Video, app: &App) -> Result<String> {
+    (&video).to_select_file_display(&app).await
+}
+
 pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<()> {
     let temp_file = Builder::new()
         .prefix("yt_video_select-")
@@ -65,28 +68,25 @@ pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<(
         // Warmup the cache for the display rendering of the videos.
         // Otherwise the futures would all try to warm it up at the same time.
         if let Some(vid) = matching_videos.first() {
-            drop(vid.to_formatted_video(app).await?);
+            drop(vid.to_line_display(app).await?);
         }
 
         let mut edit_file = BufWriter::new(&temp_file);
 
-        join_all(
-            matching_videos
-                .into_iter()
-                .map(|vid| async { vid.to_formatted_video_owned(app).await })
-                .collect::<Vec<_>>(),
-        )
-        .await
-        .into_iter()
-        .try_for_each(|line| -> Result<()> {
-            let formatted_line = (&line?).to_select_file_display();
-
-            edit_file
-                .write_all(formatted_line.as_bytes())
-                .context("Failed to write to `edit_file`")?;
+        matching_videos
+            .into_iter()
+            .map(|vid| to_select_file_display_owned(vid, app))
+            .collect::<FuturesOrdered<_>>()
+            .try_collect::<Vec<String>>()
+            .await?
+            .into_iter()
+            .try_for_each(|line| -> Result<()> {
+                edit_file
+                    .write_all(line.as_bytes())
+                    .context("Failed to write to `edit_file`")?;
 
-            Ok(())
-        })?;
+                Ok(())
+            })?;
 
         edit_file.write_all(HELP_STR.as_bytes())?;
         edit_file.flush().context("Failed to flush edit file")?;
@@ -153,7 +153,7 @@ pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<(
 }
 
 // // FIXME: There should be no reason why we need to re-run yt, just to get the help string. But I've
-// // yet to find a way to do it with out the extra exec <2024-08-20>
+// // yet to find a way to do it without the extra exec <2024-08-20>
 // async fn get_help() -> Result<String> {
 //     let binary_name = current_exe()?;
 //     let cmd = Command::new(binary_name)
diff --git a/yt/src/update/mod.rs b/yt/src/update/mod.rs
index 7bd37b6..da19bae 100644
--- a/yt/src/update/mod.rs
+++ b/yt/src/update/mod.rs
@@ -25,7 +25,6 @@ use crate::{
             setters::add_video,
         },
     },
-    videos::display::format_video::FormatVideo,
 };
 
 mod updater;
@@ -188,12 +187,10 @@ async fn process_subscription(app: &App, sub: &Subscription, entry: InfoJson) ->
         .with_context(|| format!("Failed to add video to database: '{}'", video.title))?;
     println!(
         "{}",
-        (&video
-            .to_formatted_video(app)
+        &video
+            .to_line_display(app)
             .await
             .with_context(|| format!("Failed to format video: '{}'", video.title))?
-            .colorize(app))
-            .to_line_display()
     );
     Ok(())
 }
diff --git a/yt/src/videos/display/format_video.rs b/yt/src/videos/display/format_video.rs
index 26f0f5b..f9c50af 100644
--- a/yt/src/videos/display/format_video.rs
+++ b/yt/src/videos/display/format_video.rs
@@ -8,119 +8,48 @@
 // 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;
+use anyhow::Result;
 
-use crate::comments::output::format_text;
+use crate::{app::App, comments::output::format_text, storage::video_database::Video};
 
-pub trait FormatVideo {
-    type Output;
+impl Video {
+    pub async fn to_info_display(&self, app: &App) -> Result<String> {
+        let cache_path = self.cache_path_fmt(app);
+        let description = self.description_fmt();
+        let duration = self.duration_fmt(app);
+        let extractor_hash = self.extractor_hash_fmt(app).await?;
+        let in_playlist = self.in_playlist_fmt(app);
+        let last_status_change = self.last_status_change_fmt(app);
+        let parent_subscription_name = self.parent_subscription_name_fmt(app);
+        let priority = self.priority_fmt();
+        let publish_date = self.publish_date_fmt(app);
+        let status = self.status_fmt(app);
+        let thumbnail_url = self.thumbnail_url_fmt();
+        let title = self.title_fmt(app);
+        let url = self.url_fmt(app);
+        let watch_progress = self.watch_progress_fmt(app);
+        let video_options = self.video_options_fmt(app).await?;
 
-    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;
-
-    #[allow(clippy::type_complexity)]
-    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 watched_percentage_fmt = {
+            if let Some(duration) = self.duration {
+                format!(
+                    " (watched: {:0.1}%)",
+                    f64::from(self.watch_progress) / duration
+                )
+            } else {
+                format!(" {watch_progress}")
+            }
         };
 
         let string = format!(
             "\
 {title} ({extractor_hash})
 | -> {cache_path}
-| -> {duration}
+| -> {duration}{watched_percentage_fmt}
 | -> {parent_subscription_name}
 | -> priority: {priority}
 | -> {publish_date}
-| -> status: {status} since {last_status_change}
-| -> {status_change}
+| -> status: {status} since {last_status_change} ({in_playlist})
 | -> {thumbnail_url}
 | -> {url}
 | -> options: {}
@@ -128,43 +57,37 @@ pub trait FormatVideo {
             video_options.to_string().trim(),
             format_text(description.to_string().as_str())
         );
-        string
+        Ok(string)
     }
 
-    fn to_line_display(&self) -> String
-    where
-        Self::Output: Display,
-    {
+    pub async fn to_line_display(&self, app: &App) -> Result<String> {
         let f = format!(
             "{} {} {} {} {} {}",
-            self.status(),
-            self.extractor_hash(),
-            self.title(),
-            self.publish_date(),
-            self.parent_subscription_name(),
-            self.duration()
+            self.status_fmt(app),
+            self.extractor_hash_fmt(app).await?,
+            self.title_fmt(app),
+            self.publish_date_fmt(app),
+            self.parent_subscription_name_fmt(app),
+            self.duration_fmt(app)
         );
 
-        f
+        Ok(f)
     }
 
-    fn to_select_file_display(&self) -> String
-    where
-        Self::Output: Display,
-    {
+    pub async fn to_select_file_display(&self, app: &App) -> Result<String> {
         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(),
+            self.status_fmt_no_color(),
+            self.video_options_fmt_no_color(app).await?,
+            self.extractor_hash_fmt_no_color(app).await?,
+            self.title_fmt_no_color(),
+            self.publish_date_fmt_no_color(),
+            self.parent_subscription_name_fmt_no_color(),
+            self.duration_fmt_no_color(),
+            self.url_fmt_no_color(),
             '\n'
         );
 
-        f
+        Ok(f)
     }
 }
diff --git a/yt/src/videos/display/mod.rs b/yt/src/videos/display/mod.rs
index 3f03ec0..21ab1d4 100644
--- a/yt/src/videos/display/mod.rs
+++ b/yt/src/videos/display/mod.rs
@@ -11,14 +11,13 @@
 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::{Video, getters::get_video_opts},
+    storage::video_database::{InPlaylist, Video, getters::get_video_opts},
 };
 
 use anyhow::{Context, Result};
@@ -35,105 +34,62 @@ macro_rules! get {
     };
 }
 
-/// This is identical to a [`FormattedVideo`], but has colorized fields.
-#[derive(Debug)]
-pub struct ColorizedFormattedVideo(FormattedVideo);
-
-impl FormattedVideo {
-    #[must_use]
-    pub fn colorize(self, app: &App) -> ColorizedFormattedVideo {
-        if app.config.global.display_colors {
-            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(),
-            })
-        } else {
-            ColorizedFormattedVideo(self)
-        }
-    }
+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()
 }
-
-/// 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,
+fn maybe_add_color<F>(app: &App, input: String, mut color_fn: F) -> String
+where
+    F: FnMut(String) -> String,
+{
+    if app.config.global.display_colors {
+        color_fn(input)
+    } else {
+        input
+    }
 }
-
 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!(
+    #[must_use]
+    pub fn cache_path_fmt(&self, app: &App) -> String {
+        let cache_path = get!(
             self,
             cache_path,
             "Cache Path",
             (|value: &PathBuf| value.to_string_lossy().to_string())
         );
-        let description = get!(
+        maybe_add_color(app, cache_path, |v| v.blue().bold().to_string())
+    }
+
+    #[must_use]
+    pub fn description_fmt(&self) -> String {
+        get!(
             self,
             description,
             "Description",
             (|value: &str| value.to_owned())
-        );
-        let duration = Duration::from(self.duration);
-        let extractor_hash = self
+        )
+    }
+
+    #[must_use]
+    pub fn duration_fmt_no_color(&self) -> String {
+        Duration::from(self.duration).to_string()
+    }
+    #[must_use]
+    pub fn duration_fmt(&self, app: &App) -> String {
+        let duration = self.duration_fmt_no_color();
+        maybe_add_color(app, duration, |v| v.cyan().bold().to_string())
+    }
+
+    #[must_use]
+    pub fn watch_progress_fmt(&self, app: &App) -> String {
+        let progress = Duration::from(Some(f64::from(self.watch_progress))).to_string();
+        maybe_add_color(app, progress, |v| v.cyan().bold().to_string())
+    }
+
+    pub async fn extractor_hash_fmt_no_color(&self, app: &App) -> Result<String> {
+        let hash = self
             .extractor_hash
             .into_short_hash(app)
             .await
@@ -142,179 +98,124 @@ impl Video {
                     "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
+            })?
+            .to_string();
+        Ok(hash)
     }
-
-    fn thumbnail_url(&self) -> Self::Output {
-        &self.thumbnail_url
-    }
-
-    fn title(&self) -> Self::Output {
-        &self.title
+    pub async fn extractor_hash_fmt(&self, app: &App) -> Result<String> {
+        let hash = self.extractor_hash_fmt_no_color(app).await?;
+        Ok(maybe_add_color(app, hash, |v| {
+            v.bright_purple().italic().to_string()
+        }))
     }
 
-    fn url(&self) -> Self::Output {
-        &self.url
+    #[must_use]
+    pub fn in_playlist_fmt(&self, app: &App) -> String {
+        let output = match self.in_playlist {
+            InPlaylist::Excluded => "Not in the playlist",
+            InPlaylist::Hidden => "In the playlist",
+            InPlaylist::Focused => "In the playlist and focused",
+        };
+        maybe_add_color(app, output.to_owned(), |v| v.yellow().italic().to_string())
     }
-
-    fn video_options(&self) -> Self::Output {
-        &self.video_options
+    #[must_use]
+    pub fn last_status_change_fmt(&self, app: &App) -> String {
+        let lsc = date_from_stamp(self.last_status_change);
+        maybe_add_color(app, lsc, |v| v.bright_cyan().to_string())
     }
-}
-impl<'a> FormatVideo for &'a ColorizedFormattedVideo {
-    type Output = &'a str;
 
-    fn cache_path(&self) -> Self::Output {
-        &self.0.cache_path
+    #[must_use]
+    pub fn parent_subscription_name_fmt_no_color(&self) -> String {
+        get!(
+            self,
+            parent_subscription_name,
+            "author",
+            (|sub: &str| sub.replace('"', "'"))
+        )
     }
-
-    fn description(&self) -> Self::Output {
-        &self.0.description
+    #[must_use]
+    pub fn parent_subscription_name_fmt(&self, app: &App) -> String {
+        let psn = self.parent_subscription_name_fmt_no_color();
+        maybe_add_color(app, psn, |v| v.bright_magenta().to_string())
     }
 
-    fn duration(&self) -> Self::Output {
-        &self.0.duration
+    #[must_use]
+    pub fn priority_fmt(&self) -> String {
+        self.priority.to_string()
     }
 
-    fn extractor_hash(&self) -> Self::Output {
-        &self.0.extractor_hash
+    #[must_use]
+    pub fn publish_date_fmt_no_color(&self) -> String {
+        get!(
+            self,
+            publish_date,
+            "release date",
+            (|date: &i64| date_from_stamp(*date))
+        )
     }
-
-    fn last_status_change(&self) -> Self::Output {
-        &self.0.last_status_change
+    #[must_use]
+    pub fn publish_date_fmt(&self, app: &App) -> String {
+        let date = self.publish_date_fmt_no_color();
+        maybe_add_color(app, date, |v| v.bright_white().bold().to_string())
     }
 
-    fn parent_subscription_name(&self) -> Self::Output {
-        &self.0.parent_subscription_name
+    #[must_use]
+    pub fn status_fmt_no_color(&self) -> String {
+        // TODO: We might support `.trim()`ing that, as the extra whitespace could be bad in the
+        // selection file. <2024-10-07>
+        self.status.as_command().to_string()
     }
-
-    fn priority(&self) -> Self::Output {
-        &self.0.priority
+    #[must_use]
+    pub fn status_fmt(&self, app: &App) -> String {
+        let status = self.status_fmt_no_color();
+        maybe_add_color(app, status, |v| v.red().bold().to_string())
     }
 
-    fn publish_date(&self) -> Self::Output {
-        &self.0.publish_date
+    #[must_use]
+    pub fn thumbnail_url_fmt(&self) -> String {
+        get!(
+            self,
+            thumbnail_url,
+            "thumbnail URL",
+            (|url: &Url| url.to_string())
+        )
     }
 
-    fn status(&self) -> Self::Output {
-        &self.0.status
+    #[must_use]
+    pub fn title_fmt_no_color(&self) -> String {
+        self.title.replace(['"', '„', '”', '“'], "'")
     }
-
-    fn status_change(&self) -> Self::Output {
-        &self.0.status_change
+    #[must_use]
+    pub fn title_fmt(&self, app: &App) -> String {
+        let title = self.title_fmt_no_color();
+        maybe_add_color(app, title, |v| v.green().bold().to_string())
     }
 
-    fn thumbnail_url(&self) -> Self::Output {
-        &self.0.thumbnail_url
+    #[must_use]
+    pub fn url_fmt_no_color(&self) -> String {
+        self.url.as_str().replace('"', "\\\"")
     }
-
-    fn title(&self) -> Self::Output {
-        &self.0.title
+    #[must_use]
+    pub fn url_fmt(&self, app: &App) -> String {
+        let url = self.url_fmt_no_color();
+        maybe_add_color(app, url, |v| v.italic().to_string())
     }
 
-    fn url(&self) -> Self::Output {
-        &self.0.url
+    pub async fn video_options_fmt_no_color(&self, app: &App) -> Result<String> {
+        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(video_options)
     }
-
-    fn video_options(&self) -> Self::Output {
-        &self.0.video_options
+    pub async fn video_options_fmt(&self, app: &App) -> Result<String> {
+        let opts = self.video_options_fmt_no_color(app).await?;
+        Ok(maybe_add_color(app, opts, |v| v.bright_green().to_string()))
     }
 }
diff --git a/yt/src/videos/mod.rs b/yt/src/videos/mod.rs
index 23613f7..2f9d8af 100644
--- a/yt/src/videos/mod.rs
+++ b/yt/src/videos/mod.rs
@@ -9,7 +9,6 @@
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 use anyhow::Result;
-use display::{FormattedVideo, format_video::FormatVideo};
 use futures::{TryStreamExt, stream::FuturesUnordered};
 use nucleo_matcher::{
     Matcher,
@@ -20,15 +19,19 @@ pub mod display;
 
 use crate::{
     app::App,
-    storage::video_database::{VideoStatus, getters::get_videos},
+    storage::video_database::{Video, VideoStatus, getters::get_videos},
 };
 
+async fn to_line_display_owned(video: Video, app: &App) -> Result<String> {
+    (&video).to_line_display(&app).await
+}
+
 pub async fn query(app: &App, limit: Option<usize>, search_query: Option<String>) -> Result<()> {
     let all_videos = get_videos(app, VideoStatus::ALL, None).await?;
 
     // turn one video to a color display, to pre-warm the hash shrinking cache
     if let Some(val) = all_videos.first() {
-        val.to_formatted_video(app).await?;
+        val.to_line_display(app).await?;
     }
 
     let limit = limit.unwrap_or(all_videos.len());
@@ -36,13 +39,10 @@ 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_formatted_video_owned(app))
+        .map(|vid| to_line_display_owned(vid, app))
         .collect::<FuturesUnordered<_>>()
-        .try_collect::<Vec<FormattedVideo>>()
-        .await?
-        .into_iter()
-        .map(|vid| (&vid.colorize(app)).to_line_display())
-        .collect();
+        .try_collect::<Vec<String>>()
+        .await?;
 
     if let Some(query) = search_query {
         let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT.match_paths());