about summary refs log tree commit diff stats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/yt/src/download/progress_hook.rs2
-rw-r--r--crates/yt/src/select/cmds/add.rs12
-rw-r--r--crates/yt/src/select/mod.rs29
-rw-r--r--crates/yt/src/storage/subscriptions.rs6
-rw-r--r--crates/yt/src/storage/video_database/set/mod.rs10
-rw-r--r--crates/yt/src/subscribe/mod.rs29
-rw-r--r--crates/yt_dlp/Cargo.toml2
-rw-r--r--crates/yt_dlp/src/lib.rs1
-rw-r--r--crates/yt_dlp/src/options.rs8
-rw-r--r--crates/yt_dlp/src/package_hacks/mod.rs11
-rw-r--r--crates/yt_dlp/src/package_hacks/urllib3.rs35
-rw-r--r--crates/yt_dlp/src/package_hacks/urllib3_polyfill.py13
12 files changed, 121 insertions, 37 deletions
diff --git a/crates/yt/src/download/progress_hook.rs b/crates/yt/src/download/progress_hook.rs
index c507165..ad754b0 100644
--- a/crates/yt/src/download/progress_hook.rs
+++ b/crates/yt/src/download/progress_hook.rs
@@ -168,7 +168,7 @@ pub fn progress_hook(
             move_to_col(1);
 
             eprint!(
-                "'{}' [{}/{} at {}] -> [{} of {}{} {}] ",
+                "{} [{}/{} at {}] -> [{} of {}{} {}] ",
                 c!("34;1", get_title()),
                 c!("33;1", MaybeDuration::from_secs_f64(elapsed)),
                 c!("33;1", MaybeDuration::from_secs_f64(eta)),
diff --git a/crates/yt/src/select/cmds/add.rs b/crates/yt/src/select/cmds/add.rs
index 2fff298..2c9a323 100644
--- a/crates/yt/src/select/cmds/add.rs
+++ b/crates/yt/src/select/cmds/add.rs
@@ -48,6 +48,7 @@ pub(super) async fn add(
             let hashes = get_all_hashes(app)
                 .await
                 .context("Failed to fetch all video hashes")?;
+
             let extractor_hash = blake3::hash(json_get!(entry, "id", as_str).as_bytes());
             if hashes.contains(&extractor_hash) {
                 error!(
@@ -61,9 +62,10 @@ pub(super) async fn add(
                                 .get("url")
                                 .map_or("<Unknown video Url>".to_owned(), ToString::to_string)
                         ))?,
-                    entry
-                        .get("title")
-                        .map_or(String::new(), |title| format!(" ('{title}')"))
+                    entry.get("title").map_or(String::new(), |title| format!(
+                        " (\"{}\")",
+                        json_cast!(title, as_str)
+                    ))
                 );
                 return Ok(());
             }
@@ -88,7 +90,7 @@ pub(super) async fn add(
             .with_context(|| format!("Failed to fetch entry for url: '{url}'"))?;
 
         match entry.get("_type").map(|val| json_cast!(val, as_str)) {
-            Some("Video") => {
+            Some("video") => {
                 add_entry(app, entry).await?;
                 if start.is_some() || stop.is_some() {
                     warn!(
@@ -96,7 +98,7 @@ pub(super) async fn add(
                     );
                 }
             }
-            Some("Playlist") => {
+            Some("playlist") => {
                 if let Some(entries) = entry.get("entries") {
                     let entries = json_cast!(entries, as_array);
                     let start = start.unwrap_or(0);
diff --git a/crates/yt/src/select/mod.rs b/crates/yt/src/select/mod.rs
index 135bd76..2478b76 100644
--- a/crates/yt/src/select/mod.rs
+++ b/crates/yt/src/select/mod.rs
@@ -12,8 +12,8 @@
 use std::{
     collections::HashMap,
     env::{self},
-    fs::{self, File},
-    io::{BufRead, BufReader, BufWriter, Write},
+    fs::{self, File, OpenOptions},
+    io::{BufRead, BufReader, BufWriter, Read, Seek, Write},
     iter,
     path::Path,
     string::String,
@@ -134,12 +134,25 @@ pub async fn select_split(
 
     paths.sort();
 
-    let mut processed = 0;
+    let mut persistent_file = OpenOptions::new()
+        .read(true)
+        .write(true)
+        .truncate(true)
+        .open(&app.config.paths.last_selection_path)
+        .context("Failed to open persistent selection file")?;
+
     for path in paths {
-        let read_file = File::open(path)?;
-        processed = process_file(app, &read_file, processed).await?;
+        let mut read_file = File::open(path)?;
+
+        let mut buffer = vec![];
+        read_file.read_to_end(&mut buffer)?;
+        persistent_file.write_all(&buffer)?;
     }
 
+    persistent_file.rewind()?;
+
+    let processed = process_file(app, &persistent_file).await?;
+
     info!("Processed {processed} records.");
     temp_dir.close().context("Failed to close the temp dir")?;
     Ok(())
@@ -167,7 +180,7 @@ pub async fn select_file(app: &App, done: bool, use_last_selection: bool) -> Res
     fs::copy(temp_file.path(), &app.config.paths.last_selection_path)
         .context("Failed to persist selection file")?;
 
-    let processed = process_file(app, &read_file, 0).await?;
+    let processed = process_file(app, &read_file).await?;
     info!("Processed {processed} records.");
 
     Ok(())
@@ -220,10 +233,10 @@ async fn write_videos_to_file(app: &App, file: &File, videos: &[Video]) -> Resul
     Ok(())
 }
 
-async fn process_file(app: &App, file: &File, processed: i64) -> Result<i64> {
+async fn process_file(app: &App, file: &File) -> Result<i64> {
     let reader = BufReader::new(file);
 
-    let mut line_number = -processed;
+    let mut line_number = 0;
 
     for line in reader.lines() {
         let line = line.context("Failed to read a line")?;
diff --git a/crates/yt/src/storage/subscriptions.rs b/crates/yt/src/storage/subscriptions.rs
index 1ab0d72..0e8ae85 100644
--- a/crates/yt/src/storage/subscriptions.rs
+++ b/crates/yt/src/storage/subscriptions.rs
@@ -17,7 +17,7 @@ use anyhow::Result;
 use log::debug;
 use sqlx::query;
 use url::Url;
-use yt_dlp::options::YoutubeDLOptions;
+use yt_dlp::{json_cast, options::YoutubeDLOptions};
 
 use crate::{app::App, unreachable::Unreachable};
 
@@ -48,9 +48,9 @@ pub async fn check_url(url: Url) -> Result<bool> {
 
     let info = yt_dlp.extract_info(&url, false, false)?;
 
-    debug!("{:#?}", info);
+    debug!("{info:#?}");
 
-    Ok(info.get("_type") == Some(&serde_json::Value::String("Playlist".to_owned())))
+    Ok(info.get("_type").map(|v| json_cast!(v, as_str)) == Some("playlist"))
 }
 
 #[derive(Default, Debug)]
diff --git a/crates/yt/src/storage/video_database/set/mod.rs b/crates/yt/src/storage/video_database/set/mod.rs
index 8c1be4a..1b19011 100644
--- a/crates/yt/src/storage/video_database/set/mod.rs
+++ b/crates/yt/src/storage/video_database/set/mod.rs
@@ -92,10 +92,7 @@ pub async fn video_status(
 
         let now = Utc::now().timestamp();
 
-        debug!(
-            "Running status change: {:#?} -> {:#?}...",
-            old_marker, new_status,
-        );
+        debug!("Running status change: {old_marker:#?} -> {new_status:#?}...",);
 
         let new_status = new_status.as_db_integer();
         let new_priority = new_priority.as_db_integer();
@@ -121,10 +118,7 @@ pub async fn video_status(
 
         let now = Utc::now().timestamp();
 
-        debug!(
-            "Running status change: {:#?} -> {:#?}...",
-            old_marker, new_status,
-        );
+        debug!("Running status change: {old_marker:#?} -> {new_status:#?}...",);
 
         let new_status = new_status.as_db_integer();
         query!(
diff --git a/crates/yt/src/subscribe/mod.rs b/crates/yt/src/subscribe/mod.rs
index a965ac0..66797e8 100644
--- a/crates/yt/src/subscribe/mod.rs
+++ b/crates/yt/src/subscribe/mod.rs
@@ -13,10 +13,10 @@ use std::str::FromStr;
 
 use anyhow::{Context, Result, bail};
 use futures::FutureExt;
-use log::warn;
+use log::{error, warn};
 use tokio::io::{AsyncBufRead, AsyncBufReadExt};
 use url::Url;
-use yt_dlp::{json_get, options::YoutubeDLOptions};
+use yt_dlp::{json_cast, json_get, options::YoutubeDLOptions};
 
 use crate::{
     app::App,
@@ -121,17 +121,26 @@ pub async fn subscribe(app: &App, name: Option<String>, url: Url) -> Result<()>
 
             out?;
         } else {
-            actual_subscribe(app, None, url.join("videos/").unreachable("See above."))
+            let _ = actual_subscribe(app, None, url.join("videos/").unreachable("See above."))
                 .await
-                .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Videos}"))?;
+                .map_err(|err| {
+                    error!("Failed to subscribe to the '{}' variant: {err}", "{Videos}");
+                });
 
-            actual_subscribe(app, None, url.join("streams/").unreachable("See above."))
+            let _ = actual_subscribe(app, None, url.join("streams/").unreachable("See above."))
                 .await
-                .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Streams}"))?;
-
-            actual_subscribe(app, None, url.join("shorts/").unreachable("See above."))
+                .map_err(|err| {
+                    error!(
+                        "Failed to subscribe to the '{}' variant: {err}",
+                        "{Streams}"
+                    );
+                });
+
+            let _ = actual_subscribe(app, None, url.join("shorts/").unreachable("See above."))
                 .await
-                .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Shorts}"))?;
+                .map_err(|err| {
+                    error!("Failed to subscribe to the '{}' variant: {err}", "{Shorts}");
+                });
         }
     } else {
         actual_subscribe(app, name, url).await?;
@@ -157,7 +166,7 @@ async fn actual_subscribe(app: &App, name: Option<String>, url: Url) -> Result<(
 
         let info = yt_dlp.extract_info(&url, false, false)?;
 
-        if info.get("_type") == Some(&serde_json::Value::String("Playlist".to_owned())) {
+        if info.get("_type").map(|v| json_cast!(v, as_str)) == Some("playlist") {
             json_get!(info, "title", as_str).to_owned()
         } else {
             bail!("The url ('{}') does not represent a playlist!", &url)
diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml
index b8791a0..3632b23 100644
--- a/crates/yt_dlp/Cargo.toml
+++ b/crates/yt_dlp/Cargo.toml
@@ -25,7 +25,7 @@ publish = true
 curl = "0.4.48"
 indexmap = { version = "2.9.0", default-features = false }
 log.workspace = true
-rustpython = { git = "https://github.com/RustPython/RustPython.git", rev = "c968fe0f", features = [
+rustpython = { git = "https://github.com/RustPython/RustPython.git", rev = "6a992d4f", features = [
   "threading",
   "stdlib",
   "stdio",
diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs
index c412704..a03e444 100644
--- a/crates/yt_dlp/src/lib.rs
+++ b/crates/yt_dlp/src/lib.rs
@@ -33,6 +33,7 @@ pub mod progress_hook;
 pub mod python_error;
 
 mod logging;
+mod package_hacks;
 
 #[macro_export]
 macro_rules! json_get {
diff --git a/crates/yt_dlp/src/options.rs b/crates/yt_dlp/src/options.rs
index 182b8a1..dc3c154 100644
--- a/crates/yt_dlp/src/options.rs
+++ b/crates/yt_dlp/src/options.rs
@@ -22,7 +22,8 @@ use rustpython::{
 };
 
 use crate::{
-    YoutubeDL, json_loads, logging::setup_logging, post_processors, python_error::process_exception,
+    YoutubeDL, json_loads, logging::setup_logging, package_hacks, post_processors,
+    python_error::process_exception,
 };
 
 /// Wrap your function with [`mk_python_function`].
@@ -138,6 +139,11 @@ impl YoutubeDL {
         let output_options = options.options.clone();
 
         let (yt_dlp_module, youtube_dl_class) = match interpreter.enter(|vm| {
+            {
+                // Add missing (and required) values to the stdlib
+                package_hacks::urllib3::apply_hacks(vm)?;
+            }
+
             let yt_dlp_module = vm.import("yt_dlp", 0)?;
             let class = yt_dlp_module.get_attr("YoutubeDL", vm)?;
 
diff --git a/crates/yt_dlp/src/package_hacks/mod.rs b/crates/yt_dlp/src/package_hacks/mod.rs
new file mode 100644
index 0000000..53fe323
--- /dev/null
+++ b/crates/yt_dlp/src/package_hacks/mod.rs
@@ -0,0 +1,11 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// 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>.
+
+pub(super) mod urllib3;
diff --git a/crates/yt_dlp/src/package_hacks/urllib3.rs b/crates/yt_dlp/src/package_hacks/urllib3.rs
new file mode 100644
index 0000000..28ae37a
--- /dev/null
+++ b/crates/yt_dlp/src/package_hacks/urllib3.rs
@@ -0,0 +1,35 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// 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 rustpython::vm::{PyResult, VirtualMachine};
+
+// NOTE(@bpeetz): Remove this, once rust-python supports these features. <2025-06-27>
+pub(crate) fn apply_hacks(vm: &VirtualMachine) -> PyResult<()> {
+    {
+        // Urllib3 tries to import this value, regardless if it is set.
+        let ssl_module = vm.import("ssl", 0)?;
+        ssl_module.set_attr("VERIFY_X509_STRICT", vm.ctx.new_int(0x20), vm)?;
+    }
+
+    {
+        // Urllib3 tries to set the SSLContext.verify_flags value, regardless if it exists or not.
+        // So we need to provide a polyfill.
+
+        let scope = vm.new_scope_with_builtins();
+
+        vm.run_code_string(
+            scope,
+            include_str!("urllib3_polyfill.py"),
+            "<embedded urllib3 polyfill workaround code>".to_owned(),
+        )?;
+    }
+
+    Ok(())
+}
diff --git a/crates/yt_dlp/src/package_hacks/urllib3_polyfill.py b/crates/yt_dlp/src/package_hacks/urllib3_polyfill.py
new file mode 100644
index 0000000..610fd99
--- /dev/null
+++ b/crates/yt_dlp/src/package_hacks/urllib3_polyfill.py
@@ -0,0 +1,13 @@
+# yt - A fully featured command line YouTube client
+#
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of Yt.
+#
+# 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>.
+
+import ssl
+
+ssl.SSLContext.verify_flags = 0