about summary refs log tree commit diff stats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/yt/src/update/updater.rs36
-rw-r--r--crates/yt_dlp/src/lib.rs140
2 files changed, 123 insertions, 53 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;
-                    // }
-
-                    // 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}");
+                    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}");
+                    }
+
                     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
+}