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.
-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());