diff options
-rw-r--r-- | yt/src/cli.rs | 7 | ||||
-rw-r--r-- | yt/src/main.rs | 1 | ||||
-rw-r--r-- | yt/src/watch/playlist.rs | 99 |
3 files changed, 107 insertions, 0 deletions
diff --git a/yt/src/cli.rs b/yt/src/cli.rs index 948138d..9851211 100644 --- a/yt/src/cli.rs +++ b/yt/src/cli.rs @@ -89,6 +89,13 @@ pub enum Command { /// Watch the already cached (and selected) videos Watch {}, + /// Visualize the current playlist + Playlist { + /// Linger and display changes + #[arg(short, long)] + watch: bool, + }, + /// Show, which videos have been selected to be watched (and their cache status) Status {}, diff --git a/yt/src/main.rs b/yt/src/main.rs index b32b1d3..9141fac 100644 --- a/yt/src/main.rs +++ b/yt/src/main.rs @@ -204,6 +204,7 @@ async fn main() -> Result<()> { }, Command::Watch {} => watch::watch(Arc::new(app)).await?, + Command::Playlist { watch } => watch::playlist::playlist(&app, watch).await?, Command::Status {} => status::show(&app).await?, Command::Config {} => status::config(&app)?, diff --git a/yt/src/watch/playlist.rs b/yt/src/watch/playlist.rs new file mode 100644 index 0000000..9b07b09 --- /dev/null +++ b/yt/src/watch/playlist.rs @@ -0,0 +1,99 @@ +use std::path::Path; + +use crate::{ + app::App, + storage::video_database::{Video, VideoStatus, get, notify::wait_for_db_write}, +}; + +use anyhow::Result; +use futures::{TryStreamExt, stream::FuturesOrdered}; + +/// Extract the values of the [`VideoStatus::Cached`] value from a Video. +fn cache_values(video: &Video) -> (&Path, bool) { + if let VideoStatus::Cached { + cache_path, + is_focused, + } = &video.status + { + (cache_path, *is_focused) + } else { + unreachable!("All of these videos should be cached"); + } +} + +// ANSI ESCAPE CODES Wrappers {{{ +// see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands +const CSI: &str = "\x1b["; +fn erase_in_display_from_cursor() { + print!("{CSI}0J"); +} +fn cursor_up(number: usize) { + print!("{CSI}{number}A"); +} +// }}} + +/// # Panics +/// Only if internal assertions fail. +pub async fn playlist(app: &App, watch: bool) -> Result<()> { + let mut previous_output_length = 0; + loop { + let playlist = get::playlist(app).await?.to_videos(); + + let output = playlist + .into_iter() + .map(|video| async move { + let mut output = String::new(); + + let (_, is_focused) = cache_values(&video); + + if is_focused { + output.push_str("🔻 "); + } else { + output.push_str(" "); + } + + output.push_str(&video.title_fmt(app)); + + output.push_str(" ("); + output.push_str(&video.parent_subscription_name_fmt(app)); + output.push(')'); + + output.push_str(" ["); + output.push_str(&video.duration_fmt(app)); + + if is_focused { + output.push_str(" ("); + output.push_str(&if let Some(duration) = video.duration.as_secs() { + format!("{:0.0}%", (video.watch_progress.as_secs() / duration) * 100) + } else { + video.watch_progress_fmt(app) + }); + output.push(')'); + } + output.push(']'); + + output.push('\n'); + + Ok::<String, anyhow::Error>(output) + }) + .collect::<FuturesOrdered<_>>() + .try_collect::<String>() + .await?; + + // Delete the previous output + cursor_up(previous_output_length); + erase_in_display_from_cursor(); + + previous_output_length = output.chars().filter(|ch| *ch == '\n').count(); + + print!("{output}"); + + if !watch { + break; + } + + wait_for_db_write(app).await?; + } + + Ok(()) +} |