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.toml2
-rw-r--r--crates/yt_dlp/src/error.rs68
-rw-r--r--crates/yt_dlp/src/lib.rs151
-rw-r--r--crates/yt_dlp/src/logging.rs5
-rw-r--r--crates/yt_dlp/src/python_json_decode_failed.error_msg5
-rw-r--r--crates/yt_dlp/src/python_json_decode_failed.error_msg.license9
-rw-r--r--crates/yt_dlp/src/tests.rs6
-rw-r--r--crates/yt_dlp/src/wrapper/info_json.rs278
-rw-r--r--crates/yt_dlp/src/wrapper/yt_dlp_options.rs2
9 files changed, 482 insertions, 44 deletions
diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml
index 1d34371..a948a34 100644
--- a/crates/yt_dlp/Cargo.toml
+++ b/crates/yt_dlp/Cargo.toml
@@ -22,7 +22,7 @@ rust-version.workspace = true
 publish = false
 
 [dependencies]
-pyo3 = { version = "0.23.3", features = ["auto-initialize"] }
+pyo3 = { version = "0.23.4", features = ["auto-initialize"] }
 bytes.workspace = true
 log.workspace = true
 serde.workspace = true
diff --git a/crates/yt_dlp/src/error.rs b/crates/yt_dlp/src/error.rs
new file mode 100644
index 0000000..3881f0b
--- /dev/null
+++ b/crates/yt_dlp/src/error.rs
@@ -0,0 +1,68 @@
+// 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, io};
+
+use pyo3::Python;
+
+#[derive(Debug)]
+#[allow(clippy::module_name_repetitions)]
+pub enum YtDlpError {
+    ResponseParseError {
+        error: serde_json::error::Error,
+    },
+    PythonError {
+        error: Box<pyo3::PyErr>,
+        kind: String,
+    },
+    IoError {
+        error: io::Error,
+    },
+}
+
+impl std::error::Error for YtDlpError {}
+
+impl Display for YtDlpError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            YtDlpError::ResponseParseError { error } => write!(
+                f,
+                include_str!("./python_json_decode_failed.error_msg"),
+                error
+            ),
+            YtDlpError::PythonError { error, kind: _ } => write!(f, "Python error: {error}"),
+            YtDlpError::IoError { error } => write!(f, "Io error: {error}"),
+        }
+    }
+}
+
+impl From<serde_json::error::Error> for YtDlpError {
+    fn from(value: serde_json::error::Error) -> Self {
+        Self::ResponseParseError { error: value }
+    }
+}
+
+impl From<pyo3::PyErr> for YtDlpError {
+    fn from(value: pyo3::PyErr) -> Self {
+        Python::with_gil(|py| {
+            let kind = value.get_type(py).to_string();
+            Self::PythonError {
+                error: Box::new(value),
+                kind,
+            }
+        })
+    }
+}
+
+impl From<io::Error> for YtDlpError {
+    fn from(value: io::Error) -> Self {
+        Self::IoError { error: value }
+    }
+}
diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs
index 970bfe2..40610c2 100644
--- a/crates/yt_dlp/src/lib.rs
+++ b/crates/yt_dlp/src/lib.rs
@@ -12,8 +12,8 @@
 #![allow(unsafe_op_in_unsafe_fn)]
 #![allow(clippy::missing_errors_doc)]
 
-use std::env;
-use std::io::stdout;
+use std::io::stderr;
+use std::{env, process};
 use std::{fs::File, io::Write};
 
 use std::{path::PathBuf, sync::Once};
@@ -21,18 +21,20 @@ 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 error::YtDlpError;
+use log::{Level, debug, info, log_enabled};
 use pyo3::types::{PyString, PyTuple, PyTupleMethods};
 use pyo3::{
-    pyfunction,
+    Bound, PyAny, PyResult, Python, pyfunction,
     types::{PyAnyMethods, PyDict, PyDictMethods, PyList, PyListMethods, PyModule},
-    wrap_pyfunction, Bound, PyAny, PyResult, Python,
+    wrap_pyfunction,
 };
 use serde::Serialize;
 use serde_json::{Map, Value};
 use url::Url;
 
 pub mod duration;
+pub mod error;
 pub mod logging;
 pub mod wrapper;
 
@@ -51,6 +53,33 @@ pub fn add_logger_and_sig_handler<'a>(
     opts: Bound<'a, PyDict>,
     py: Python<'_>,
 ) -> PyResult<Bound<'a, PyDict>> {
+    /// 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.
+    #[pyfunction]
+    fn filter_error_log(_py: Python<'_>, record: &Bound<'_, PyAny>) -> bool {
+        // Filter out all error logs (they are propagated as rust errors)
+        let levelname: String = record
+            .getattr("levelname")
+            .expect("This should exist")
+            .extract()
+            .expect("This should be a String");
+
+        let return_value = levelname.as_str() != "ERROR";
+
+        if log_enabled!(Level::Debug) && !return_value {
+            let message: String = record
+                .call_method0("getMessage")
+                .expect("This method exists")
+                .extract()
+                .expect("The message is a string");
+
+            debug!("Swollowed error message: '{message}'");
+        }
+        return_value
+    }
+
     setup_logging(py, "yt_dlp")?;
 
     let logging = PyModule::import(py, "logging")?;
@@ -81,6 +110,11 @@ signal.signal(signal.SIGINT, signal.SIG_DFL)",
             .expect("This method exists");
     });
 
+    ytdl_logger.call_method1(
+        "addFilter",
+        (wrap_pyfunction!(filter_error_log, py).expect("This function can be wrapped"),),
+    )?;
+
     // 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")?;
@@ -111,10 +145,10 @@ pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()>
     // see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
     const CSI: &str = "\x1b[";
     fn clear_whole_line() {
-        print!("{CSI}2K");
+        eprint!("{CSI}2K");
     }
     fn move_to_col(x: usize) {
-        print!("{CSI}{x}G");
+        eprint!("{CSI}{x}G");
     }
     // }}}
 
@@ -125,7 +159,7 @@ pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()>
             .expect("Will always work")
             .to_owned(),
     )?)
-    .expect("Python should always produce valid json");
+    .expect("python's json is valid");
 
     macro_rules! get {
         (@interrogate $item:ident, $type_fun:ident, $get_fun:ident, $name:expr) => {{
@@ -198,7 +232,7 @@ pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()>
         format!("{bytes}/s")
     }
 
-    let get_title = |add_extension: bool| -> String {
+    let get_title = || -> String {
         match get! {is_string, as_str, "info_dict", "ext"} {
             "vtt" => {
                 format!(
@@ -206,16 +240,8 @@ pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()>
                     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
-                    )
-                } else {
-                    default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned()
-                }
+            "webm" | "mp4" | "mp3" | "m4a" => {
+                default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned()
             }
             other => panic!("The extension '{other}' is not yet implemented"),
         }
@@ -257,9 +283,9 @@ pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()>
             clear_whole_line();
             move_to_col(1);
 
-            print!(
+            eprint!(
                 "'{}' [{}/{} at {}] -> [{} of {}{} {}] ",
-                c!("34;1", get_title(true)),
+                c!("34;1", get_title()),
                 c!("33;1", Duration::from(Some(elapsed))),
                 c!("33;1", Duration::from(Some(eta))),
                 c!("32;1", format_speed(speed)),
@@ -268,13 +294,16 @@ pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()>
                 c!("31;1", format_bytes(total_bytes)),
                 c!("36;1", format!("{:.02}%", percent))
             );
-            stdout().flush()?;
+            stderr().flush()?;
         }
         "finished" => {
-            println!("-> Finished downloading.");
+            eprintln!("-> Finished downloading.");
         }
         "error" => {
-            panic!("-> Error while downloading: {}", get_title(true))
+            // TODO: This should probably return an Err. But I'm not so sure where the error would
+            // bubble up to (i.e., who would catch it) <2025-01-21>
+            eprintln!("-> Error while downloading: {}", get_title());
+            process::exit(1);
         }
         other => unreachable!("'{other}' should not be a valid state!"),
     };
@@ -298,6 +327,42 @@ pub fn add_hooks<'a>(opts: Bound<'a, PyDict>, py: Python<'_>) -> PyResult<Bound<
     Ok(opts)
 }
 
+/// Take the result of the ie (may be modified) and resolve all unresolved
+/// references (URLs, playlist items).
+///
+/// It will also download the videos if 'download'.
+/// Returns the resolved `ie_result`.
+#[allow(clippy::unused_async)]
+#[allow(clippy::missing_panics_doc)]
+pub async fn process_ie_result(
+    yt_dlp_opts: &Map<String, Value>,
+    ie_result: InfoJson,
+    download: bool,
+) -> Result<InfoJson, YtDlpError> {
+    Python::with_gil(|py| -> Result<InfoJson, YtDlpError> {
+        let opts = json_map_to_py_dict(yt_dlp_opts, py)?;
+
+        let instance = get_yt_dlp(py, opts)?;
+
+        let args = {
+            let ie_result = json_loads_str(py, ie_result)?;
+            (ie_result,)
+        };
+
+        let kwargs = PyDict::new(py);
+        kwargs.set_item("download", download)?;
+
+        let result = instance
+            .call_method("process_ie_result", args, Some(&kwargs))?
+            .downcast_into::<PyDict>()
+            .expect("This is a dict");
+
+        let result_str = json_dumps(py, result.into_any())?;
+
+        serde_json::from_str(&result_str).map_err(Into::into)
+    })
+}
+
 /// `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
@@ -320,8 +385,8 @@ pub async fn extract_info(
     url: &Url,
     download: bool,
     process: bool,
-) -> PyResult<InfoJson> {
-    Python::with_gil(|py| {
+) -> Result<InfoJson, YtDlpError> {
+    Python::with_gil(|py| -> Result<InfoJson, YtDlpError> {
         let opts = json_map_to_py_dict(yt_dlp_opts, py)?;
 
         let instance = get_yt_dlp(py, opts)?;
@@ -331,14 +396,33 @@ pub async fn extract_info(
         kwargs.set_item("download", download)?;
         kwargs.set_item("process", process)?;
 
-        let result = instance.call_method("extract_info", args, Some(&kwargs))?;
+        let result = instance
+            .call_method("extract_info", args, Some(&kwargs))?
+            .downcast_into::<PyDict>()
+            .expect("This is a dict");
+
+        // Resolve the generator object
+        if let Some(generator) = result.get_item("entries")? {
+            if generator.is_instance_of::<PyList>() {
+                // already resolved. Do nothing
+            } else {
+                let max_backlog = yt_dlp_opts.get("playlistend").map_or(10000, |value| {
+                    usize::try_from(value.as_u64().expect("Works")).expect("Should work")
+                });
+
+                let mut out = vec![];
+                while let Ok(output) = generator.call_method0("__next__") {
+                    out.push(output);
 
-        // Remove the `<generator at 0xsome_hex>`, by setting it to null
-        if !process {
-            result.set_item("entries", ())?;
+                    if out.len() == max_backlog {
+                        break;
+                    }
+                }
+                result.set_item("entries", out)?;
+            }
         }
 
-        let result_str = json_dumps(py, result)?;
+        let result_str = json_dumps(py, result.into_any())?;
 
         if let Ok(confirm) = env::var("YT_STORE_INFO_JSON") {
             if confirm == "yes" {
@@ -347,8 +431,7 @@ pub async fn extract_info(
             }
         }
 
-        Ok(serde_json::from_str(&result_str)
-            .expect("Python should be able to produce correct json"))
+        serde_json::from_str(&result_str).map_err(Into::into)
     })
 }
 
@@ -380,7 +463,7 @@ pub fn unsmuggle_url(smug_url: &Url) -> PyResult<Url> {
 pub async fn download(
     urls: &[Url],
     download_options: &Map<String, Value>,
-) -> PyResult<Vec<PathBuf>> {
+) -> Result<Vec<PathBuf>, YtDlpError> {
     let mut out_paths = Vec::with_capacity(urls.len());
 
     for url in urls {
diff --git a/crates/yt_dlp/src/logging.rs b/crates/yt_dlp/src/logging.rs
index 670fc1c..e731502 100644
--- a/crates/yt_dlp/src/logging.rs
+++ b/crates/yt_dlp/src/logging.rs
@@ -17,10 +17,11 @@
 
 use std::ffi::CString;
 
-use log::{logger, Level, MetadataBuilder, Record};
+use log::{Level, MetadataBuilder, Record, logger};
 use pyo3::{
+    Bound, PyAny, PyResult, Python,
     prelude::{PyAnyMethods, PyListMethods, PyModuleMethods},
-    pyfunction, wrap_pyfunction, Bound, PyAny, PyResult, Python,
+    pyfunction, wrap_pyfunction,
 };
 
 /// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead.
diff --git a/crates/yt_dlp/src/python_json_decode_failed.error_msg b/crates/yt_dlp/src/python_json_decode_failed.error_msg
new file mode 100644
index 0000000..d10688e
--- /dev/null
+++ b/crates/yt_dlp/src/python_json_decode_failed.error_msg
@@ -0,0 +1,5 @@
+Failed to decode yt-dlp's response: {}
+
+This is probably a bug.
+Try running the command again with the `YT_STORE_INFO_JSON=yes` environment variable set
+and maybe debug it further via `yt check info-json output.info.json`.
diff --git a/crates/yt_dlp/src/python_json_decode_failed.error_msg.license b/crates/yt_dlp/src/python_json_decode_failed.error_msg.license
new file mode 100644
index 0000000..7813eb6
--- /dev/null
+++ b/crates/yt_dlp/src/python_json_decode_failed.error_msg.license
@@ -0,0 +1,9 @@
+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>.
diff --git a/crates/yt_dlp/src/tests.rs b/crates/yt_dlp/src/tests.rs
index b48deb4..91b6626 100644
--- a/crates/yt_dlp/src/tests.rs
+++ b/crates/yt_dlp/src/tests.rs
@@ -10,7 +10,7 @@
 
 use std::sync::LazyLock;
 
-use serde_json::{json, Value};
+use serde_json::{Value, json};
 use url::Url;
 
 static YT_OPTS: LazyLock<serde_json::Map<String, Value>> = LazyLock::new(|| {
@@ -26,6 +26,7 @@ static YT_OPTS: LazyLock<serde_json::Map<String, Value>> = LazyLock::new(|| {
 });
 
 #[tokio::test]
+#[ignore = "This test hangs forever"]
 async fn test_extract_info_video() {
     let info = crate::extract_info(
         &YT_OPTS,
@@ -41,6 +42,7 @@ async fn test_extract_info_video() {
 }
 
 #[tokio::test]
+#[ignore = "This test hangs forever"]
 async fn test_extract_info_url() {
     let err = crate::extract_info(
         &YT_OPTS,
@@ -56,6 +58,7 @@ async fn test_extract_info_url() {
 }
 
 #[tokio::test]
+#[ignore = "This test hangs forever"]
 async fn test_extract_info_playlist() {
     let err = crate::extract_info(
         &YT_OPTS,
@@ -70,6 +73,7 @@ async fn test_extract_info_playlist() {
     println!("{err:#?}");
 }
 #[tokio::test]
+#[ignore = "This test hangs forever"]
 async fn test_extract_info_playlist_full() {
     let err = crate::extract_info(
         &YT_OPTS,
diff --git a/crates/yt_dlp/src/wrapper/info_json.rs b/crates/yt_dlp/src/wrapper/info_json.rs
index 35d155e..a2c00df 100644
--- a/crates/yt_dlp/src/wrapper/info_json.rs
+++ b/crates/yt_dlp/src/wrapper/info_json.rs
@@ -13,7 +13,7 @@
 
 use std::{collections::HashMap, path::PathBuf};
 
-use pyo3::{types::PyDict, Bound, PyResult, Python};
+use pyo3::{Bound, PyResult, Python, types::PyDict};
 use serde::{Deserialize, Deserializer, Serialize};
 use serde_json::Value;
 use url::Url;
@@ -29,123 +29,385 @@ type ExtractorKey = String;
 #[derive(Debug, Deserialize, Serialize, PartialEq)]
 #[serde(deny_unknown_fields)]
 pub struct InfoJson {
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub __files_to_move: Option<FilesToMove>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub __last_playlist_index: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub __post_extractor: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub __x_forwarded_for_ip: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub _filename: Option<PathBuf>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub _format_sort_fields: Option<Vec<String>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub _has_drm: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub _type: Option<InfoType>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub _version: Option<Version>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub abr: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub acodec: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub age_limit: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub artists: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub aspect_ratio: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub asr: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub audio_channels: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub audio_ext: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub automatic_captions: Option<HashMap<String, Vec<Caption>>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub availability: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub average_rating: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub categories: Option<Vec<String>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub channel: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub channel_follower_count: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub channel_id: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub channel_is_verified: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub channel_url: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub chapters: Option<Vec<Chapter>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub comment_count: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub comments: Option<Vec<Comment>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub concurrent_view_count: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub container: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub description: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub direct: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub display_id: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub downloader_options: Option<DownloaderOptions>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub duration: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub duration_string: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub dynamic_range: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub entries: Option<Vec<InfoJson>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub episode: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub episode_number: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub epoch: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub ext: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub extractor: Option<Extractor>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub extractor_key: Option<ExtractorKey>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub filename: Option<PathBuf>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub filesize: Option<u64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub filesize_approx: Option<u64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub format: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub format_id: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub format_index: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub format_note: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub formats: Option<Vec<Format>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub fps: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub fulltitle: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub genre: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub genres: Option<Vec<String>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub has_drm: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub heatmap: Option<Vec<HeatMapEntry>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub height: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub hls_aes: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub http_headers: Option<HttpHeader>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub id: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub ie_key: Option<ExtractorKey>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub is_live: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub language: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub language_preference: Option<i32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub license: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub like_count: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub live_status: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub location: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub manifest_url: Option<Url>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub media_type: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub modified_date: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub n_entries: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub original_url: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playable_in_embed: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_autonumber: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_channel: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_channel_id: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_count: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_id: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_index: Option<u64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_title: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_uploader: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub playlist_uploader_id: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub playlist_webpage_url: Option<Url>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub preference: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub protocol: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub quality: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub release_date: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub release_timestamp: Option<u64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub release_year: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub repost_count: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub requested_downloads: Option<Vec<RequestedDownloads>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub requested_entries: Option<Vec<u32>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub requested_formats: Option<Vec<Format>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub requested_subtitles: Option<HashMap<String, Subtitle>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub resolution: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub season: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub season_number: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub series: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub source_preference: Option<i32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub sponsorblock_chapters: Option<Vec<SponsorblockChapter>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub stretched_ratio: Option<Todo>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub subtitles: Option<HashMap<String, Vec<Caption>>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub tags: Option<Vec<String>>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub tbr: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub thumbnail: Option<Url>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub thumbnails: Option<Vec<ThumbNail>>,
-    pub timestamp: Option<u64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub timestamp: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub title: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub upload_date: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub uploader: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub uploader_id: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub uploader_url: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub url: Option<Url>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub vbr: Option<f64>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub vcodec: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub video_ext: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub view_count: Option<u32>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub was_live: Option<bool>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub webpage_url: Option<Url>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub webpage_url_basename: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub webpage_url_domain: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub width: Option<u32>,
 }
 
@@ -181,7 +443,7 @@ pub struct RequestedDownloads {
     pub filesize_approx: Option<u64>,
     pub format: String,
     pub format_id: String,
-    pub format_note: String,
+    pub format_note: Option<String>,
     pub fps: Option<f64>,
     pub has_drm: Option<bool>,
     pub height: Option<u32>,
@@ -190,6 +452,7 @@ pub struct RequestedDownloads {
     pub language: Option<String>,
     pub manifest_url: Option<Url>,
     pub protocol: String,
+    pub quality: Option<i64>,
     pub requested_formats: Option<Vec<Format>>,
     pub resolution: String,
     pub tbr: f64,
@@ -350,12 +613,15 @@ pub struct HeatMapEntry {
 #[serde(deny_unknown_fields)]
 pub enum InfoType {
     #[serde(alias = "playlist")]
+    #[serde(rename(serialize = "playlist"))]
     Playlist,
 
     #[serde(alias = "url")]
+    #[serde(rename(serialize = "url"))]
     Url,
 
     #[serde(alias = "video")]
+    #[serde(rename(serialize = "video"))]
     Video,
 }
 
@@ -432,7 +698,7 @@ pub struct Comment {
     // Can't also be deserialized, as it's already used in 'edited'
     // _time_text: String,
     pub timestamp: i64,
-    pub author_url: Url,
+    pub author_url: Option<Url>,
     pub author_is_uploader: bool,
     pub is_favorited: bool,
 }
@@ -496,6 +762,7 @@ pub struct Format {
     pub height: Option<u32>,
     pub http_headers: Option<HttpHeader>,
     pub is_dash_periods: Option<bool>,
+    pub is_live: Option<bool>,
     pub language: Option<String>,
     pub language_preference: Option<i32>,
     pub manifest_stream_number: Option<u32>,
@@ -543,9 +810,10 @@ pub struct HttpHeader {
 #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
 #[serde(deny_unknown_fields)]
 pub struct Fragment {
-    pub url: Option<Url>,
     pub duration: Option<f64>,
+    pub fragment_count: Option<usize>,
     pub path: Option<PathBuf>,
+    pub url: Option<Url>,
 }
 
 impl InfoJson {
diff --git a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs b/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
index c2a86df..25595b5 100644
--- a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
+++ b/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
@@ -8,7 +8,7 @@
 // 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 pyo3::{Bound, PyResult, Python, types::PyDict};
 use serde::Serialize;
 
 use crate::json_loads;