about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/cli.rs4
-rw-r--r--src/select/cmds.rs60
-rw-r--r--src/select/selection_file/display.rs5
-rw-r--r--src/select/selection_file/help.str1
-rw-r--r--src/update/mod.rs66
5 files changed, 109 insertions, 27 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 51809c0..d19586e 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -238,6 +238,10 @@ pub enum SelectCommand {
         use_last_selection: bool,
     },
 
+    /// Add a video to the database
+    #[command(visible_alias = "a")]
+    Add { urls: Vec<Url> },
+
     /// Mark the video given by the hash to be watched
     #[command(visible_alias = "w")]
     Watch {
diff --git a/src/select/cmds.rs b/src/select/cmds.rs
index 3a7a800..b45cc48 100644
--- a/src/select/cmds.rs
+++ b/src/select/cmds.rs
@@ -11,14 +11,19 @@
 use crate::{
     app::App,
     cli::{SelectCommand, SharedSelectionCommandArgs},
+    download::download_options::download_opts,
     storage::video_database::{
+        self,
         getters::get_video_by_hash,
-        setters::{set_video_options, set_video_status},
+        setters::{add_video, set_video_options, set_video_status},
         VideoOptions, VideoStatus,
     },
+    update::video_entry_to_video,
 };
 
-use anyhow::{Context, Result};
+use anyhow::{bail, Context, Result};
+use futures::future::join_all;
+use yt_dlp::wrapper::info_json::InfoType;
 
 pub async fn handle_select_cmd(
     app: &App,
@@ -35,6 +40,57 @@ pub async fn handle_select_cmd(
         SelectCommand::Watched { shared } => {
             handle_status_change(app, shared, line_number, VideoStatus::Watched).await?;
         }
+        SelectCommand::Add { urls } => {
+            for url in urls {
+                let opts = download_opts(
+                    &app,
+                    video_database::YtDlpOptions {
+                        subtitle_langs: "".to_owned(),
+                    },
+                );
+                let entry = yt_dlp::extract_info(&opts, &url, false, true)
+                    .await
+                    .with_context(|| format!("Failed to fetch entry for url: '{}'", url))?;
+
+                async fn add_entry(
+                    app: &App,
+                    entry: yt_dlp::wrapper::info_json::InfoJson,
+                ) -> Result<()> {
+                    let video = video_entry_to_video(entry, None)?;
+                    println!("{}", video.to_color_display(app).await?);
+                    add_video(app, video).await?;
+
+                    Ok(())
+                }
+
+                match entry._type {
+                    Some(InfoType::Video) => {
+                        add_entry(&app, entry).await?;
+                    }
+                    Some(InfoType::Playlist) => {
+                        if let Some(mut entries) = entry.entries {
+                            if !entries.is_empty() {
+                                // Pre-warm the cache
+                                add_entry(app, entries.remove(0)).await?;
+
+                                let futures: Vec<_> = entries
+                                    .into_iter()
+                                    .map(|entry| add_entry(&app, entry))
+                                    .collect();
+
+                                join_all(futures).await.into_iter().collect::<Result<_>>()?;
+                            }
+                        } else {
+                            bail!("Your playlist does not seem to have any entries!")
+                        }
+                    }
+                    other => bail!(
+                        "Your URL should point to a video or a playlist, but points to a '{:#?}'",
+                        other
+                    ),
+                }
+            }
+        }
         SelectCommand::Watch { shared } => {
             let hash = shared.hash.clone().realize(app).await?;
 
diff --git a/src/select/selection_file/display.rs b/src/select/selection_file/display.rs
index 8ff6a15..0714015 100644
--- a/src/select/selection_file/display.rs
+++ b/src/select/selection_file/display.rs
@@ -10,7 +10,7 @@
 
 use std::fmt::Write;
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use chrono::DateTime;
 use log::debug;
 
@@ -31,7 +31,8 @@ impl Video {
         let mut f = String::new();
 
         let opts = get_video_opts(app, &self.extractor_hash)
-            .await?
+            .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 { "" };
 
diff --git a/src/select/selection_file/help.str b/src/select/selection_file/help.str
index f3ad2f2..eb76ce5 100644
--- a/src/select/selection_file/help.str
+++ b/src/select/selection_file/help.str
@@ -4,6 +4,7 @@
 #   d,  drop     [-p,-s,-l]   Mark the video given by the hash to be dropped
 #   u,  url      [-p,-s,-l]   Open the video URL in Firefox's `timesinks.youtube` profile
 #   p,  pick     [-p,-s,-l]   Reset the videos status to 'Pick'
+#   a,  add      URL          Add a video, defined by the URL
 #
 # See `yt select <cmd_name> --help` for more help.
 #
diff --git a/src/update/mod.rs b/src/update/mod.rs
index ce0a7e5..ce3a7f9 100644
--- a/src/update/mod.rs
+++ b/src/update/mod.rs
@@ -120,12 +120,7 @@ pub async fn update(
     Ok(())
 }
 
-async fn process_subscription(
-    app: &App,
-    sub: &Subscription,
-    entry: InfoJson,
-    hashes: &[blake3::Hash],
-) -> Result<()> {
+pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Result<Video> {
     macro_rules! unwrap_option {
         ($option:expr) => {
             match $option {
@@ -197,26 +192,51 @@ async fn process_subscription(
 
     let extractor_hash = blake3::hash(unwrap_option!(entry.id).as_bytes());
 
-    if hashes.contains(&extractor_hash) {
+    let subscription_name = if let Some(sub) = sub {
+        Some(sub.name.clone())
+    } else {
+        if let Some(uploader) = entry.uploader {
+            if entry.webpage_url_domain == Some("youtube.com".to_owned()) {
+                Some(format!("{} - Videos", uploader))
+            } else {
+                Some(uploader.clone())
+            }
+        } else {
+            None
+        }
+    };
+
+    let video = Video {
+        cache_path: None,
+        description: entry.description.clone(),
+        duration: entry.duration,
+        extractor_hash: ExtractorHash::from_hash(extractor_hash),
+        last_status_change: Utc::now().timestamp(),
+        parent_subscription_name: subscription_name,
+        priority: 0,
+        publish_date,
+        status: VideoStatus::Pick,
+        status_change: false,
+        thumbnail_url,
+        title: unwrap_option!(entry.title.clone()),
+        url,
+    };
+    Ok(video)
+}
+
+async fn process_subscription(
+    app: &App,
+    sub: &Subscription,
+    entry: InfoJson,
+    hashes: &[blake3::Hash],
+) -> Result<()> {
+    let video =
+        video_entry_to_video(entry, Some(sub)).context("Failed to parse search entry as Video")?;
+
+    if hashes.contains(&video.extractor_hash.hash()) {
         // We already stored the video information
         unreachable!("The python update script should have never provided us a duplicated video");
     } else {
-        let video = Video {
-            cache_path: None,
-            description: entry.description.clone(),
-            duration: entry.duration,
-            extractor_hash: ExtractorHash::from_hash(extractor_hash),
-            last_status_change: Utc::now().timestamp(),
-            parent_subscription_name: Some(sub.name.clone()),
-            priority: 0,
-            publish_date,
-            status: VideoStatus::Pick,
-            status_change: false,
-            thumbnail_url,
-            title: unwrap_option!(entry.title.clone()),
-            url,
-        };
-
         println!("{}", video.to_color_display(app).await?);
         add_video(app, video).await?;
         Ok(())