about summary refs log tree commit diff stats
path: root/crates/yt_dlp
diff options
context:
space:
mode:
Diffstat (limited to 'crates/yt_dlp')
-rw-r--r--crates/yt_dlp/Cargo.toml22
-rw-r--r--crates/yt_dlp/README.md2
-rw-r--r--crates/yt_dlp/src/duration.rs78
-rw-r--r--crates/yt_dlp/src/info_json.rs60
-rw-r--r--crates/yt_dlp/src/lib.rs701
-rw-r--r--crates/yt_dlp/src/logging.rs123
-rw-r--r--crates/yt_dlp/src/options.rs286
-rw-r--r--crates/yt_dlp/src/package_hacks/mod.rs (renamed from crates/yt_dlp/src/wrapper/mod.rs)5
-rw-r--r--crates/yt_dlp/src/package_hacks/urllib3.rs35
-rw-r--r--crates/yt_dlp/src/package_hacks/urllib3_polyfill.py (renamed from crates/yt_dlp/.cargo/config.toml)7
-rw-r--r--crates/yt_dlp/src/post_processors/dearrow.rs184
-rw-r--r--crates/yt_dlp/src/post_processors/mod.rs123
-rw-r--r--crates/yt_dlp/src/progress_hook.rs54
-rw-r--r--crates/yt_dlp/src/python_error.rs116
-rw-r--r--crates/yt_dlp/src/tests.rs85
-rw-r--r--crates/yt_dlp/src/wrapper/info_json.rs556
-rw-r--r--crates/yt_dlp/src/wrapper/yt_dlp_options.rs62
17 files changed, 1241 insertions, 1258 deletions
diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml
index 1d34371..3632b23 100644
--- a/crates/yt_dlp/Cargo.toml
+++ b/crates/yt_dlp/Cargo.toml
@@ -10,7 +10,7 @@
 
 [package]
 name = "yt_dlp"
-description = "A wrapper around the python yt_dlp library"
+description = "A rust ffi wrapper library for the python yt_dlp library"
 keywords = []
 categories = []
 version.workspace = true
@@ -19,19 +19,25 @@ authors.workspace = true
 license.workspace = true
 repository.workspace = true
 rust-version.workspace = true
-publish = false
+publish = true
 
 [dependencies]
-pyo3 = { version = "0.23.3", features = ["auto-initialize"] }
-bytes.workspace = true
+curl = "0.4.48"
+indexmap = { version = "2.9.0", default-features = false }
 log.workspace = true
-serde.workspace = true
+rustpython = { git = "https://github.com/RustPython/RustPython.git", rev = "6a992d4f", features = [
+  "threading",
+  "stdlib",
+  "stdio",
+  "freeze-stdlib",
+  "importlib",
+  "ssl",
+], default-features = false }
+serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
+thiserror = "2.0.12"
 url.workspace = true
 
-[dev-dependencies]
-tokio.workspace = true
-
 [lints]
 workspace = true
 
diff --git a/crates/yt_dlp/README.md b/crates/yt_dlp/README.md
index 591ef2e..ece8540 100644
--- a/crates/yt_dlp/README.md
+++ b/crates/yt_dlp/README.md
@@ -12,7 +12,7 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 # Yt_py
 
-> \[can be empty\]
+> [can be empty]
 
 Some text about the project.
 
diff --git a/crates/yt_dlp/src/duration.rs b/crates/yt_dlp/src/duration.rs
deleted file mode 100644
index 19181a5..0000000
--- a/crates/yt_dlp/src/duration.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 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>.
-
-// TODO: This file should be de-duplicated with the same file in the 'yt' crate <2024-06-25>
-
-#[derive(Debug, Clone, Copy)]
-pub struct Duration {
-    time: u32,
-}
-
-impl From<&str> for Duration {
-    fn from(v: &str) -> Self {
-        let buf: Vec<_> = v.split(':').take(2).collect();
-        Self {
-            time: (buf[0].parse::<u32>().expect("Should be a number") * 60)
-                + buf[1].parse::<u32>().expect("Should be a number"),
-        }
-    }
-}
-
-impl From<Option<f64>> for Duration {
-    fn from(value: Option<f64>) -> Self {
-        Self {
-            #[allow(
-                clippy::cast_possible_truncation,
-                clippy::cast_precision_loss,
-                clippy::cast_sign_loss
-            )]
-            time: value.unwrap_or(0.0).ceil() as u32,
-        }
-    }
-}
-
-impl std::fmt::Display for Duration {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        const SECOND: u32 = 1;
-        const MINUTE: u32 = 60 * SECOND;
-        const HOUR: u32 = 60 * MINUTE;
-
-        let base_hour = self.time - (self.time % HOUR);
-        let base_min = (self.time % HOUR) - ((self.time % HOUR) % MINUTE);
-        let base_sec = (self.time % HOUR) % MINUTE;
-
-        let h = base_hour / HOUR;
-        let m = base_min / MINUTE;
-        let s = base_sec / SECOND;
-
-        if self.time == 0 {
-            write!(f, "0s")
-        } else if h > 0 {
-            write!(f, "{h}h {m}m")
-        } else {
-            write!(f, "{m}m {s}s")
-        }
-    }
-}
-#[cfg(test)]
-mod test {
-    use super::Duration;
-
-    #[test]
-    fn test_display_duration_1h() {
-        let dur = Duration { time: 60 * 60 };
-        assert_eq!("1h 0m".to_owned(), dur.to_string());
-    }
-    #[test]
-    fn test_display_duration_30min() {
-        let dur = Duration { time: 60 * 30 };
-        assert_eq!("30m 0s".to_owned(), dur.to_string());
-    }
-}
diff --git a/crates/yt_dlp/src/info_json.rs b/crates/yt_dlp/src/info_json.rs
new file mode 100644
index 0000000..31f4a69
--- /dev/null
+++ b/crates/yt_dlp/src/info_json.rs
@@ -0,0 +1,60 @@
+// 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::{
+    PyRef, VirtualMachine,
+    builtins::{PyDict, PyStr},
+};
+
+pub type InfoJson = serde_json::Map<String, serde_json::Value>;
+
+pub fn json_loads(
+    input: serde_json::Map<String, serde_json::Value>,
+    vm: &VirtualMachine,
+) -> PyRef<PyDict> {
+    let json = vm.import("json", 0).expect("Module exists");
+    let loads = json.get_attr("loads", vm).expect("Method exists");
+    let self_str = serde_json::to_string(&serde_json::Value::Object(input)).expect("Vaild json");
+    let dict = loads
+        .call((self_str,), vm)
+        .expect("Vaild json is always a valid dict");
+
+    dict.downcast().expect("Should always be a dict")
+}
+
+/// # Panics
+/// If expectation about python operations fail.
+pub fn json_dumps(
+    input: PyRef<PyDict>,
+    vm: &VirtualMachine,
+) -> serde_json::Map<String, serde_json::Value> {
+    let json = vm.import("json", 0).expect("Module exists");
+    let dumps = json.get_attr("dumps", vm).expect("Method exists");
+    let dict = dumps
+        .call((input,), vm)
+        .map_err(|err| vm.print_exception(err))
+        .expect("Might not always work, but for our dicts it works");
+
+    let string: PyRef<PyStr> = dict.downcast().expect("Should always be a string");
+
+    let real_string = string.to_str().expect("Should be valid utf8");
+
+    // {
+    //     let mut file = File::create("debug.dump.json").unwrap();
+    //     write!(file, "{}", real_string).unwrap();
+    // }
+
+    let value: serde_json::Value = serde_json::from_str(real_string).expect("Should be valid json");
+
+    match value {
+        serde_json::Value::Object(map) => map,
+        _ => unreachable!("These should not be json.dumps output"),
+    }
+}
diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs
index 970bfe2..a03e444 100644
--- a/crates/yt_dlp/src/lib.rs
+++ b/crates/yt_dlp/src/lib.rs
@@ -1,6 +1,6 @@
 // yt - A fully featured command line YouTube client
 //
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // SPDX-License-Identifier: GPL-3.0-or-later
 //
 // This file is part of Yt.
@@ -8,461 +8,322 @@
 // 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>.
 
-// The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint.
-#![allow(unsafe_op_in_unsafe_fn)]
-#![allow(clippy::missing_errors_doc)]
+//! The `yt_dlp` interface is completely contained in the [`YoutubeDL`] structure.
 
-use std::env;
-use std::io::stdout;
-use std::{fs::File, io::Write};
+use std::path::PathBuf;
 
-use std::{path::PathBuf, sync::Once};
-
-use crate::{duration::Duration, logging::setup_logging, wrapper::info_json::InfoJson};
-
-use bytes::Bytes;
-use log::{info, log_enabled, Level};
-use pyo3::types::{PyString, PyTuple, PyTupleMethods};
-use pyo3::{
-    pyfunction,
-    types::{PyAnyMethods, PyDict, PyDictMethods, PyList, PyListMethods, PyModule},
-    wrap_pyfunction, Bound, PyAny, PyResult, Python,
+use indexmap::IndexMap;
+use log::info;
+use rustpython::vm::{
+    Interpreter, PyObjectRef, PyRef, VirtualMachine,
+    builtins::{PyDict, PyList, PyStr},
+    function::{FuncArgs, KwArgs, PosArgs},
 };
-use serde::Serialize;
-use serde_json::{Map, Value};
 use url::Url;
 
-pub mod duration;
-pub mod logging;
-pub mod wrapper;
-
-#[cfg(test)]
-mod tests;
-
-/// Synchronisation helper, to ensure that we don't setup the logger multiple times
-static SYNC_OBJ: Once = Once::new();
-
-/// Add a logger to the yt-dlp options.
-/// If you have an logger set (i.e. for rust), than this will log to rust
-///
-/// # Panics
-/// This should never panic.
-pub fn add_logger_and_sig_handler<'a>(
-    opts: Bound<'a, PyDict>,
-    py: Python<'_>,
-) -> PyResult<Bound<'a, PyDict>> {
-    setup_logging(py, "yt_dlp")?;
-
-    let logging = PyModule::import(py, "logging")?;
-    let ytdl_logger = logging.call_method1("getLogger", ("yt_dlp",))?;
-
-    // Ensure that all events are logged by setting the log level to NOTSET (we filter on rust's side)
-    // Also use this static, to ensure that we don't configure the logger every time
-    SYNC_OBJ.call_once(|| {
-        // Disable the SIGINT (Ctrl+C) handler, python installs.
-        // This allows the user to actually stop the application with Ctrl+C.
-        // This is here because it can only be run in the main thread and this was here already.
-        py.run(
-            c"\
-import signal
-signal.signal(signal.SIGINT, signal.SIG_DFL)",
-            None,
-            None,
-        )
-        .expect("This code should always work");
-
-        let config_opts = PyDict::new(py);
-        config_opts
-            .set_item("level", 0)
-            .expect("Setting this item should always work");
-
-        logging
-            .call_method("basicConfig", (), Some(&config_opts))
-            .expect("This method exists");
-    });
-
-    // This was taken from `ytcc`, I don't think it is still applicable
-    // ytdl_logger.setattr("propagate", false)?;
-    // let logging_null_handler = logging.call_method0("NullHandler")?;
-    // ytdl_logger.setattr("addHandler", logging_null_handler)?;
-
-    opts.set_item("logger", ytdl_logger).expect("Should work");
-
-    Ok(opts)
+use crate::{
+    info_json::{InfoJson, json_dumps, json_loads},
+    python_error::PythonError,
+};
+
+pub mod info_json;
+pub mod options;
+pub mod post_processors;
+pub mod progress_hook;
+pub mod python_error;
+
+mod logging;
+mod package_hacks;
+
+#[macro_export]
+macro_rules! json_get {
+    ($value:expr, $name:literal, $into:ident) => {{
+        match $value.get($name) {
+            Some(val) => $crate::json_cast!(val, $into),
+            None => panic!(
+                concat!(
+                    "Expected '",
+                    $name,
+                    "' to be a key for the'",
+                    stringify!($value),
+                    "' object: {:#?}"
+                ),
+                $value
+            ),
+        }
+    }};
 }
 
-#[pyfunction]
-#[allow(clippy::too_many_lines)]
-#[allow(clippy::missing_panics_doc)]
-#[allow(clippy::items_after_statements)]
-#[allow(
-    clippy::cast_possible_truncation,
-    clippy::cast_sign_loss,
-    clippy::cast_precision_loss
-)]
-pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()> {
-    // Only add the handler, if the log-level is higher than Debug (this avoids covering debug
-    // messages).
-    if log_enabled!(Level::Debug) {
-        return Ok(());
-    }
+#[macro_export]
+macro_rules! json_cast {
+    ($value:expr, $into:ident) => {{
+        match $value.$into() {
+            Some(result) => result,
+            None => panic!(
+                concat!(
+                    "Expected to be able to cast value ({:#?}) ",
+                    stringify!($into)
+                ),
+                $value
+            ),
+        }
+    }};
+}
 
-    // ANSI ESCAPE CODES Wrappers {{{
-    // see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
-    const CSI: &str = "\x1b[";
-    fn clear_whole_line() {
-        print!("{CSI}2K");
-    }
-    fn move_to_col(x: usize) {
-        print!("{CSI}{x}G");
-    }
-    // }}}
-
-    let input: Map<String, Value> = serde_json::from_str(&json_dumps(
-        py,
-        input
-            .downcast::<PyAny>()
-            .expect("Will always work")
-            .to_owned(),
-    )?)
-    .expect("Python should always produce valid json");
-
-    macro_rules! get {
-        (@interrogate $item:ident, $type_fun:ident, $get_fun:ident, $name:expr) => {{
-            let a = $item.get($name).expect(concat!(
-                "The field '",
-                stringify!($name),
-                "' should exist."
-            ));
-
-            if a.$type_fun() {
-                a.$get_fun().expect(
-                    "The should have been checked in the if guard, so unpacking here is fine",
-                )
-            } else {
-                panic!(
-                    "Value {} => \n{}\n is not of type: {}",
-                    $name,
-                    a,
-                    stringify!($type_fun)
-                );
-            }
-        }};
+/// The core of the `yt_dlp` interface.
+pub struct YoutubeDL {
+    interpreter: Interpreter,
+    youtube_dl_class: PyObjectRef,
+    yt_dlp_module: PyObjectRef,
+    options: serde_json::Map<String, serde_json::Value>,
+}
 
-        ($type_fun:ident, $get_fun:ident, $name1:expr, $name2:expr) => {{
-            let a = get! {@interrogate input, is_object, as_object, $name1};
-            let b = get! {@interrogate a, $type_fun, $get_fun, $name2};
-            b
-        }};
+impl std::fmt::Debug for YoutubeDL {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        // TODO(@bpeetz): Use something useful here. <2025-06-13>
+        f.write_str("YoutubeDL")
+    }
+}
 
-        ($type_fun:ident, $get_fun:ident, $name:expr) => {{
-            get! {@interrogate input, $type_fun, $get_fun, $name}
-        }};
+impl YoutubeDL {
+    /// Fetch the underlying `yt_dlp` and `python` version.
+    ///
+    ///
+    /// # Panics
+    ///
+    /// If `yt_dlp` changed their location or type of `__version__`.
+    pub fn version(&self) -> (String, String) {
+        let yt_dlp: PyRef<PyStr> = self.interpreter.enter_and_expect(
+            |vm| {
+                let version_module = self.yt_dlp_module.get_attr("version", vm)?;
+                let version = version_module.get_attr("__version__", vm)?;
+                let version = version.downcast().expect("This should always be a string");
+                Ok(version)
+            },
+            "yt_dlp version location has changed",
+        );
+
+        let python: PyRef<PyStr> = self.interpreter.enter_and_expect(
+            |vm| {
+                let version_module = vm.import("sys", 0)?;
+                let version = version_module.get_attr("version", vm)?;
+                let version = version.downcast().expect("This should always be a string");
+                Ok(version)
+            },
+            "python version location has changed",
+        );
+
+        (yt_dlp.to_string(), python.to_string())
     }
 
-    macro_rules! default_get {
-        (@interrogate $item:ident, $default:expr, $get_fun:ident, $name:expr) => {{
-            let a = if let Some(field) = $item.get($name) {
-                field.$get_fun().unwrap_or($default)
+    /// Download a given list of URLs.
+    /// Returns the paths they were downloaded to.
+    ///
+    /// # Errors
+    /// If one of the downloads error.
+    pub fn download(&self, urls: &[Url]) -> Result<Vec<PathBuf>, extract_info::Error> {
+        let mut out_paths = Vec::with_capacity(urls.len());
+
+        for url in urls {
+            info!("Started downloading url: '{url}'");
+            let info_json = self.extract_info(url, true, true)?;
+
+            // Try to work around yt-dlp type weirdness
+            let result_string = if let Some(filename) = info_json.get("filename") {
+                PathBuf::from(json_cast!(filename, as_str))
             } else {
-                $default
+                PathBuf::from(json_get!(
+                    json_cast!(
+                        json_get!(info_json, "requested_downloads", as_array)[0],
+                        as_object
+                    ),
+                    "filename",
+                    as_str
+                ))
             };
-            a
-        }};
-
-        ($get_fun:ident, $default:expr, $name1:expr, $name2:expr) => {{
-            let a = get! {@interrogate input, is_object, as_object, $name1};
-            let b = default_get! {@interrogate a, $default, $get_fun, $name2};
-            b
-        }};
-
-        ($get_fun:ident, $default:expr, $name:expr) => {{
-            default_get! {@interrogate input, $default, $get_fun, $name}
-        }};
-    }
 
-    macro_rules! c {
-        ($color:expr, $format:expr) => {
-            format!("\x1b[{}m{}\x1b[0m", $color, $format)
-        };
-    }
-
-    fn format_bytes(bytes: u64) -> String {
-        let bytes = Bytes::new(bytes);
-        bytes.to_string()
-    }
+            out_paths.push(result_string);
+            info!("Finished downloading url");
+        }
 
-    fn format_speed(speed: f64) -> String {
-        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
-        let bytes = Bytes::new(speed.floor() as u64);
-        format!("{bytes}/s")
+        Ok(out_paths)
     }
 
-    let get_title = |add_extension: bool| -> String {
-        match get! {is_string, as_str, "info_dict", "ext"} {
-            "vtt" => {
-                format!(
-                    "Subtitles ({})",
-                    default_get! {as_str, "<No Subtitle Language>", "info_dict", "name"}
-                )
-            }
-            title_extension @ ("webm" | "mp4" | "m4a") => {
-                if add_extension {
-                    format!(
-                        "{} ({})",
-                        default_get! { as_str, "<No title>", "info_dict", "title"},
-                        title_extension
-                    )
+    /// `extract_info(self, url, download=True, ie_key=None, extra_info=None, process=True, force_generic_extractor=False)`
+    ///
+    /// Extract and return the information dictionary of the URL
+    ///
+    /// Arguments:
+    /// - `url`          URL to extract
+    ///
+    /// Keyword arguments:
+    /// :`download`     Whether to download videos
+    /// :`process`      Whether to resolve all unresolved references (URLs, playlist items).
+    ///                 Must be True for download to work
+    ///
+    /// # Panics
+    /// If expectations about python fail to hold.
+    ///
+    /// # Errors
+    /// If python operations fail.
+    pub fn extract_info(
+        &self,
+        url: &Url,
+        download: bool,
+        process: bool,
+    ) -> Result<InfoJson, extract_info::Error> {
+        self.interpreter.enter(|vm| {
+            let pos_args = PosArgs::new(vec![vm.new_pyobj(url.to_string())]);
+
+            let kw_args = KwArgs::new({
+                let mut map = IndexMap::new();
+                map.insert("download".to_owned(), vm.new_pyobj(download));
+                map.insert("process".to_owned(), vm.new_pyobj(process));
+                map
+            });
+
+            let fun_args = FuncArgs::new(pos_args, kw_args);
+
+            let inner = self
+                .youtube_dl_class
+                .get_attr("extract_info", vm)
+                .map_err(|exc| PythonError::from_exception(vm, &exc))?;
+            let result = inner
+                .call_with_args(fun_args, vm)
+                .map_err(|exc| PythonError::from_exception(vm, &exc))?
+                .downcast::<PyDict>()
+                .expect("This is a dict");
+
+            // Resolve the generator object
+            if let Ok(generator) = result.get_item("entries", vm) {
+                if generator.payload_is::<PyList>() {
+                    // already resolved. Do nothing
                 } else {
-                    default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned()
-                }
-            }
-            other => panic!("The extension '{other}' is not yet implemented"),
-        }
-    };
-
-    match get! {is_string, as_str, "status"} {
-        "downloading" => {
-            let elapsed = default_get! {as_f64, 0.0f64, "elapsed"};
-            let eta = default_get! {as_f64, 0.0, "eta"};
-            let speed = default_get! {as_f64, 0.0, "speed"};
-
-            let downloaded_bytes = get! {is_u64, as_u64, "downloaded_bytes"};
-            let (total_bytes, bytes_is_estimate): (u64, &'static str) = {
-                let total_bytes = default_get!(as_u64, 0, "total_bytes");
-                if total_bytes == 0 {
-                    let maybe_estimate = default_get!(as_u64, 0, "total_bytes_estimate");
-
-                    if maybe_estimate == 0 {
-                        // The download speed should be in bytes per second and the eta in seconds.
-                        // Thus multiplying them gets us the raw bytes (which were estimated by `yt_dlp`, from their `info.json`)
-                        let bytes_still_needed = (speed * eta).ceil() as u64;
-
-                        (downloaded_bytes + bytes_still_needed, "~")
-                    } else {
-                        (maybe_estimate, "~")
+                    let max_backlog = self.options.get("playlistend").map_or(10000, |value| {
+                        usize::try_from(value.as_u64().expect("Works")).expect("Should work")
+                    });
+
+                    let mut out = vec![];
+                    let next = generator
+                        .get_attr("__next__", vm)
+                        .map_err(|exc| PythonError::from_exception(vm, &exc))?;
+                    while let Ok(output) = next.call((), vm) {
+                        out.push(output);
+
+                        if out.len() == max_backlog {
+                            break;
+                        }
                     }
-                } else {
-                    (total_bytes, "")
+                    result
+                        .set_item("entries", vm.new_pyobj(out), vm)
+                        .map_err(|exc| PythonError::from_exception(vm, &exc))?;
                 }
-            };
-            let percent: f64 = {
-                if total_bytes == 0 {
-                    100.0
-                } else {
-                    (downloaded_bytes as f64 / total_bytes as f64) * 100.0
-                }
-            };
-
-            clear_whole_line();
-            move_to_col(1);
-
-            print!(
-                "'{}' [{}/{} at {}] -> [{} of {}{} {}] ",
-                c!("34;1", get_title(true)),
-                c!("33;1", Duration::from(Some(elapsed))),
-                c!("33;1", Duration::from(Some(eta))),
-                c!("32;1", format_speed(speed)),
-                c!("31;1", format_bytes(downloaded_bytes)),
-                c!("31;1", bytes_is_estimate),
-                c!("31;1", format_bytes(total_bytes)),
-                c!("36;1", format!("{:.02}%", percent))
-            );
-            stdout().flush()?;
-        }
-        "finished" => {
-            println!("-> Finished downloading.");
-        }
-        "error" => {
-            panic!("-> Error while downloading: {}", get_title(true))
-        }
-        other => unreachable!("'{other}' should not be a valid state!"),
-    };
-
-    Ok(())
-}
-
-pub fn add_hooks<'a>(opts: Bound<'a, PyDict>, py: Python<'_>) -> PyResult<Bound<'a, PyDict>> {
-    if let Some(hooks) = opts.get_item("progress_hooks")? {
-        let hooks = hooks.downcast::<PyList>()?;
-        hooks.append(wrap_pyfunction!(progress_hook, py)?)?;
-
-        opts.set_item("progress_hooks", hooks)?;
-    } else {
-        // No hooks are set yet
-        let hooks_list = PyList::new(py, &[wrap_pyfunction!(progress_hook, py)?])?;
-
-        opts.set_item("progress_hooks", hooks_list)?;
-    }
-
-    Ok(opts)
-}
-
-/// `extract_info(self, url, download=True, ie_key=None, extra_info=None, process=True, force_generic_extractor=False)`
-///
-/// Extract and return the information dictionary of the URL
-///
-/// Arguments:
-/// @param url          URL to extract
-///
-/// Keyword arguments:
-/// @param download     Whether to download videos
-/// @param process      Whether to resolve all unresolved references (URLs, playlist items).
-///                     Must be True for download to work
-/// @param `ie_key`       Use only the extractor with this key
-///
-/// @param `extra_info`   Dictionary containing the extra values to add to the info (For internal use only)
-/// @`force_generic_extractor`  Force using the generic extractor (Deprecated; use `ie_key`='Generic')
-#[allow(clippy::unused_async)]
-#[allow(clippy::missing_panics_doc)]
-pub async fn extract_info(
-    yt_dlp_opts: &Map<String, Value>,
-    url: &Url,
-    download: bool,
-    process: bool,
-) -> PyResult<InfoJson> {
-    Python::with_gil(|py| {
-        let opts = json_map_to_py_dict(yt_dlp_opts, py)?;
-
-        let instance = get_yt_dlp(py, opts)?;
-        let args = (url.as_str(),);
-
-        let kwargs = PyDict::new(py);
-        kwargs.set_item("download", download)?;
-        kwargs.set_item("process", process)?;
-
-        let result = instance.call_method("extract_info", args, Some(&kwargs))?;
-
-        // Remove the `<generator at 0xsome_hex>`, by setting it to null
-        if !process {
-            result.set_item("entries", ())?;
-        }
-
-        let result_str = json_dumps(py, result)?;
-
-        if let Ok(confirm) = env::var("YT_STORE_INFO_JSON") {
-            if confirm == "yes" {
-                let mut file = File::create("output.info.json")?;
-                write!(file, "{result_str}").unwrap();
             }
-        }
 
-        Ok(serde_json::from_str(&result_str)
-            .expect("Python should be able to produce correct json"))
-    })
-}
-
-/// # Panics
-/// Only if python fails to return a valid URL.
-pub fn unsmuggle_url(smug_url: &Url) -> PyResult<Url> {
-    Python::with_gil(|py| {
-        let utils = get_yt_dlp_utils(py)?;
-        let url = utils
-            .call_method1("unsmuggle_url", (smug_url.as_str(),))?
-            .downcast::<PyTuple>()?
-            .get_item(0)?;
-
-        let url: Url = url
-            .downcast::<PyString>()?
-            .to_string()
-            .parse()
-            .expect("Python should be able to return a valid url");
-
-        Ok(url)
-    })
-}
+            let result = self.prepare_info_json(result, vm)?;
 
-/// Download a given list of URLs.
-/// Returns the paths they were downloaded to.
-///
-/// # Panics
-/// Only if `yt_dlp` changes their `info_json` schema.
-pub async fn download(
-    urls: &[Url],
-    download_options: &Map<String, Value>,
-) -> PyResult<Vec<PathBuf>> {
-    let mut out_paths = Vec::with_capacity(urls.len());
-
-    for url in urls {
-        info!("Started downloading url: '{}'", url);
-        let info_json = extract_info(download_options, url, true, true).await?;
-
-        // Try to work around yt-dlp type weirdness
-        let result_string = if let Some(filename) = info_json.filename {
-            filename
-        } else {
-            info_json.requested_downloads.expect("This must exist")[0]
-                .filename
-                .clone()
-        };
-
-        out_paths.push(result_string);
-        info!("Finished downloading url: '{}'", url);
+            Ok(result)
+        })
     }
 
-    Ok(out_paths)
-}
-
-fn json_map_to_py_dict<'a>(
-    map: &Map<String, Value>,
-    py: Python<'a>,
-) -> PyResult<Bound<'a, PyDict>> {
-    let json_string = serde_json::to_string(&map).expect("This must always work");
-
-    let python_dict = json_loads(py, json_string)?;
-
-    Ok(python_dict)
-}
-
-fn json_dumps(py: Python<'_>, input: Bound<'_, PyAny>) -> PyResult<String> {
-    //     json.dumps(yt_dlp.sanitize_info(input))
-
-    let yt_dlp = get_yt_dlp(py, PyDict::new(py))?;
-    let sanitized_result = yt_dlp.call_method1("sanitize_info", (input,))?;
-
-    let json = PyModule::import(py, "json")?;
-    let dumps = json.getattr("dumps")?;
+    /// Take the (potentially modified) result of the information extractor (i.e.,
+    /// [`Self::extract_info`] with `process` and `download` set to false)
+    /// and resolve all unresolved references (URLs,
+    /// playlist items).
+    ///
+    /// It will also download the videos if 'download' is true.
+    /// Returns the resolved `ie_result`.
+    ///
+    /// # Panics
+    /// If expectations about python fail to hold.
+    ///
+    /// # Errors
+    /// If python operations fail.
+    pub fn process_ie_result(
+        &self,
+        ie_result: InfoJson,
+        download: bool,
+    ) -> Result<InfoJson, process_ie_result::Error> {
+        self.interpreter.enter(|vm| {
+            let pos_args = PosArgs::new(vec![vm.new_pyobj(json_loads(ie_result, vm))]);
+
+            let kw_args = KwArgs::new({
+                let mut map = IndexMap::new();
+                map.insert("download".to_owned(), vm.new_pyobj(download));
+                map
+            });
+
+            let fun_args = FuncArgs::new(pos_args, kw_args);
+
+            let inner = self
+                .youtube_dl_class
+                .get_attr("process_ie_result", vm)
+                .map_err(|exc| PythonError::from_exception(vm, &exc))?;
+            let result = inner
+                .call_with_args(fun_args, vm)
+                .map_err(|exc| PythonError::from_exception(vm, &exc))?
+                .downcast::<PyDict>()
+                .expect("This is a dict");
+
+            let result = self.prepare_info_json(result, vm)?;
+
+            Ok(result)
+        })
+    }
 
-    let output = dumps.call1((sanitized_result,))?;
+    fn prepare_info_json(
+        &self,
+        info: PyRef<PyDict>,
+        vm: &VirtualMachine,
+    ) -> Result<InfoJson, prepare::Error> {
+        let sanitize = self
+            .youtube_dl_class
+            .get_attr("sanitize_info", vm)
+            .map_err(|exc| PythonError::from_exception(vm, &exc))?;
 
-    let output_str = output.extract::<String>()?;
+        let value = sanitize
+            .call((info,), vm)
+            .map_err(|exc| PythonError::from_exception(vm, &exc))?;
 
-    Ok(output_str)
-}
+        let result = value.downcast::<PyDict>().expect("This should stay a dict");
 
-fn json_loads_str<T: Serialize>(py: Python<'_>, input: T) -> PyResult<Bound<'_, PyDict>> {
-    let string = serde_json::to_string(&input).expect("Correct json must be pased");
-
-    json_loads(py, string)
+        Ok(json_dumps(result, vm))
+    }
 }
 
-fn json_loads(py: Python<'_>, input: String) -> PyResult<Bound<'_, PyDict>> {
-    //     json.loads(input)
+#[allow(missing_docs)]
+pub mod process_ie_result {
+    use crate::{prepare, python_error::PythonError};
 
-    let json = PyModule::import(py, "json")?;
-    let dumps = json.getattr("loads")?;
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
 
-    let output = dumps.call1((input,))?;
-
-    Ok(output
-        .downcast::<PyDict>()
-        .expect("This should always be a PyDict")
-        .clone())
+        #[error("Failed to prepare the info json")]
+        InfoJsonPrepare(#[from] prepare::Error),
+    }
 }
+#[allow(missing_docs)]
+pub mod extract_info {
+    use crate::{prepare, python_error::PythonError};
 
-fn get_yt_dlp_utils(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
-    let yt_dlp = PyModule::import(py, "yt_dlp")?;
-    let utils = yt_dlp.getattr("utils")?;
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
 
-    Ok(utils)
+        #[error("Failed to prepare the info json")]
+        InfoJsonPrepare(#[from] prepare::Error),
+    }
 }
-fn get_yt_dlp<'a>(py: Python<'a>, opts: Bound<'a, PyDict>) -> PyResult<Bound<'a, PyAny>> {
-    // Unconditionally set a logger
-    let opts = add_logger_and_sig_handler(opts, py)?;
-    let opts = add_hooks(opts, py)?;
-
-    let yt_dlp = PyModule::import(py, "yt_dlp")?;
-    let youtube_dl = yt_dlp.call_method1("YoutubeDL", (opts,))?;
-
-    Ok(youtube_dl)
+#[allow(missing_docs)]
+pub mod prepare {
+    use crate::python_error::PythonError;
+
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
+    }
 }
diff --git a/crates/yt_dlp/src/logging.rs b/crates/yt_dlp/src/logging.rs
index 670fc1c..112836e 100644
--- a/crates/yt_dlp/src/logging.rs
+++ b/crates/yt_dlp/src/logging.rs
@@ -10,33 +10,66 @@
 
 // This file is taken from: https://github.com/dylanbstorey/pyo3-pylogger/blob/d89e0d6820ebc4f067647e3b74af59dbc4941dd5/src/lib.rs
 // It is licensed under the Apache 2.0 License, copyright up to 2024 by Dylan Storey
-// It was modified by Benedikt Peetz 2024
+// It was modified by Benedikt Peetz 2024, 2025
+
+use log::{Level, MetadataBuilder, Record, logger};
+use rustpython::vm::{
+    PyObjectRef, PyRef, PyResult, VirtualMachine,
+    builtins::{PyInt, PyStr},
+    convert::ToPyObject,
+    function::FuncArgs,
+};
 
-// The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint.
-#![allow(unsafe_op_in_unsafe_fn)]
+/// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead.
+fn host_log(mut input: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
+    let record = input.args.remove(0);
+    let rust_target = {
+        let base: PyRef<PyStr> = input.args.remove(0).downcast().expect("Should be a string");
+        base.as_str().to_owned()
+    };
 
-use std::ffi::CString;
+    let level = {
+        let level: PyRef<PyInt> = record
+            .get_attr("levelno", vm)?
+            .downcast()
+            .expect("Should always be an int");
+        level.as_u32_mask()
+    };
+    let message = {
+        let get_message = record.get_attr("getMessage", vm)?;
+        let message: PyRef<PyStr> = get_message
+            .call((), vm)?
+            .downcast()
+            .expect("Downcasting works");
+
+        message.as_str().to_owned()
+    };
 
-use log::{logger, Level, MetadataBuilder, Record};
-use pyo3::{
-    prelude::{PyAnyMethods, PyListMethods, PyModuleMethods},
-    pyfunction, wrap_pyfunction, Bound, PyAny, PyResult, Python,
-};
+    let pathname = {
+        let pathname: PyRef<PyStr> = record
+            .get_attr("pathname", vm)?
+            .downcast()
+            .expect("Is a string");
 
-/// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead.
-#[allow(clippy::needless_pass_by_value)]
-#[pyfunction]
-fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> {
-    let level = record.getattr("levelno")?;
-    let message = record.getattr("getMessage")?.call0()?.to_string();
-    let pathname = record.getattr("pathname")?.to_string();
-    let lineno = record
-        .getattr("lineno")?
-        .to_string()
-        .parse::<u32>()
-        .expect("This should always be a u32");
-
-    let logger_name = record.getattr("name")?.to_string();
+        pathname.as_str().to_owned()
+    };
+
+    let lineno = {
+        let lineno: PyRef<PyInt> = record
+            .get_attr("lineno", vm)?
+            .downcast()
+            .expect("Is a number");
+
+        lineno.as_u32_mask()
+    };
+
+    let logger_name = {
+        let name: PyRef<PyStr> = record
+            .get_attr("name", vm)?
+            .downcast()
+            .expect("Should be a string");
+        name.as_str().to_owned()
+    };
 
     let full_target: Option<String> = if logger_name.trim().is_empty() || logger_name == "root" {
         None
@@ -47,25 +80,25 @@ fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> {
         Some(format!("{rust_target}::{logger_name}"))
     };
 
-    let target = full_target.as_deref().unwrap_or(rust_target);
+    let target = full_target.as_deref().unwrap_or(&rust_target);
 
     // error
-    let error_metadata = if level.ge(40u8)? {
+    let error_metadata = if level >= 40 {
         MetadataBuilder::new()
             .target(target)
             .level(Level::Error)
             .build()
-    } else if level.ge(30u8)? {
+    } else if level >= 30 {
         MetadataBuilder::new()
             .target(target)
             .level(Level::Warn)
             .build()
-    } else if level.ge(20u8)? {
+    } else if level >= 20 {
         MetadataBuilder::new()
             .target(target)
             .level(Level::Info)
             .build()
-    } else if level.ge(10u8)? {
+    } else if level >= 10 {
         MetadataBuilder::new()
             .target(target)
             .level(Level::Debug)
@@ -97,13 +130,24 @@ fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> {
 /// # Panics
 /// Only if internal assertions fail.
 #[allow(clippy::module_name_repetitions)]
-pub fn setup_logging(py: Python<'_>, target: &str) -> PyResult<()> {
-    let logging = py.import("logging")?;
+pub(super) fn setup_logging(vm: &VirtualMachine, target: &str) -> PyResult<PyObjectRef> {
+    let logging = vm.import("logging", 0)?;
 
-    logging.setattr("host_log", wrap_pyfunction!(host_log, &logging)?)?;
+    let scope = vm.new_scope_with_builtins();
 
-    py.run(
-        CString::new(format!(
+    for (key, value) in logging.dict().expect("Should be a dict") {
+        let key: PyRef<PyStr> = key.downcast().expect("Is a string");
+
+        scope.globals.set_item(key.as_str(), value, vm)?;
+    }
+    scope
+        .globals
+        .set_item("host_log", vm.new_function("host_log", host_log).into(), vm)?;
+
+    let local_scope = scope.clone();
+    vm.run_code_string(
+        local_scope,
+        format!(
             r#"
 class HostHandler(Handler):
     def __init__(self, level=0):
@@ -118,15 +162,10 @@ def basicConfig(*pargs, **kwargs):
         kwargs["handlers"] = [HostHandler()]
     return oldBasicConfig(*pargs, **kwargs)
 "#
-        ))
-        .expect("This is hardcoded")
-        .as_c_str(),
-        Some(&logging.dict()),
-        None,
+        )
+        .as_str(),
+        "<embedded logging inintializing code>".to_owned(),
     )?;
 
-    let all = logging.index()?;
-    all.append("HostHandler")?;
-
-    Ok(())
+    Ok(scope.globals.to_pyobject(vm))
 }
diff --git a/crates/yt_dlp/src/options.rs b/crates/yt_dlp/src/options.rs
new file mode 100644
index 0000000..dc3c154
--- /dev/null
+++ b/crates/yt_dlp/src/options.rs
@@ -0,0 +1,286 @@
+// 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 std::env;
+
+use indexmap::IndexMap;
+use log::{Level, debug, error, log_enabled};
+use rustpython::{
+    InterpreterConfig,
+    vm::{
+        self, PyObjectRef, PyRef, PyResult, VirtualMachine,
+        builtins::{PyBaseException, PyStr},
+        function::{FuncArgs, KwArgs, PosArgs},
+    },
+};
+
+use crate::{
+    YoutubeDL, json_loads, logging::setup_logging, package_hacks, post_processors,
+    python_error::process_exception,
+};
+
+/// Wrap your function with [`mk_python_function`].
+pub type ProgressHookFunction = fn(input: FuncArgs, vm: &VirtualMachine);
+
+pub type PostProcessorFunction = fn(vm: &VirtualMachine) -> PyResult<PyObjectRef>;
+
+/// Options, that are used to customize the download behaviour.
+///
+/// In the future, this might get a Builder api.
+///
+/// See `help(yt_dlp.YoutubeDL())` from python for a full list of available options.
+#[derive(Default, Debug)]
+pub struct YoutubeDLOptions {
+    options: serde_json::Map<String, serde_json::Value>,
+    progress_hook: Option<ProgressHookFunction>,
+    post_processors: Vec<PostProcessorFunction>,
+}
+
+impl YoutubeDLOptions {
+    #[must_use]
+    pub fn new() -> Self {
+        let me = Self {
+            options: serde_json::Map::new(),
+            progress_hook: None,
+            post_processors: vec![],
+        };
+
+        me.with_post_processor(post_processors::dearrow::process)
+    }
+
+    #[must_use]
+    pub fn set(self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
+        let mut options = self.options;
+        options.insert(key.into(), value.into());
+
+        Self { options, ..self }
+    }
+
+    #[must_use]
+    pub fn with_progress_hook(self, progress_hook: ProgressHookFunction) -> Self {
+        if let Some(_previous_hook) = self.progress_hook {
+            todo!()
+        } else {
+            Self {
+                progress_hook: Some(progress_hook),
+                ..self
+            }
+        }
+    }
+
+    #[must_use]
+    pub fn with_post_processor(mut self, pp: PostProcessorFunction) -> Self {
+        self.post_processors.push(pp);
+        self
+    }
+
+    /// # Errors
+    /// If the underlying [`YoutubeDL::from_options`] errors.
+    pub fn build(self) -> Result<YoutubeDL, build::Error> {
+        YoutubeDL::from_options(self)
+    }
+
+    #[must_use]
+    pub fn from_json_options(options: serde_json::Map<String, serde_json::Value>) -> Self {
+        Self {
+            options,
+            ..Self::new()
+        }
+    }
+
+    #[must_use]
+    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
+        self.options.get(key)
+    }
+}
+
+impl YoutubeDL {
+    /// Construct this instance from options.
+    ///
+    /// # Panics
+    /// If `yt_dlp` changed their interface.
+    ///
+    /// # Errors
+    /// If a python call fails.
+    #[allow(clippy::too_many_lines)]
+    pub fn from_options(options: YoutubeDLOptions) -> Result<Self, build::Error> {
+        let mut settings = vm::Settings::default();
+        if let Ok(python_path) = env::var("PYTHONPATH") {
+            for path in python_path.split(':') {
+                settings.path_list.push(path.to_owned());
+            }
+        } else {
+            error!(
+                "No PYTHONPATH found or invalid utf8. \
+                This means, that you probably did not \
+                supply a yt_dlp python package!"
+            );
+        }
+
+        settings.install_signal_handlers = false;
+
+        // NOTE(@bpeetz): Another value leads to an internal codegen error. <2025-06-13>
+        settings.optimize = 0;
+
+        settings.isolated = true;
+
+        let interpreter = InterpreterConfig::new()
+            .init_stdlib()
+            .settings(settings)
+            .interpreter();
+
+        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)?;
+
+            let opts = json_loads(options.options, vm);
+
+            {
+                // Setup the progress hook
+                if let Some(function) = options.progress_hook {
+                    opts.get_or_insert(vm, vm.new_pyobj("progress_hooks"), || {
+                        let hook: PyObjectRef = vm.new_function("progress_hook", function).into();
+                        vm.new_pyobj(vec![hook])
+                    })
+                    .expect("Should work?");
+                }
+            }
+
+            {
+                // Unconditionally set a logger.
+                // Otherwise, yt_dlp will log to stderr.
+
+                /// Is the specified record to be logged? Returns false for no,
+                /// true for yes. Filters can either modify log records in-place or
+                /// return a completely different record instance which will replace
+                /// the original log record in any future processing of the event.
+                fn filter_error_log(mut input: FuncArgs, vm: &VirtualMachine) -> bool {
+                    let record = input.args.remove(0);
+
+                    // Filter out all error logs (they are propagated as rust errors)
+                    let levelname: PyRef<PyStr> = record
+                        .get_attr("levelname", vm)
+                        .expect("This should exist")
+                        .downcast()
+                        .expect("This should be a String");
+
+                    let return_value = levelname.as_str() != "ERROR";
+
+                    if log_enabled!(Level::Debug) && !return_value {
+                        let message: String = {
+                            let get_message = record.get_attr("getMessage", vm).expect("Is set");
+                            let message: PyRef<PyStr> = get_message
+                                .call((), vm)
+                                .expect("Can be called")
+                                .downcast()
+                                .expect("Downcasting works");
+
+                            message.as_str().to_owned()
+                        };
+
+                        debug!("Swollowed error message: '{message}'");
+                    }
+                    return_value
+                }
+
+                let logging = setup_logging(vm, "yt_dlp")?;
+                let ytdl_logger = {
+                    let get_logger = logging.get_item("getLogger", vm)?;
+                    get_logger.call(("yt_dlp",), vm)?
+                };
+
+                {
+                    let args = FuncArgs::new(
+                        PosArgs::new(vec![]),
+                        KwArgs::new({
+                            let mut map = IndexMap::new();
+                            // Ensure that all events are logged by setting
+                            // the log level to NOTSET (we filter on rust's side)
+                            map.insert("level".to_owned(), vm.new_pyobj(0));
+                            map
+                        }),
+                    );
+
+                    let basic_config = logging.get_item("basicConfig", vm)?;
+                    basic_config.call(args, vm)?;
+                }
+
+                {
+                    let add_filter = ytdl_logger.get_attr("addFilter", vm)?;
+                    add_filter.call(
+                        (vm.new_function("yt_dlp_error_filter", filter_error_log),),
+                        vm,
+                    )?;
+                }
+
+                opts.set_item("logger", ytdl_logger, vm)?;
+            }
+
+            let youtube_dl_class = class.call((opts,), vm)?;
+
+            {
+                // Setup the post processors
+
+                let add_post_processor_fun = youtube_dl_class.get_attr("add_post_processor", vm)?;
+
+                for pp in options.post_processors {
+                    let args = {
+                        FuncArgs::new(
+                            PosArgs::new(vec![pp(vm)?]),
+                            KwArgs::new({
+                                let mut map = IndexMap::new();
+                                //  "when" can take any value in yt_dlp.utils.POSTPROCESS_WHEN
+                                map.insert("when".to_owned(), vm.new_pyobj("pre_process"));
+                                map
+                            }),
+                        )
+                    };
+
+                    add_post_processor_fun.call(args, vm)?;
+                }
+            }
+
+            Ok::<_, PyRef<PyBaseException>>((yt_dlp_module, youtube_dl_class))
+        }) {
+            Ok(ok) => Ok(ok),
+            Err(err) => {
+                // 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,
+            youtube_dl_class,
+            yt_dlp_module,
+            options: output_options,
+        })
+    }
+}
+
+#[allow(missing_docs)]
+pub mod build {
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error("Python threw an exception: {0}")]
+        Python(String),
+    }
+}
diff --git a/crates/yt_dlp/src/wrapper/mod.rs b/crates/yt_dlp/src/package_hacks/mod.rs
index 3fe3247..53fe323 100644
--- a/crates/yt_dlp/src/wrapper/mod.rs
+++ b/crates/yt_dlp/src/package_hacks/mod.rs
@@ -1,6 +1,6 @@
 // yt - A fully featured command line YouTube client
 //
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // SPDX-License-Identifier: GPL-3.0-or-later
 //
 // This file is part of Yt.
@@ -8,5 +8,4 @@
 // 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 mod info_json;
-// pub mod yt_dlp_options;
+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/.cargo/config.toml b/crates/yt_dlp/src/package_hacks/urllib3_polyfill.py
index d84f14d..610fd99 100644
--- a/crates/yt_dlp/.cargo/config.toml
+++ b/crates/yt_dlp/src/package_hacks/urllib3_polyfill.py
@@ -1,6 +1,6 @@
 # yt - A fully featured command line YouTube client
 #
-# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 # SPDX-License-Identifier: GPL-3.0-or-later
 #
 # This file is part of Yt.
@@ -8,5 +8,6 @@
 # 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>.
 
-[env]
-PYO3_PYTHON = "/nix/store/7xzk119acyws2c4ysygdv66l0grxkr39-python3-3.11.9-env/bin/python3"
+import ssl
+
+ssl.SSLContext.verify_flags = 0
diff --git a/crates/yt_dlp/src/post_processors/dearrow.rs b/crates/yt_dlp/src/post_processors/dearrow.rs
new file mode 100644
index 0000000..3cac745
--- /dev/null
+++ b/crates/yt_dlp/src/post_processors/dearrow.rs
@@ -0,0 +1,184 @@
+// 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 curl::easy::Easy;
+use log::{error, info, warn};
+use rustpython::vm::{
+    PyRef, VirtualMachine,
+    builtins::{PyDict, PyStr},
+};
+use serde::{Deserialize, Serialize};
+
+use crate::{pydict_cast, pydict_get, wrap_post_processor};
+
+wrap_post_processor!("DeArrow", unwrapped_process, process);
+
+/// # Errors
+/// If the API access fails.
+pub fn unwrapped_process(info: PyRef<PyDict>, vm: &VirtualMachine) -> Result<PyRef<PyDict>, Error> {
+    if pydict_get!(@vm, info, "extractor_key", PyStr).as_str() != "Youtube" {
+        warn!("DeArrow: Extractor did not match, exiting.");
+        return Ok(info);
+    }
+
+    let mut output: DeArrowApi = {
+        let output_bytes = {
+            let mut dst = Vec::new();
+
+            let mut easy = Easy::new();
+            easy.url(
+                format!(
+                    "https://sponsor.ajay.app/api/branding?videoID={}",
+                    pydict_get!(@vm, info, "id", PyStr).as_str()
+                )
+                .as_str(),
+            )?;
+
+            let mut transfer = easy.transfer();
+            transfer.write_function(|data| {
+                dst.extend_from_slice(data);
+                Ok(data.len())
+            })?;
+            transfer.perform()?;
+            drop(transfer);
+
+            dst
+        };
+
+        serde_json::from_slice(&output_bytes)?
+    };
+
+    // We pop the titles, so we need this vector reversed.
+    output.titles.reverse();
+
+    let title_len = output.titles.len();
+    let mut iterator = output.titles.clone();
+    let selected = loop {
+        let Some(title) = iterator.pop() else {
+            break false;
+        };
+
+        if (title.locked || title.votes < 1) && title_len > 1 {
+            info!(
+                "DeArrow: Skipping title {:#?}, as it is not good enough",
+                title.value
+            );
+            // Skip titles that are not “good” enough.
+            continue;
+        }
+
+        update_title(&info, &title.value, vm);
+
+        break true;
+    };
+
+    if !selected && title_len != 0 {
+        // No title was selected, even though we had some titles.
+        // Just pick the first one in this case.
+        update_title(&info, &output.titles[0].value, vm);
+    }
+
+    Ok(info)
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error("Failed to access the DeArrow api: {0}")]
+    Get(#[from] curl::Error),
+
+    #[error("Failed to deserialize a api json return object: {0}")]
+    Deserialize(#[from] serde_json::Error),
+}
+
+fn update_title(info: &PyRef<PyDict>, new_title: &str, vm: &VirtualMachine) {
+    assert!(!info.contains_key("original_title", vm));
+
+    if let Ok(old_title) = info.get_item("title", vm) {
+        warn!(
+            "DeArrow: Updating title from {:#?} to {:#?}",
+            pydict_cast!(@ref old_title, PyStr).as_str(),
+            new_title
+        );
+
+        info.set_item("original_title", old_title, vm)
+            .expect("We checked, it is a new key");
+    } else {
+        warn!("DeArrow: Setting title to {new_title:#?}");
+    }
+
+    let cleaned_title = {
+        // NOTE(@bpeetz): DeArrow uses `>` as a “Don't format the next word” mark.
+        // They should be removed, if one does not use a auto-formatter. <2025-06-16>
+        new_title.replace('>', "")
+    };
+
+    info.set_item("title", vm.new_pyobj(cleaned_title), vm)
+        .expect("This should work?");
+}
+
+#[derive(Serialize, Deserialize)]
+/// See: <https://wiki.sponsor.ajay.app/w/API_Docs/DeArrow>
+struct DeArrowApi {
+    titles: Vec<Title>,
+    thumbnails: Vec<Thumbnail>,
+
+    #[serde(alias = "randomTime")]
+    random_time: Option<f64>,
+
+    #[serde(alias = "videoDuration")]
+    video_duration: Option<f64>,
+
+    #[serde(alias = "casualVotes")]
+    casual_votes: Vec<CasualVote>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct CasualVote {
+    id: String,
+    count: u32,
+    title: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct Title {
+    /// Note: Titles will sometimes contain > before a word.
+    /// This tells the auto-formatter to not format a word.
+    /// If you have no auto-formatter, you can ignore this and replace it with an empty string
+    #[serde(alias = "title")]
+    value: String,
+
+    original: bool,
+    votes: u64,
+    locked: bool,
+
+    #[serde(alias = "UUID")]
+    uuid: String,
+
+    /// only present if requested
+    #[serde(alias = "userID")]
+    user_id: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Thumbnail {
+    // null if original is true
+    timestamp: Option<f64>,
+
+    original: bool,
+    votes: u64,
+    locked: bool,
+
+    #[serde(alias = "UUID")]
+    uuid: String,
+
+    /// only present if requested
+    #[serde(alias = "userID")]
+    user_id: Option<String>,
+}
diff --git a/crates/yt_dlp/src/post_processors/mod.rs b/crates/yt_dlp/src/post_processors/mod.rs
new file mode 100644
index 0000000..00b0ad5
--- /dev/null
+++ b/crates/yt_dlp/src/post_processors/mod.rs
@@ -0,0 +1,123 @@
+// 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 mod dearrow;
+
+#[macro_export]
+macro_rules! pydict_get {
+    (@$vm:expr, $value:expr, $name:literal, $into:ident) => {{
+        match $value.get_item($name, $vm) {
+            Ok(val) => $crate::pydict_cast!(val, $into),
+            Err(_) => panic!(
+                concat!(
+                    "Expected '",
+                    $name,
+                    "' to be a key for the'",
+                    stringify!($value),
+                    "' py dictionary: {:#?}"
+                ),
+                $value
+            ),
+        }
+    }};
+}
+
+#[macro_export]
+macro_rules! pydict_cast {
+    ($value:expr, $into:ident) => {{
+        match $value.downcast::<$into>() {
+            Ok(result) => result,
+            Err(val) => panic!(
+                concat!(
+                    "Expected to be able to downcast value ({:#?}) as ",
+                    stringify!($into)
+                ),
+                val
+            ),
+        }
+    }};
+    (@ref $value:expr, $into:ident) => {{
+        match $value.downcast_ref::<$into>() {
+            Some(result) => result,
+            None => panic!(
+                concat!(
+                    "Expected to be able to downcast value ({:#?}) as ",
+                    stringify!($into)
+                ),
+                $value
+            ),
+        }
+    }};
+}
+
+#[macro_export]
+macro_rules! wrap_post_processor {
+    ($name:literal, $unwrap:ident, $wrapped:ident) => {
+        use $crate::progress_hook::__priv::vm;
+
+        /// # Errors
+        /// - If the underlying function returns an error.
+        /// - If python operations fail.
+        pub fn $wrapped(vm: &vm::VirtualMachine) -> vm::PyResult<vm::PyObjectRef> {
+            fn actual_processor(
+                mut input: vm::function::FuncArgs,
+                vm: &vm::VirtualMachine,
+            ) -> vm::PyResult<vm::PyRef<vm::builtins::PyDict>> {
+                let input = input
+                    .args
+                    .remove(0)
+                    .downcast::<vm::builtins::PyDict>()
+                    .expect("Should be a py dict");
+
+                let output = match unwrapped_process(input, vm) {
+                    Ok(ok) => ok,
+                    Err(err) => {
+                        return Err(vm.new_runtime_error(err.to_string()));
+                    }
+                };
+
+                Ok(output)
+            }
+
+            let scope = vm.new_scope_with_builtins();
+
+            scope.globals.set_item(
+                "actual_processor",
+                vm.new_function("actual_processor", actual_processor).into(),
+                vm,
+            )?;
+
+            let local_scope = scope.clone();
+            vm.run_code_string(
+                local_scope,
+                format!(
+                    "
+import yt_dlp
+
+class {}(yt_dlp.postprocessor.PostProcessor):
+    def run(self, info):
+        info = actual_processor(info)
+        return [], info
+
+inst = {}()
+",
+                    $name, $name
+                )
+                .as_str(),
+                "<embedded post processor initializing code>".to_owned(),
+            )?;
+
+            Ok(scope
+                .globals
+                .get_item("inst", vm)
+                .expect("We just declared it"))
+        }
+    };
+}
diff --git a/crates/yt_dlp/src/progress_hook.rs b/crates/yt_dlp/src/progress_hook.rs
new file mode 100644
index 0000000..b42ae21
--- /dev/null
+++ b/crates/yt_dlp/src/progress_hook.rs
@@ -0,0 +1,54 @@
+// 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>.
+
+#[macro_export]
+macro_rules! mk_python_function {
+    ($name:ident, $new_name:ident) => {
+        pub fn $new_name(
+            mut args: $crate::progress_hook::__priv::vm::function::FuncArgs,
+            vm: &$crate::progress_hook::__priv::vm::VirtualMachine,
+        ) {
+            use $crate::progress_hook::__priv::vm;
+
+            let input = {
+                let dict: vm::PyRef<vm::builtins::PyDict> = args
+                    .args
+                    .remove(0)
+                    .downcast()
+                    .expect("The progress hook is always called with these args");
+                let new_dict = vm::builtins::PyDict::new_ref(&vm.ctx);
+                dict.into_iter()
+                    .filter_map(|(name, value)| {
+                        let real_name: vm::PyRefExact<vm::builtins::PyStr> =
+                            name.downcast_exact(vm).expect("Is a string");
+                        let name_str = real_name.to_str().expect("Is a string");
+                        if name_str.starts_with('_') {
+                            None
+                        } else {
+                            Some((name_str.to_owned(), value))
+                        }
+                    })
+                    .for_each(|(key, value)| {
+                        new_dict
+                            .set_item(&key, value, vm)
+                            .expect("This is a transpositions, should always be valid");
+                    });
+
+                $crate::progress_hook::__priv::json_dumps(new_dict, vm)
+            };
+            $name(input).expect("Shall not fail!");
+        }
+    };
+}
+
+pub mod __priv {
+    pub use crate::info_json::{json_dumps, json_loads};
+    pub use rustpython::vm;
+}
diff --git a/crates/yt_dlp/src/python_error.rs b/crates/yt_dlp/src/python_error.rs
new file mode 100644
index 0000000..9513956
--- /dev/null
+++ b/crates/yt_dlp/src/python_error.rs
@@ -0,0 +1,116 @@
+// 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 std::fmt::Display;
+
+use log::{Level, debug, log_enabled};
+use rustpython::vm::{
+    AsObject, PyPayload, PyRef, VirtualMachine,
+    builtins::{PyBaseException, PyBaseExceptionRef, PyStr},
+    py_io::Write,
+    suggestion::offer_suggestions,
+};
+
+#[derive(thiserror::Error, Debug)]
+pub struct PythonError(pub String);
+
+impl Display for PythonError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Python threw an exception: {}", self.0)
+    }
+}
+
+impl PythonError {
+    pub(super) fn from_exception(vm: &VirtualMachine, exc: &PyRef<PyBaseException>) -> Self {
+        let buffer = process_exception(vm, exc);
+        Self(buffer)
+    }
+}
+
+pub(super) 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
+}
+
+// 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(()),
+    }
+}
diff --git a/crates/yt_dlp/src/tests.rs b/crates/yt_dlp/src/tests.rs
deleted file mode 100644
index b48deb4..0000000
--- a/crates/yt_dlp/src/tests.rs
+++ /dev/null
@@ -1,85 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 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 std::sync::LazyLock;
-
-use serde_json::{json, Value};
-use url::Url;
-
-static YT_OPTS: LazyLock<serde_json::Map<String, Value>> = LazyLock::new(|| {
-    match json!({
-        "playliststart": 1,
-        "playlistend": 10,
-        "noplaylist": false,
-        "extract_flat": false,
-    }) {
-        Value::Object(obj) => obj,
-        _ => unreachable!("This json is hardcoded"),
-    }
-});
-
-#[tokio::test]
-async fn test_extract_info_video() {
-    let info = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://www.youtube.com/watch?v=dbjPnXaacAU").expect("Is valid."),
-        false,
-        false,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{info:#?}");
-}
-
-#[tokio::test]
-async fn test_extract_info_url() {
-    let err = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://google.com").expect("Is valid."),
-        false,
-        false,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{err:#?}");
-}
-
-#[tokio::test]
-async fn test_extract_info_playlist() {
-    let err = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://www.youtube.com/@TheGarriFrischer/videos").expect("Is valid."),
-        false,
-        true,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{err:#?}");
-}
-#[tokio::test]
-async fn test_extract_info_playlist_full() {
-    let err = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://www.youtube.com/@NixOS-Foundation/videos").expect("Is valid."),
-        false,
-        true,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{err:#?}");
-}
diff --git a/crates/yt_dlp/src/wrapper/info_json.rs b/crates/yt_dlp/src/wrapper/info_json.rs
deleted file mode 100644
index 35d155e..0000000
--- a/crates/yt_dlp/src/wrapper/info_json.rs
+++ /dev/null
@@ -1,556 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 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>.
-
-// `yt_dlp` named them like this.
-#![allow(clippy::pub_underscore_fields)]
-
-use std::{collections::HashMap, path::PathBuf};
-
-use pyo3::{types::PyDict, Bound, PyResult, Python};
-use serde::{Deserialize, Deserializer, Serialize};
-use serde_json::Value;
-use url::Url;
-
-use crate::json_loads_str;
-
-type Todo = String;
-type Extractor = String;
-type ExtractorKey = String;
-
-// TODO: Change this to map `_type` to a structure of values, instead of the options <2024-05-27>
-// And replace all the strings with better types (enums or urls)
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub struct InfoJson {
-    pub __files_to_move: Option<FilesToMove>,
-    pub __last_playlist_index: Option<u32>,
-    pub __post_extractor: Option<String>,
-    pub __x_forwarded_for_ip: Option<String>,
-    pub _filename: Option<PathBuf>,
-    pub _format_sort_fields: Option<Vec<String>>,
-    pub _has_drm: Option<Todo>,
-    pub _type: Option<InfoType>,
-    pub _version: Option<Version>,
-    pub abr: Option<f64>,
-    pub acodec: Option<String>,
-    pub age_limit: Option<u32>,
-    pub aspect_ratio: Option<f64>,
-    pub asr: Option<u32>,
-    pub audio_channels: Option<u32>,
-    pub audio_ext: Option<String>,
-    pub automatic_captions: Option<HashMap<String, Vec<Caption>>>,
-    pub availability: Option<String>,
-    pub average_rating: Option<String>,
-    pub categories: Option<Vec<String>>,
-    pub channel: Option<String>,
-    pub channel_follower_count: Option<u32>,
-    pub channel_id: Option<String>,
-    pub channel_is_verified: Option<bool>,
-    pub channel_url: Option<String>,
-    pub chapters: Option<Vec<Chapter>>,
-    pub comment_count: Option<u32>,
-    pub comments: Option<Vec<Comment>>,
-    pub concurrent_view_count: Option<u32>,
-    pub description: Option<String>,
-    pub display_id: Option<String>,
-    pub downloader_options: Option<DownloaderOptions>,
-    pub duration: Option<f64>,
-    pub duration_string: Option<String>,
-    pub dynamic_range: Option<String>,
-    pub entries: Option<Vec<InfoJson>>,
-    pub episode: Option<String>,
-    pub episode_number: Option<u32>,
-    pub epoch: Option<u32>,
-    pub ext: Option<String>,
-    pub extractor: Option<Extractor>,
-    pub extractor_key: Option<ExtractorKey>,
-    pub filename: Option<PathBuf>,
-    pub filesize: Option<u64>,
-    pub filesize_approx: Option<u64>,
-    pub format: Option<String>,
-    pub format_id: Option<String>,
-    pub format_index: Option<u32>,
-    pub format_note: Option<String>,
-    pub formats: Option<Vec<Format>>,
-    pub fps: Option<f64>,
-    pub fulltitle: Option<String>,
-    pub has_drm: Option<bool>,
-    pub heatmap: Option<Vec<HeatMapEntry>>,
-    pub height: Option<u32>,
-    pub http_headers: Option<HttpHeader>,
-    pub id: Option<String>,
-    pub ie_key: Option<ExtractorKey>,
-    pub is_live: Option<bool>,
-    pub language: Option<String>,
-    pub language_preference: Option<i32>,
-    pub license: Option<Todo>,
-    pub like_count: Option<u32>,
-    pub live_status: Option<String>,
-    pub location: Option<Todo>,
-    pub manifest_url: Option<Url>,
-    pub modified_date: Option<String>,
-    pub n_entries: Option<u32>,
-    pub original_url: Option<String>,
-    pub playable_in_embed: Option<bool>,
-    pub playlist: Option<Todo>,
-    pub playlist_autonumber: Option<u32>,
-    pub playlist_channel: Option<Todo>,
-    pub playlist_channel_id: Option<Todo>,
-    pub playlist_count: Option<u32>,
-    pub playlist_id: Option<Todo>,
-    pub playlist_index: Option<u64>,
-    pub playlist_title: Option<Todo>,
-    pub playlist_uploader: Option<Todo>,
-    pub playlist_uploader_id: Option<Todo>,
-    pub preference: Option<Todo>,
-    pub protocol: Option<String>,
-    pub quality: Option<f64>,
-    pub release_date: Option<String>,
-    pub release_timestamp: Option<u64>,
-    pub release_year: Option<u32>,
-    pub requested_downloads: Option<Vec<RequestedDownloads>>,
-    pub requested_entries: Option<Vec<u32>>,
-    pub requested_formats: Option<Vec<Format>>,
-    pub requested_subtitles: Option<HashMap<String, Subtitle>>,
-    pub resolution: Option<String>,
-    pub season: Option<String>,
-    pub season_number: Option<u32>,
-    pub series: Option<String>,
-    pub source_preference: Option<i32>,
-    pub sponsorblock_chapters: Option<Vec<SponsorblockChapter>>,
-    pub stretched_ratio: Option<Todo>,
-    pub subtitles: Option<HashMap<String, Vec<Caption>>>,
-    pub tags: Option<Vec<String>>,
-    pub tbr: Option<f64>,
-    pub thumbnail: Option<Url>,
-    pub thumbnails: Option<Vec<ThumbNail>>,
-    pub timestamp: Option<u64>,
-    pub title: Option<String>,
-    pub upload_date: Option<String>,
-    pub uploader: Option<String>,
-    pub uploader_id: Option<String>,
-    pub uploader_url: Option<String>,
-    pub url: Option<Url>,
-    pub vbr: Option<f64>,
-    pub vcodec: Option<String>,
-    pub video_ext: Option<String>,
-    pub view_count: Option<u32>,
-    pub was_live: Option<bool>,
-    pub webpage_url: Option<Url>,
-    pub webpage_url_basename: Option<String>,
-    pub webpage_url_domain: Option<String>,
-    pub width: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-#[allow(missing_copy_implementations)]
-pub struct FilesToMove {}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub struct RequestedDownloads {
-    pub __files_to_merge: Option<Vec<Todo>>,
-    pub __finaldir: PathBuf,
-    pub __infojson_filename: PathBuf,
-    pub __postprocessors: Vec<Todo>,
-    pub __real_download: bool,
-    pub __write_download_archive: bool,
-    pub _filename: PathBuf,
-    pub _type: InfoType,
-    pub _version: Version,
-    pub abr: f64,
-    pub acodec: String,
-    pub aspect_ratio: Option<f64>,
-    pub asr: Option<u32>,
-    pub audio_channels: Option<u32>,
-    pub audio_ext: Option<String>,
-    pub chapters: Option<Vec<SponsorblockChapter>>,
-    pub duration: Option<f64>,
-    pub dynamic_range: Option<String>,
-    pub ext: String,
-    pub filename: PathBuf,
-    pub filepath: PathBuf,
-    pub filesize_approx: Option<u64>,
-    pub format: String,
-    pub format_id: String,
-    pub format_note: String,
-    pub fps: Option<f64>,
-    pub has_drm: Option<bool>,
-    pub height: Option<u32>,
-    pub http_headers: Option<HttpHeader>,
-    pub infojson_filename: PathBuf,
-    pub language: Option<String>,
-    pub manifest_url: Option<Url>,
-    pub protocol: String,
-    pub requested_formats: Option<Vec<Format>>,
-    pub resolution: String,
-    pub tbr: f64,
-    pub url: Option<Url>,
-    pub vbr: f64,
-    pub vcodec: String,
-    pub video_ext: Option<String>,
-    pub width: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Subtitle {
-    pub ext: SubtitleExt,
-    pub filepath: PathBuf,
-    pub filesize: Option<u64>,
-    pub fragment_base_url: Option<Url>,
-    pub fragments: Option<Vec<Fragment>>,
-    pub manifest_url: Option<Url>,
-    pub name: Option<String>,
-    pub protocol: Option<Todo>,
-    pub url: Url,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-pub enum SubtitleExt {
-    #[serde(alias = "vtt")]
-    Vtt,
-
-    #[serde(alias = "mp4")]
-    Mp4,
-
-    #[serde(alias = "json")]
-    Json,
-    #[serde(alias = "json3")]
-    Json3,
-
-    #[serde(alias = "ttml")]
-    Ttml,
-
-    #[serde(alias = "srv1")]
-    Srv1,
-    #[serde(alias = "srv2")]
-    Srv2,
-    #[serde(alias = "srv3")]
-    Srv3,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Caption {
-    pub ext: SubtitleExt,
-    pub filepath: Option<PathBuf>,
-    pub filesize: Option<u64>,
-    pub fragments: Option<Vec<SubtitleFragment>>,
-    pub fragment_base_url: Option<Url>,
-    pub manifest_url: Option<Url>,
-    pub name: Option<String>,
-    pub protocol: Option<String>,
-    pub url: String,
-    pub video_id: Option<String>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct SubtitleFragment {
-    path: PathBuf,
-    duration: Option<f64>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Chapter {
-    pub end_time: f64,
-    pub start_time: f64,
-    pub title: String,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub struct SponsorblockChapter {
-    /// This is an utterly useless field, and should thus be ignored
-    pub _categories: Option<Vec<Vec<Value>>>,
-
-    pub categories: Option<Vec<SponsorblockChapterCategory>>,
-    pub category: Option<SponsorblockChapterCategory>,
-    pub category_names: Option<Vec<String>>,
-    pub end_time: f64,
-    pub name: Option<String>,
-    pub r#type: Option<SponsorblockChapterType>,
-    pub start_time: f64,
-    pub title: String,
-}
-
-pub fn get_none<'de, D, T>(_: D) -> Result<Option<T>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    Ok(None)
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub enum SponsorblockChapterType {
-    #[serde(alias = "skip")]
-    Skip,
-
-    #[serde(alias = "chapter")]
-    Chapter,
-
-    #[serde(alias = "poi")]
-    Poi,
-}
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub enum SponsorblockChapterCategory {
-    #[serde(alias = "filler")]
-    Filler,
-
-    #[serde(alias = "interaction")]
-    Interaction,
-
-    #[serde(alias = "music_offtopic")]
-    MusicOfftopic,
-
-    #[serde(alias = "poi_highlight")]
-    PoiHighlight,
-
-    #[serde(alias = "preview")]
-    Preview,
-
-    #[serde(alias = "sponsor")]
-    Sponsor,
-
-    #[serde(alias = "selfpromo")]
-    SelfPromo,
-
-    #[serde(alias = "chapter")]
-    Chapter,
-
-    #[serde(alias = "intro")]
-    Intro,
-
-    #[serde(alias = "outro")]
-    Outro,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-#[allow(missing_copy_implementations)]
-pub struct HeatMapEntry {
-    pub start_time: f64,
-    pub end_time: f64,
-    pub value: f64,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub enum InfoType {
-    #[serde(alias = "playlist")]
-    Playlist,
-
-    #[serde(alias = "url")]
-    Url,
-
-    #[serde(alias = "video")]
-    Video,
-}
-
-#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-pub struct Version {
-    pub current_git_head: Option<String>,
-    pub release_git_head: String,
-    pub repository: String,
-    pub version: String,
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(from = "String")]
-#[serde(deny_unknown_fields)]
-pub enum Parent {
-    Root,
-    Id(String),
-}
-
-impl Parent {
-    #[must_use]
-    pub fn id(&self) -> Option<&str> {
-        if let Self::Id(id) = self {
-            Some(id)
-        } else {
-            None
-        }
-    }
-}
-
-impl From<String> for Parent {
-    fn from(value: String) -> Self {
-        if value == "root" {
-            Self::Root
-        } else {
-            Self::Id(value)
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(from = "String")]
-#[serde(deny_unknown_fields)]
-pub struct Id {
-    pub id: String,
-}
-impl From<String> for Id {
-    fn from(value: String) -> Self {
-        Self {
-            // Take the last element if the string is split with dots, otherwise take the full id
-            id: value.split('.').last().unwrap_or(&value).to_owned(),
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-#[allow(clippy::struct_excessive_bools)]
-pub struct Comment {
-    pub id: Id,
-    pub text: String,
-    #[serde(default = "zero")]
-    pub like_count: u32,
-    pub is_pinned: bool,
-    pub author_id: String,
-    #[serde(default = "unknown")]
-    pub author: String,
-    pub author_is_verified: bool,
-    pub author_thumbnail: Url,
-    pub parent: Parent,
-    #[serde(deserialize_with = "edited_from_time_text", alias = "_time_text")]
-    pub edited: bool,
-    // Can't also be deserialized, as it's already used in 'edited'
-    // _time_text: String,
-    pub timestamp: i64,
-    pub author_url: Url,
-    pub author_is_uploader: bool,
-    pub is_favorited: bool,
-}
-fn unknown() -> String {
-    "<Unknown>".to_string()
-}
-fn zero() -> u32 {
-    0
-}
-fn edited_from_time_text<'de, D>(d: D) -> Result<bool, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let s = String::deserialize(d)?;
-    if s.contains(" (edited)") {
-        Ok(true)
-    } else {
-        Ok(false)
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-pub struct ThumbNail {
-    pub id: Option<String>,
-    pub preference: Option<i32>,
-    /// in the form of "[`height`]x[`width`]"
-    pub resolution: Option<String>,
-    pub url: Url,
-    pub width: Option<u32>,
-    pub height: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Format {
-    pub __needs_testing: Option<bool>,
-    pub __working: Option<bool>,
-    pub abr: Option<f64>,
-    pub acodec: Option<String>,
-    pub aspect_ratio: Option<f64>,
-    pub asr: Option<f64>,
-    pub audio_channels: Option<u32>,
-    pub audio_ext: Option<String>,
-    pub columns: Option<u32>,
-    pub container: Option<String>,
-    pub downloader_options: Option<DownloaderOptions>,
-    pub dynamic_range: Option<String>,
-    pub ext: String,
-    pub filepath: Option<PathBuf>,
-    pub filesize: Option<u64>,
-    pub filesize_approx: Option<u64>,
-    pub format: Option<String>,
-    pub format_id: String,
-    pub format_index: Option<String>,
-    pub format_note: Option<String>,
-    pub fps: Option<f64>,
-    pub fragment_base_url: Option<Todo>,
-    pub fragments: Option<Vec<Fragment>>,
-    pub has_drm: Option<bool>,
-    pub height: Option<u32>,
-    pub http_headers: Option<HttpHeader>,
-    pub is_dash_periods: Option<bool>,
-    pub language: Option<String>,
-    pub language_preference: Option<i32>,
-    pub manifest_stream_number: Option<u32>,
-    pub manifest_url: Option<Url>,
-    pub preference: Option<i32>,
-    pub protocol: Option<String>,
-    pub quality: Option<f64>,
-    pub resolution: Option<String>,
-    pub rows: Option<u32>,
-    pub source_preference: Option<i32>,
-    pub tbr: Option<f64>,
-    pub url: Url,
-    pub vbr: Option<f64>,
-    pub vcodec: String,
-    pub video_ext: Option<String>,
-    pub width: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-#[allow(missing_copy_implementations)]
-pub struct DownloaderOptions {
-    http_chunk_size: u64,
-}
-
-#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-pub struct HttpHeader {
-    #[serde(alias = "User-Agent")]
-    pub user_agent: Option<String>,
-
-    #[serde(alias = "Accept")]
-    pub accept: Option<String>,
-
-    #[serde(alias = "X-Forwarded-For")]
-    pub x_forwarded_for: Option<String>,
-
-    #[serde(alias = "Accept-Language")]
-    pub accept_language: Option<String>,
-
-    #[serde(alias = "Sec-Fetch-Mode")]
-    pub sec_fetch_mode: Option<String>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Fragment {
-    pub url: Option<Url>,
-    pub duration: Option<f64>,
-    pub path: Option<PathBuf>,
-}
-
-impl InfoJson {
-    pub fn to_py_dict(self, py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
-        let output: Bound<'_, PyDict> = json_loads_str(py, self)?;
-        Ok(output)
-    }
-}
diff --git a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs b/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
deleted file mode 100644
index c2a86df..0000000
--- a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 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 pyo3::{types::PyDict, Bound, PyResult, Python};
-use serde::Serialize;
-
-use crate::json_loads;
-
-#[derive(Serialize, Clone)]
-pub struct YtDlpOptions {
-    pub playliststart: u32,
-    pub playlistend: u32,
-    pub noplaylist: bool,
-    pub extract_flat: ExtractFlat,
-    // pub extractor_args: ExtractorArgs,
-    // pub format: String,
-    // pub fragment_retries: u32,
-    // #[serde(rename(serialize = "getcomments"))]
-    // pub get_comments: bool,
-    // #[serde(rename(serialize = "ignoreerrors"))]
-    // pub ignore_errors: bool,
-    // pub retries: u32,
-    // #[serde(rename(serialize = "writeinfojson"))]
-    // pub write_info_json: bool,
-    // pub postprocessors: Vec<serde_json::Map<String, serde_json::Value>>,
-}
-
-#[derive(Serialize, Copy, Clone)]
-pub enum ExtractFlat {
-    #[serde(rename(serialize = "in_playlist"))]
-    InPlaylist,
-
-    #[serde(rename(serialize = "discard_in_playlist"))]
-    DiscardInPlaylist,
-}
-
-#[derive(Serialize, Clone)]
-pub struct ExtractorArgs {
-    pub youtube: YoutubeExtractorArgs,
-}
-
-#[derive(Serialize, Clone)]
-pub struct YoutubeExtractorArgs {
-    comment_sort: Vec<String>,
-    max_comments: Vec<String>,
-}
-
-impl YtDlpOptions {
-    pub fn to_py_dict(self, py: Python) -> PyResult<Bound<PyDict>> {
-        let string = serde_json::to_string(&self).expect("This should always work");
-
-        let output: Bound<PyDict> = json_loads(py, string)?;
-        Ok(output)
-    }
-}