aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/yt/src/update/updater.rs34
-rw-r--r--crates/yt_dlp/src/lib.rs140
2 files changed, 122 insertions, 52 deletions
diff --git a/crates/yt/src/update/updater.rs b/crates/yt/src/update/updater.rs
index 8da654b..b314172 100644
--- a/crates/yt/src/update/updater.rs
+++ b/crates/yt/src/update/updater.rs
@@ -18,7 +18,7 @@ use futures::{
};
use log::{Level, debug, error, log_enabled};
use serde_json::json;
-use yt_dlp::{InfoJson, YoutubeDLOptions, json_cast, json_get};
+use yt_dlp::{InfoJson, YoutubeDL, YoutubeDLOptions, json_cast, json_get, process_ie_result};
use crate::{
ansi_escape_codes::{clear_whole_line, move_to_col},
@@ -135,28 +135,18 @@ impl<'a> Updater<'a> {
.filter_map(|base| match base {
Ok(ok) => Some(ok),
Err(err) => {
- // TODO(@bpeetz): Add this <2025-06-13>
- // if let YtDlpError::PythonError { error, kind } = &err {
- // if kind.as_str() == "<class 'yt_dlp.utils.DownloadError'>"
- // && error.to_string().as_str().contains(
- // "Join this channel to get access to members-only content ",
- // )
- // {
- // // Hide this error
- // } else {
- // let error_string = error.to_string();
- // let error = error_string
- // .strip_prefix("DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ")
- // .expect("This prefix should exists");
- // error!("{error}");
- // }
- // return None;
- // }
+ let process_ie_result::Error::Python(err) = &err;
+
+ if err.contains("Join this channel to get access to members-only content ") {
+ // Hide this error
+ } else {
+ // Show the error, but don't fail.
+ let error = err
+ .strip_prefix("DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ")
+ .unwrap_or(err);
+ error!("{error}");
+ }
- // TODO(@bpeetz): Ideally, we _would_ actually exit on unexpected errors, but
- // this is fine for now. <2025-06-13>
- // Some(Err(err).context("Failed to process new entries."))
- error!("While processing entry: {err}");
None
}
})
diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs
index 34b8a5d..99197b2 100644
--- a/crates/yt_dlp/src/lib.rs
+++ b/crates/yt_dlp/src/lib.rs
@@ -1,19 +1,18 @@
//! The `yt_dlp` interface is completely contained in the [`YoutubeDL`] structure.
-use std::io::Write;
-use std::mem;
-use std::{env, fs::File, path::PathBuf};
+use std::{self, env, mem, path::PathBuf};
use indexmap::IndexMap;
use log::{Level, debug, error, info, log_enabled};
use logging::setup_logging;
-use rustpython::vm::builtins::PyList;
use rustpython::{
InterpreterConfig,
vm::{
- self, Interpreter, PyObjectRef, PyRef, VirtualMachine,
- builtins::{PyBaseException, PyDict, PyStr},
+ self, AsObject, Interpreter, PyObjectRef, PyPayload, PyRef, VirtualMachine,
+ builtins::{PyBaseException, PyBaseExceptionRef, PyDict, PyList, PyStr},
function::{FuncArgs, KwArgs, PosArgs},
+ py_io::Write,
+ suggestion::offer_suggestions,
},
};
use url::Url;
@@ -177,12 +176,16 @@ impl YoutubeDL {
Ok::<_, PyRef<PyBaseException>>((yt_dlp_module, youtube_dl_class))
}) {
- Ok(ok) => ok,
+ Ok(ok) => Ok(ok),
Err(err) => {
- interpreter.finalize(Some(err));
- return Err(build::Error::Python);
+ // TODO(@bpeetz): Do we want to run `interpreter.finalize` here? <2025-06-14>
+ // interpreter.finalize(Some(err));
+ interpreter.enter(|vm| {
+ let buffer = process_exception(vm, &err);
+ Err(build::Error::Python(buffer))
+ })
}
- };
+ }?;
Ok(Self {
interpreter,
@@ -331,12 +334,10 @@ impl YoutubeDL {
Ok::<_, PyRef<PyBaseException>>(result_json)
}) {
Ok(ok) => Ok(ok),
- Err(err) => {
- self.interpreter.enter(|vm| {
- vm.print_exception(err);
- });
- Err(extract_info::Error::Python)
- }
+ Err(err) => self.interpreter.enter(|vm| {
+ let buffer = process_exception(vm, &err);
+ Err(extract_info::Error::Python(buffer))
+ }),
}
}
@@ -387,30 +388,28 @@ impl YoutubeDL {
Ok::<_, PyRef<PyBaseException>>(result_json)
}) {
Ok(ok) => Ok(ok),
- Err(err) => {
- self.interpreter.enter(|vm| {
- vm.print_exception(err);
- });
- Err(process_ie_result::Error::Python)
- }
+ Err(err) => self.interpreter.enter(|vm| {
+ let buffer = process_exception(vm, &err);
+ Err(process_ie_result::Error::Python(buffer))
+ }),
}
}
}
#[allow(missing_docs)]
pub mod process_ie_result {
- #[derive(Debug, thiserror::Error, Clone, Copy)]
+ #[derive(Debug, thiserror::Error)]
pub enum Error {
- #[error("Python threw an exception")]
- Python,
+ #[error("Python threw an exception: {0}")]
+ Python(String),
}
}
#[allow(missing_docs)]
pub mod extract_info {
- #[derive(Debug, thiserror::Error, Clone, Copy)]
+ #[derive(Debug, thiserror::Error)]
pub enum Error {
- #[error("Python threw an exception")]
- Python,
+ #[error("Python threw an exception: {0}")]
+ Python(String),
}
}
@@ -488,8 +487,8 @@ impl YoutubeDLOptions {
pub mod build {
#[derive(Debug, thiserror::Error)]
pub enum Error {
- #[error("Python threw an exception")]
- Python,
+ #[error("Python threw an exception: {0}")]
+ Python(String),
#[error("Io error: {0}")]
Io(#[from] std::io::Error),
@@ -539,3 +538,84 @@ pub fn json_dumps(
_ => unreachable!("These should not be json.dumps output"),
}
}
+
+// Inlined and changed from `vm.write_exception_inner`
+fn write_exception<W: Write>(
+ vm: &VirtualMachine,
+ output: &mut W,
+ exc: &PyBaseExceptionRef,
+) -> Result<(), W::Error> {
+ let varargs = exc.args();
+ let args_repr = {
+ match varargs.len() {
+ 0 => vec![],
+ 1 => {
+ let args0_repr = if true {
+ varargs[0]
+ .str(vm)
+ .unwrap_or_else(|_| PyStr::from("<element str() failed>").into_ref(&vm.ctx))
+ } else {
+ varargs[0].repr(vm).unwrap_or_else(|_| {
+ PyStr::from("<element repr() failed>").into_ref(&vm.ctx)
+ })
+ };
+ vec![args0_repr]
+ }
+ _ => varargs
+ .iter()
+ .map(|vararg| {
+ vararg.repr(vm).unwrap_or_else(|_| {
+ PyStr::from("<element repr() failed>").into_ref(&vm.ctx)
+ })
+ })
+ .collect(),
+ }
+ };
+
+ let exc_class = exc.class();
+
+ if exc_class.fast_issubclass(vm.ctx.exceptions.syntax_error) {
+ unreachable!(
+ "A syntax error should never be raised, \
+ as yt_dlp should not have them and neither our embedded code"
+ );
+ }
+
+ let exc_name = exc_class.name();
+ match args_repr.len() {
+ 0 => write!(output, "{exc_name}"),
+ 1 => write!(output, "{}: {}", exc_name, args_repr[0]),
+ _ => write!(
+ output,
+ "{}: ({})",
+ exc_name,
+ args_repr
+ .iter()
+ .map(|val| val.as_str())
+ .collect::<Vec<_>>()
+ .join(", "),
+ ),
+ }?;
+
+ match offer_suggestions(exc, vm) {
+ Some(suggestions) => {
+ write!(output, ". Did you mean: '{suggestions}'?")
+ }
+ None => Ok(()),
+ }
+}
+
+fn process_exception(vm: &VirtualMachine, err: &PyBaseExceptionRef) -> String {
+ let mut buffer = String::new();
+ write_exception(vm, &mut buffer, err)
+ .expect("We are writing into an *in-memory* string, it will always work");
+
+ if log_enabled!(Level::Debug) {
+ let mut output = String::new();
+ vm.write_exception(&mut output, err)
+ .expect("We are writing into an *in-memory* string, it will always work");
+ debug!("Python threw an exception: {output}");
+ }
+
+ buffer
+}