about summary refs log tree commit diff stats
path: root/crates/yt_dlp/src/options.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/yt_dlp/src/options.rs207
1 files changed, 207 insertions, 0 deletions
diff --git a/crates/yt_dlp/src/options.rs b/crates/yt_dlp/src/options.rs
new file mode 100644
index 0000000..ad30301
--- /dev/null
+++ b/crates/yt_dlp/src/options.rs
@@ -0,0 +1,207 @@
+// 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::sync;
+
+use pyo3::{
+    Bound, IntoPyObjectExt, PyAny, PyResult, Python, intern,
+    types::{PyAnyMethods, PyCFunction, PyDict, PyTuple},
+};
+use pyo3_pylogger::setup_logging;
+
+use crate::{
+    YoutubeDL, json_loads, post_processors, py_kw_args,
+    python_error::{IntoPythonError, PythonError},
+};
+
+pub type ProgressHookFunction = fn(py: Python<'_>) -> PyResult<Bound<'_, PyCFunction>>;
+pub type PostProcessorFunction = fn(py: Python<'_>) -> PyResult<Bound<'_, PyAny>>;
+
+/// 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> {
+        pyo3::prepare_freethreaded_python();
+
+        let output_options = options.options.clone();
+
+        let yt_dlp_module = Python::with_gil(|py| {
+            let opts = json_loads(options.options, py);
+
+            {
+                static CALL_ONCE: sync::Once = sync::Once::new();
+
+                CALL_ONCE.call_once(|| {
+                    py.run(
+                        c"
+import signal
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+              ",
+                        None,
+                        None,
+                    )
+                    .unwrap_or_else(|err| {
+                        panic!("Failed to disable python signal handling: {err}")
+                    });
+                });
+            }
+
+            {
+                // Setup the progress hook
+                if let Some(ph) = options.progress_hook {
+                    opts.set_item(intern!(py, "progress_hooks"), vec![ph(py).wrap_exc(py)?])
+                        .wrap_exc(py)?;
+                }
+            }
+
+            {
+                // Unconditionally set a logger.
+                // Otherwise, yt_dlp will log to stderr.
+
+                let ytdl_logger = setup_logging(py, "yt_dlp").wrap_exc(py)?;
+
+                opts.set_item(intern!(py, "logger"), ytdl_logger)
+                    .wrap_exc(py)?;
+            }
+
+            let inner = {
+                let p_params = opts.into_bound_py_any(py).wrap_exc(py)?;
+                let p_auto_init = true.into_bound_py_any(py).wrap_exc(py)?;
+
+                py.import(intern!(py, "yt_dlp.YoutubeDL"))
+                    .wrap_exc(py)?
+                    .getattr(intern!(py, "YoutubeDL"))
+                    .wrap_exc(py)?
+                    .call1(
+                        PyTuple::new(
+                            py,
+                            [
+                                p_params.into_bound_py_any(py).wrap_exc(py)?,
+                                p_auto_init.into_bound_py_any(py).wrap_exc(py)?,
+                            ],
+                        )
+                        .wrap_exc(py)?,
+                    )
+                    .wrap_exc(py)?
+            };
+
+            {
+                // Setup the post processors
+                let add_post_processor_fun = inner
+                    .getattr(intern!(py, "add_post_processor"))
+                    .wrap_exc(py)?;
+
+                for pp in options.post_processors {
+                    add_post_processor_fun
+                        .call(
+                            (pp(py).wrap_exc(py)?.into_bound_py_any(py).wrap_exc(py)?,),
+                            // "when" can take any value in yt_dlp.utils.POSTPROCESS_WHEN
+                            py_kw_args!(py => when = "pre_process"),
+                        )
+                        .wrap_exc(py)?;
+                }
+            }
+
+            Ok::<_, PythonError>(inner.unbind())
+        })?;
+
+        Ok(Self {
+            inner: yt_dlp_module,
+            options: output_options,
+        })
+    }
+}
+
+#[allow(missing_docs)]
+pub mod build {
+    use crate::python_error::PythonError;
+
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
+    }
+}