about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-06-15 17:33:47 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-06-15 17:33:47 +0200
commite2d5dc6a9f000a875c3f2a100f660adc2a43275a (patch)
tree2f32ab1a434edb88361a6248c0378d88d71a8942
parentrefactor(yt/select): Split the `select::select` function up (diff)
downloadyt-e2d5dc6a9f000a875c3f2a100f660adc2a43275a.zip
feat(yt/select): Support a directory selection process
The single file approach becomes unwieldy once one has more open videos.
Diffstat (limited to '')
-rw-r--r--crates/yt/src/cli.rs5
-rw-r--r--crates/yt/src/main.rs12
-rw-r--r--crates/yt/src/select/mod.rs103
3 files changed, 110 insertions, 10 deletions
diff --git a/crates/yt/src/cli.rs b/crates/yt/src/cli.rs
index 11346b4..43df15f 100644
--- a/crates/yt/src/cli.rs
+++ b/crates/yt/src/cli.rs
@@ -281,6 +281,10 @@ pub enum SelectCommand {
         #[arg(long, short)]
         done: bool,
 
+        /// Generate a directory, where each file contains only one subscription.
+        #[arg(long, short, conflicts_with = "use_last_selection")]
+        split: bool,
+
         /// Use the last selection file (useful if you've spend time on it and want to get it again)
         #[arg(long, short, conflicts_with = "done")]
         use_last_selection: bool,
@@ -344,6 +348,7 @@ impl Default for SelectCommand {
         Self::File {
             done: false,
             use_last_selection: false,
+            split: false,
         }
     }
 }
diff --git a/crates/yt/src/main.rs b/crates/yt/src/main.rs
index 39f52f4..e17f958 100644
--- a/crates/yt/src/main.rs
+++ b/crates/yt/src/main.rs
@@ -115,12 +115,20 @@ async fn main() -> Result<()> {
                 SelectCommand::File {
                     done,
                     use_last_selection,
-                } => Box::pin(select::select(&app, done, use_last_selection)).await?,
+                    split,
+                } => {
+                    if split {
+                        assert!(!use_last_selection);
+                        Box::pin(select::select_split(&app, done)).await?
+                    } else {
+                        Box::pin(select::select_file(&app, done, use_last_selection)).await?
+                    }
+                }
                 _ => Box::pin(handle_select_cmd(&app, cmd, None)).await?,
             }
         }
         Command::Sedowa {} => {
-            Box::pin(select::select(&app, false, false)).await?;
+            Box::pin(select::select_file(&app, false, false)).await?;
 
             let arc_app = Arc::new(app);
             dowa(arc_app).await?;
diff --git a/crates/yt/src/select/mod.rs b/crates/yt/src/select/mod.rs
index 45aa05c..668ab02 100644
--- a/crates/yt/src/select/mod.rs
+++ b/crates/yt/src/select/mod.rs
@@ -10,9 +10,12 @@
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 use std::{
+    collections::HashMap,
     env::{self},
-    fs,
+    fs::{self, File},
     io::{BufRead, BufReader, BufWriter, Write},
+    iter,
+    path::Path,
     string::String,
 };
 
@@ -28,6 +31,7 @@ use anyhow::{Context, Result, bail};
 use clap::Parser;
 use cmds::handle_select_cmd;
 use futures::{TryStreamExt, stream::FuturesOrdered};
+use log::info;
 use selection_file::process_line;
 use tempfile::Builder;
 use tokio::process::Command;
@@ -35,11 +39,92 @@ 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_split(app: &App, done: bool) -> Result<()> {
+    let temp_dir = Builder::new()
+        .prefix("yt_video_select-")
+        .rand_bytes(6)
+        .tempdir()
+        .context("Failed to get tempdir")?;
+
+    let matching_videos = get_videos(app, done).await?;
+
+    let mut no_author = vec![];
+    let mut author_map = HashMap::new();
+    for video in matching_videos {
+        if let Some(sub) = &video.parent_subscription_name {
+            if author_map.contains_key(sub) {
+                let vec: &mut Vec<_> = author_map
+                    .get_mut(sub)
+                    .unreachable("This key is set, we checked in the if above");
+
+                vec.push(video);
+            } else {
+                author_map.insert(sub.to_owned(), vec![video]);
+            }
+        } else {
+            no_author.push(video);
+        }
+    }
+
+    let author_map = {
+        let mut temp_vec: Vec<_> = author_map.into_iter().collect();
+
+        // PERFORMANCE: The clone here should not be neeed.  <2025-06-15>
+        temp_vec.sort_by_key(|(name, _)| name.to_owned());
+
+        temp_vec
+    };
+
+    for (index, (name, videos)) in author_map
+        .into_iter()
+        .chain(iter::once((
+            "<No parent subscription>".to_owned(),
+            no_author,
+        )))
+        .enumerate()
+    {
+        let mut file_path = temp_dir.path().join(format!("{index:02}_{name}"));
+        file_path.set_extension("yts");
+
+        let tmp_file = File::create(&file_path)
+            .with_context(|| format!("Falied to create file at: {}", file_path.display()))?;
+
+        write_videos_to_file(app, &tmp_file, &videos)
+            .await
+            .with_context(|| format!("Falied to populate file at: {}", file_path.display()))?;
+    }
+
+    open_editor_at(temp_dir.path()).await?;
+
+    let mut paths = vec![];
+    for maybe_entry in temp_dir
+        .path()
+        .read_dir()
+        .context("Failed to open temp dir for reading")?
+    {
+        let entry = maybe_entry.context("Failed to read entry in temp dir")?;
+
+        if !entry.file_type()?.is_file() {
+            bail!("Found non-file entry: {}", entry.path().display());
+        }
+
+        paths.push(entry.path());
+    }
+
+    paths.sort();
+
+    let mut processed = 0;
+    for path in paths {
+        let read_file = File::open(path)?;
+        processed = process_file(app, &read_file, processed).await?;
+    }
+
+    info!("Processed {processed} records.");
+    temp_dir.close().context("Failed to close the temp dir")?;
+    Ok(())
 }
 
-pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<()> {
+pub async fn select_file(app: &App, done: bool, use_last_selection: bool) -> Result<()> {
     let temp_file = Builder::new()
         .prefix("yt_video_select-")
         .suffix(".yts")
@@ -61,7 +146,8 @@ pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<(
     fs::copy(temp_file.path(), &app.config.paths.last_selection_path)
         .context("Failed to persist selection file")?;
 
-    process_file(app, &read_file).await?;
+    let processed = process_file(app, &read_file, 0).await?;
+    info!("Processed {processed} records.");
 
     Ok(())
 }
@@ -113,10 +199,11 @@ async fn write_videos_to_file(app: &App, file: &File, videos: &[Video]) -> Resul
     Ok(())
 }
 
-async fn process_file(app: &App, file: &File) -> Result<()> {
+async fn process_file(app: &App, file: &File, processed: i64) -> Result<i64> {
     let reader = BufReader::new(file);
 
-    let mut line_number = 0;
+    let mut line_number = -processed;
+
     for line in reader.lines() {
         let line = line.context("Failed to read a line")?;
 
@@ -156,7 +243,7 @@ async fn process_file(app: &App, file: &File) -> Result<()> {
         }
     }
 
-    Ok(())
+    Ok(line_number * -1)
 }
 
 async fn open_editor_at(path: &Path) -> Result<()> {