// yt - A fully featured command line YouTube client // // Copyright (C) 2025 Benedikt Peetz // 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 . 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>; pub type PostProcessorFunction = fn(py: Python<'_>) -> PyResult>; /// 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, progress_hook: Option, post_processors: Vec, } 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, value: impl Into) -> 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::from_options(self) } #[must_use] pub fn from_json_options(options: serde_json::Map) -> 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 { 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), } }