about summary refs log tree commit diff stats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/bytes/src/error.rs5
-rw-r--r--crates/bytes/src/lib.rs11
-rw-r--r--crates/libmpv2/libmpv2-sys/src/lib.rs9
-rw-r--r--crates/libmpv2/src/mpv.rs4
-rw-r--r--crates/libmpv2/src/mpv/protocol.rs2
-rw-r--r--crates/libmpv2/src/mpv/render.rs4
-rw-r--r--crates/yt_dlp/Cargo.toml1
-rw-r--r--crates/yt_dlp/src/duration.rs7
-rw-r--r--crates/yt_dlp/src/lib.rs67
-rw-r--r--crates/yt_dlp/src/logging.rs16
-rw-r--r--crates/yt_dlp/src/main.rs96
-rw-r--r--crates/yt_dlp/src/tests.rs85
-rw-r--r--crates/yt_dlp/src/wrapper/info_json.rs25
13 files changed, 189 insertions, 143 deletions
diff --git a/crates/bytes/src/error.rs b/crates/bytes/src/error.rs
index 7643109..c9783d8 100644
--- a/crates/bytes/src/error.rs
+++ b/crates/bytes/src/error.rs
@@ -11,6 +11,7 @@
 use std::{fmt::Display, num::ParseIntError};
 
 #[derive(Debug)]
+#[allow(clippy::module_name_repetitions)]
 pub enum BytesError {
     BytesParseIntError(ParseIntError),
     NotYetSupported(String),
@@ -20,10 +21,10 @@ impl Display for BytesError {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             BytesError::BytesParseIntError(e) => {
-                f.write_fmt(format_args!("Failed to parse a number as integer: '{}'", e))
+                f.write_fmt(format_args!("Failed to parse a number as integer: '{e}'"))
             },
             BytesError::NotYetSupported(other) => {
-                f.write_fmt(format_args!("Your extension '{}' is not yet supported. Only KB,MB,GB or KiB,MiB,GiB are supported", other))
+                f.write_fmt(format_args!("Your extension '{other}' is not yet supported. Only KB,MB,GB or KiB,MiB,GiB are supported"))
             }
         }
     }
diff --git a/crates/bytes/src/lib.rs b/crates/bytes/src/lib.rs
index 78d3c4e..6e3f73c 100644
--- a/crates/bytes/src/lib.rs
+++ b/crates/bytes/src/lib.rs
@@ -8,6 +8,12 @@
 // 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>.
 
+#![allow(
+    clippy::cast_possible_truncation,
+    clippy::cast_precision_loss,
+    clippy::cast_sign_loss,
+    clippy::cast_possible_wrap
+)]
 use std::{fmt::Display, str::FromStr};
 
 use error::BytesError;
@@ -28,13 +34,15 @@ const TB: u64 = 1000 * GB;
 pub mod error;
 pub mod serde;
 
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
 pub struct Bytes(u64);
 
 impl Bytes {
+    #[must_use]
     pub fn as_u64(self) -> u64 {
         self.0
     }
+    #[must_use]
     pub fn new(v: u64) -> Self {
         Self(v)
     }
@@ -140,6 +148,7 @@ impl Display for Bytes {
 ///   assert_eq!(precision_f64(1.2300_f32 as f64, 2) as f32, 1.2f32);
 ///# }
 /// ```
+#[must_use]
 pub fn precision_f64(x: f64, decimals: u32) -> f64 {
     if x == 0. || decimals == 0 {
         0.
diff --git a/crates/libmpv2/libmpv2-sys/src/lib.rs b/crates/libmpv2/libmpv2-sys/src/lib.rs
index 36a8199..0d14dbb 100644
--- a/crates/libmpv2/libmpv2-sys/src/lib.rs
+++ b/crates/libmpv2/libmpv2-sys/src/lib.rs
@@ -1,7 +1,3 @@
-#![allow(non_upper_case_globals)]
-#![allow(non_camel_case_types)]
-#![allow(non_snake_case)]
-
 // yt - A fully featured command line YouTube client
 //
 // Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
@@ -11,6 +7,11 @@
 //
 // 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>.
+//
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(clippy::doc_lazy_continuation)]
 
 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
 
diff --git a/crates/libmpv2/src/mpv.rs b/crates/libmpv2/src/mpv.rs
index 9d554a6..bf4581e 100644
--- a/crates/libmpv2/src/mpv.rs
+++ b/crates/libmpv2/src/mpv.rs
@@ -51,6 +51,7 @@ fn mpv_err<T>(ret: T, err: ctype::c_int) -> Result<T> {
 }
 
 /// This trait describes which types are allowed to be passed to getter mpv APIs.
+#[allow(clippy::missing_safety_doc)]
 pub unsafe trait GetData: Sized {
     #[doc(hidden)]
     fn get_from_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(mut fun: F) -> Result<Self> {
@@ -62,6 +63,7 @@ pub unsafe trait GetData: Sized {
 }
 
 /// This trait describes which types are allowed to be passed to setter mpv APIs.
+#[allow(clippy::missing_safety_doc)]
 pub unsafe trait SetData: Sized {
     #[doc(hidden)]
     fn call_as_c_void<T, F: FnMut(*mut ctype::c_void) -> Result<T>>(
@@ -207,7 +209,7 @@ pub mod mpv_node {
                 }
             }
 
-            pub fn child(self: Self, node: libmpv2_sys::mpv_node) -> Self {
+            pub fn child(self, node: libmpv2_sys::mpv_node) -> Self {
                 Self {
                     parent: self.parent,
                     node,
diff --git a/crates/libmpv2/src/mpv/protocol.rs b/crates/libmpv2/src/mpv/protocol.rs
index 4ae4f16..31a5933 100644
--- a/crates/libmpv2/src/mpv/protocol.rs
+++ b/crates/libmpv2/src/mpv/protocol.rs
@@ -155,7 +155,7 @@ where
 {
     let data = Box::from_raw(cookie as *mut ProtocolData<T, U>);
 
-    panic::catch_unwind(|| ((*data).close_fn)(Box::from_raw((*data).cookie)));
+    panic::catch_unwind(|| (data.close_fn)(Box::from_raw(data.cookie)));
 }
 
 struct ProtocolData<T, U> {
diff --git a/crates/libmpv2/src/mpv/render.rs b/crates/libmpv2/src/mpv/render.rs
index 91db34e..c3f2dc9 100644
--- a/crates/libmpv2/src/mpv/render.rs
+++ b/crates/libmpv2/src/mpv/render.rs
@@ -213,8 +213,8 @@ impl RenderContext {
         params: impl IntoIterator<Item = RenderParam<C>>,
     ) -> Result<Self> {
         let params: Vec<_> = params.into_iter().collect();
-        let mut raw_params: Vec<libmpv2_sys::mpv_render_param> = Vec::new();
-        raw_params.reserve(params.len() + 1);
+        let mut raw_params: Vec<libmpv2_sys::mpv_render_param> =
+            Vec::with_capacity(params.len() + 1);
         let mut raw_ptrs: HashMap<*const c_void, DeleterFn> = HashMap::new();
 
         for p in params {
diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml
index 0f1d248..03196e9 100644
--- a/crates/yt_dlp/Cargo.toml
+++ b/crates/yt_dlp/Cargo.toml
@@ -30,6 +30,7 @@ serde_json.workspace = true
 url.workspace = true
 
 [dev-dependencies]
+tokio.workspace = true
 
 [lints]
 workspace = true
diff --git a/crates/yt_dlp/src/duration.rs b/crates/yt_dlp/src/duration.rs
index cd7454b..f91892d 100644
--- a/crates/yt_dlp/src/duration.rs
+++ b/crates/yt_dlp/src/duration.rs
@@ -9,6 +9,8 @@
 // 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,
 }
@@ -26,6 +28,11 @@ impl From<&str> for Duration {
 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,
         }
     }
diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs
index f958895..4e35cb0 100644
--- a/crates/yt_dlp/src/lib.rs
+++ b/crates/yt_dlp/src/lib.rs
@@ -8,6 +8,10 @@
 // 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)]
+
 use std::env;
 use std::{fs::File, io::Write};
 
@@ -31,14 +35,20 @@ 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,
+    py: Python<'_>,
 ) -> PyResult<Bound<'a, PyDict>> {
     setup_logging(py, "yt_dlp")?;
 
@@ -52,10 +62,9 @@ pub fn add_logger_and_sig_handler<'a>(
         // 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_bound(
-            r#"
+            "\
 import signal
-signal.signal(signal.SIGINT, signal.SIG_DFL)
-        "#,
+signal.signal(signal.SIGINT, signal.SIG_DFL)",
             None,
             None,
         )
@@ -82,14 +91,22 @@ signal.signal(signal.SIGINT, signal.SIG_DFL)
 }
 
 #[pyfunction]
-pub fn progress_hook(py: Python, input: Bound<'_, PyDict>) -> PyResult<()> {
+#[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(());
     }
 
-    let input: serde_json::Map<String, Value> = serde_json::from_str(&json_dumps(
+    let input: Map<String, Value> = serde_json::from_str(&json_dumps(
         py,
         input
             .downcast::<PyAny>()
@@ -164,8 +181,9 @@ pub fn progress_hook(py: Python, input: Bound<'_, PyDict>) -> PyResult<()> {
     }
 
     fn format_speed(speed: f64) -> String {
+        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
         let bytes = Bytes::new(speed.floor() as u64);
-        format!("{}/s", bytes)
+        format!("{bytes}/s")
     }
 
     let get_title = |add_extension: bool| -> String {
@@ -187,7 +205,7 @@ pub fn progress_hook(py: Python, input: Bound<'_, PyDict>) -> PyResult<()> {
                     default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned()
                 }
             }
-            other => panic!("The extension '{}' is not yet implemented", other),
+            other => panic!("The extension '{other}' is not yet implemented"),
         }
     };
 
@@ -242,18 +260,18 @@ pub fn progress_hook(py: Python, input: Bound<'_, PyDict>) -> PyResult<()> {
             );
         }
         "finished" => {
-            println!("Finished downloading: '{}'", c!("34;1", get_title(false)))
+            println!("Finished downloading: '{}'", c!("34;1", get_title(false)));
         }
         "error" => {
             panic!("Error whilst downloading: {}", get_title(true))
         }
-        other => panic!("{} is not a valid state!", other),
+        other => panic!("{other} is not a valid state!"),
     };
 
     Ok(())
 }
 
-pub fn add_hooks<'a>(opts: Bound<'a, PyDict>, py: Python) -> PyResult<Bound<'a, PyDict>> {
+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_bound!(progress_hook, py)?)?;
@@ -280,10 +298,12 @@ pub fn add_hooks<'a>(opts: Bound<'a, PyDict>, py: Python) -> PyResult<Bound<'a,
 /// @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 `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')
+/// @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,
@@ -311,8 +331,8 @@ pub async fn extract_info(
 
         if let Ok(confirm) = env::var("YT_STORE_INFO_JSON") {
             if confirm == "yes" {
-                let mut file = File::create("output.info.json").unwrap();
-                write!(file, "{}", result_str).unwrap();
+                let mut file = File::create("output.info.json")?;
+                write!(file, "{result_str}").unwrap();
             }
         }
 
@@ -321,7 +341,9 @@ pub async fn extract_info(
     })
 }
 
-pub fn unsmuggle_url(smug_url: Url) -> PyResult<Url> {
+/// # 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
@@ -341,6 +363,9 @@ pub fn unsmuggle_url(smug_url: Url) -> PyResult<Url> {
 
 /// 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>,
@@ -357,7 +382,7 @@ pub async fn download(
         } else {
             info_json.requested_downloads.expect("This must exist")[0]
                 .filename
-                .to_owned()
+                .clone()
         };
 
         out_paths.push(result_string);
@@ -378,7 +403,7 @@ fn json_map_to_py_dict<'a>(
     Ok(python_dict)
 }
 
-fn json_dumps(py: Python, input: Bound<PyAny>) -> PyResult<String> {
+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_bound(py))?;
@@ -394,13 +419,13 @@ fn json_dumps(py: Python, input: Bound<PyAny>) -> PyResult<String> {
     Ok(output_str)
 }
 
-fn json_loads_str<T: Serialize>(py: Python, input: T) -> PyResult<Bound<PyDict>> {
+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)
 }
 
-fn json_loads(py: Python, input: String) -> PyResult<Bound<PyDict>> {
+fn json_loads(py: Python<'_>, input: String) -> PyResult<Bound<'_, PyDict>> {
     //     json.loads(input)
 
     let json = PyModule::import_bound(py, "json")?;
diff --git a/crates/yt_dlp/src/logging.rs b/crates/yt_dlp/src/logging.rs
index 4039da4..385255d 100644
--- a/crates/yt_dlp/src/logging.rs
+++ b/crates/yt_dlp/src/logging.rs
@@ -12,6 +12,9 @@
 // It is licensed under the Apache 2.0 License, copyright up to 2024 by Dylan Storey
 // It was modified by Benedikt Peetz 2024
 
+// The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint.
+#![allow(unsafe_op_in_unsafe_fn)]
+
 use log::{logger, Level, MetadataBuilder, Record};
 use pyo3::{
     prelude::{PyAnyMethods, PyListMethods, PyModuleMethods},
@@ -19,6 +22,7 @@ use pyo3::{
 };
 
 /// 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")?;
@@ -37,7 +41,7 @@ fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> {
     } else {
         // Libraries (ex: tracing_subscriber::filter::Directive) expect rust-style targets like foo::bar,
         // and may not deal well with "." as a module separator:
-        let logger_name = logger_name.replace(".", "::");
+        let logger_name = logger_name.replace('.', "::");
         Some(format!("{rust_target}::{logger_name}"))
     };
 
@@ -84,10 +88,11 @@ fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> {
     Ok(())
 }
 
-/// Registers the host_log function in rust as the event handler for Python's logging logger
+/// Registers the `host_log` function in rust as the event handler for Python's logging logger
 /// This function needs to be called from within a pyo3 context as early as possible to ensure logging messages
 /// arrive to the rust consumer.
-pub fn setup_logging(py: Python, target: &str) -> PyResult<()> {
+#[allow(clippy::module_name_repetitions)]
+pub fn setup_logging(py: Python<'_>, target: &str) -> PyResult<()> {
     let logging = py.import_bound("logging")?;
 
     logging.setattr("host_log", wrap_pyfunction!(host_log, &logging)?)?;
@@ -100,15 +105,14 @@ class HostHandler(Handler):
         super().__init__(level=level)
 
     def emit(self, record):
-        host_log(record,"{}")
+        host_log(record,"{target}")
 
 oldBasicConfig = basicConfig
 def basicConfig(*pargs, **kwargs):
     if "handlers" not in kwargs:
         kwargs["handlers"] = [HostHandler()]
     return oldBasicConfig(*pargs, **kwargs)
-"#,
-            target
+"#
         )
         .as_str(),
         Some(&logging.dict()),
diff --git a/crates/yt_dlp/src/main.rs b/crates/yt_dlp/src/main.rs
deleted file mode 100644
index c40ddc3..0000000
--- a/crates/yt_dlp/src/main.rs
+++ /dev/null
@@ -1,96 +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::{env::args, fs};
-
-use yt_dlp::wrapper::info_json::InfoJson;
-
-#[cfg(test)]
-mod test {
-    use url::Url;
-    use yt_dlp::wrapper::yt_dlp_options::{ExtractFlat, YtDlpOptions};
-
-    const YT_OPTS: YtDlpOptions = YtDlpOptions {
-        playliststart: 1,
-        playlistend: 10,
-        noplaylist: false,
-        extract_flat: ExtractFlat::InPlaylist,
-    };
-
-    #[test]
-    fn test_extract_info_video() {
-        let info = yt_dlp::extract_info(
-            YT_OPTS,
-            &Url::parse("https://www.youtube.com/watch?v=dbjPnXaacAU").expect("Is valid."),
-            false,
-            false,
-            false,
-        )
-        .map_err(|err| format!("Encountered error: '{}'", err))
-        .unwrap();
-
-        println!("{:#?}", info);
-    }
-
-    #[test]
-    fn test_extract_info_url() {
-        let err = yt_dlp::extract_info(
-            YT_OPTS,
-            &Url::parse("https://google.com").expect("Is valid."),
-            false,
-            false,
-            false,
-        )
-        .map_err(|err| format!("Encountered error: '{}'", err))
-        .unwrap();
-
-        println!("{:#?}", err);
-    }
-
-    #[test]
-    fn test_extract_info_playlist() {
-        let err = yt_dlp::extract_info(
-            YT_OPTS,
-            &Url::parse("https://www.youtube.com/@TheGarriFrischer/videos").expect("Is valid."),
-            false,
-            false,
-            true,
-        )
-        .map_err(|err| format!("Encountered error: '{}'", err))
-        .unwrap();
-
-        println!("{:#?}", err);
-    }
-    #[test]
-    fn test_extract_info_playlist_full() {
-        let err = yt_dlp::extract_info(
-            YT_OPTS,
-            &Url::parse("https://www.youtube.com/@NixOS-Foundation/videos").expect("Is valid."),
-            false,
-            false,
-            true,
-        )
-        .map_err(|err| format!("Encountered error: '{}'", err))
-        .unwrap();
-
-        println!("{:#?}", err);
-    }
-}
-
-fn main() {
-    let input_file: &str = &args().take(2).collect::<Vec<String>>()[1];
-
-    let input = fs::read_to_string(input_file).unwrap();
-
-    let output: InfoJson =
-        serde_json::from_str(&input).expect("Python should be able to produce correct json");
-
-    println!("{:#?}", output);
-}
diff --git a/crates/yt_dlp/src/tests.rs b/crates/yt_dlp/src/tests.rs
new file mode 100644
index 0000000..08e392f
--- /dev/null
+++ b/crates/yt_dlp/src/tests.rs
@@ -0,0 +1,85 @@
+// 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
index 50a026d..f113fe5 100644
--- a/crates/yt_dlp/src/wrapper/info_json.rs
+++ b/crates/yt_dlp/src/wrapper/info_json.rs
@@ -8,6 +8,9 @@
 // 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};
@@ -146,6 +149,7 @@ pub struct InfoJson {
 
 #[derive(Debug, Deserialize, Serialize, PartialEq)]
 #[serde(deny_unknown_fields)]
+#[allow(missing_copy_implementations)]
 pub struct FilesToMove {}
 
 #[derive(Debug, Deserialize, Serialize, PartialEq)]
@@ -197,8 +201,7 @@ pub struct Subtitle {
     pub url: Url,
 }
 
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
-#[serde(deny_unknown_fields)]
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
 pub enum SubtitleExt {
     #[serde(alias = "vtt")]
     Vtt,
@@ -266,7 +269,7 @@ where
     Ok(None)
 }
 
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
 #[serde(deny_unknown_fields)]
 pub enum SponsorblockChapterType {
     #[serde(alias = "skip")]
@@ -278,7 +281,7 @@ pub enum SponsorblockChapterType {
     #[serde(alias = "poi")]
     Poi,
 }
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
 #[serde(deny_unknown_fields)]
 pub enum SponsorblockChapterCategory {
     #[serde(alias = "filler")]
@@ -314,13 +317,14 @@ pub enum SponsorblockChapterCategory {
 
 #[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)]
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
 #[serde(deny_unknown_fields)]
 pub enum Extractor {
     #[serde(alias = "generic")]
@@ -337,7 +341,7 @@ pub enum Extractor {
     YouTubeTab,
 }
 
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
 #[serde(deny_unknown_fields)]
 pub enum ExtractorKey {
     #[serde(alias = "Generic")]
@@ -354,7 +358,7 @@ pub enum ExtractorKey {
     YouTubeTab,
 }
 
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
 #[serde(deny_unknown_fields)]
 pub enum InfoType {
     #[serde(alias = "playlist")]
@@ -385,6 +389,7 @@ pub enum Parent {
 }
 
 impl Parent {
+    #[must_use]
     pub fn id(&self) -> Option<&str> {
         if let Self::Id(id) = self {
             Some(id)
@@ -421,6 +426,7 @@ impl From<String> for Id {
 
 #[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,
@@ -522,6 +528,7 @@ pub struct Format {
 
 #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
 #[serde(deny_unknown_fields)]
+#[allow(missing_copy_implementations)]
 pub struct DownloaderOptions {
     http_chunk_size: u64,
 }
@@ -554,8 +561,8 @@ pub struct Fragment {
 }
 
 impl InfoJson {
-    pub fn to_py_dict(self, py: Python) -> PyResult<Bound<PyDict>> {
-        let output: Bound<PyDict> = json_loads_str(py, self)?;
+    pub fn to_py_dict(self, py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
+        let output: Bound<'_, PyDict> = json_loads_str(py, self)?;
         Ok(output)
     }
 }