diff options
Diffstat (limited to 'crates/yt_dlp')
-rw-r--r-- | crates/yt_dlp/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/yt_dlp/src/error.rs | 68 | ||||
-rw-r--r-- | crates/yt_dlp/src/lib.rs | 151 | ||||
-rw-r--r-- | crates/yt_dlp/src/logging.rs | 5 | ||||
-rw-r--r-- | crates/yt_dlp/src/python_json_decode_failed.error_msg | 5 | ||||
-rw-r--r-- | crates/yt_dlp/src/python_json_decode_failed.error_msg.license | 9 | ||||
-rw-r--r-- | crates/yt_dlp/src/tests.rs | 6 | ||||
-rw-r--r-- | crates/yt_dlp/src/wrapper/info_json.rs | 278 | ||||
-rw-r--r-- | crates/yt_dlp/src/wrapper/yt_dlp_options.rs | 2 |
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; |