diff options
Diffstat (limited to '')
-rw-r--r-- | crates/yt/src/download/progress_hook.rs | 2 | ||||
-rw-r--r-- | crates/yt/src/select/cmds/add.rs | 12 | ||||
-rw-r--r-- | crates/yt/src/select/mod.rs | 29 | ||||
-rw-r--r-- | crates/yt/src/storage/subscriptions.rs | 6 | ||||
-rw-r--r-- | crates/yt/src/storage/video_database/set/mod.rs | 10 | ||||
-rw-r--r-- | crates/yt/src/subscribe/mod.rs | 29 | ||||
-rw-r--r-- | crates/yt_dlp/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/yt_dlp/src/lib.rs | 1 | ||||
-rw-r--r-- | crates/yt_dlp/src/options.rs | 8 | ||||
-rw-r--r-- | crates/yt_dlp/src/package_hacks/mod.rs | 11 | ||||
-rw-r--r-- | crates/yt_dlp/src/package_hacks/urllib3.rs | 35 | ||||
-rw-r--r-- | crates/yt_dlp/src/package_hacks/urllib3_polyfill.py | 13 |
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 |