aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--yt/src/cli.rs7
-rw-r--r--yt/src/main.rs1
-rw-r--r--yt/src/watch/playlist.rs99
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(())
+}