about summary refs log tree commit diff stats
path: root/crates/yt_dlp/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/yt_dlp/src/lib.rs155
1 files changed, 110 insertions, 45 deletions
diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs
index 34b8a5d..dd42fc6 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,
@@ -313,30 +316,13 @@ impl YoutubeDL {
 
             let result_json = json_dumps(result, vm);
 
-            if let Ok(confirm) = env::var("YT_STORE_INFO_JSON") {
-                if confirm == "yes" {
-                    let mut file = File::create("output.info.json").unwrap();
-                    write!(
-                        file,
-                        "{}",
-                        serde_json::to_string_pretty(&serde_json::Value::Object(
-                            result_json.clone()
-                        ))
-                        .expect("Valid json")
-                    )
-                    .unwrap();
-                }
-            }
-
             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 +373,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 +472,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 +523,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
+}