// yt - A fully featured command line YouTube client // // Copyright (C) 2024 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 . // `yt_dlp` named them like this. #![allow(clippy::pub_underscore_fields)] use std::{collections::HashMap, path::PathBuf}; use pyo3::{types::PyDict, Bound, PyResult, Python}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use url::Url; use crate::json_loads_str; type Todo = String; type Extractor = String; type ExtractorKey = String; // TODO: Change this to map `_type` to a structure of values, instead of the options <2024-05-27> // And replace all the strings with better types (enums or urls) #[derive(Debug, Deserialize, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct InfoJson { #[serde(skip_serializing_if = "Option::is_none")] pub __files_to_move: Option, #[serde(skip_serializing_if = "Option::is_none")] pub __last_playlist_index: Option, #[serde(skip_serializing_if = "Option::is_none")] pub __post_extractor: Option, #[serde(skip_serializing_if = "Option::is_none")] pub __x_forwarded_for_ip: Option, #[serde(skip_serializing_if = "Option::is_none")] pub _filename: Option, #[serde(skip_serializing_if = "Option::is_none")] pub _format_sort_fields: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub _has_drm: Option, #[serde(skip_serializing_if = "Option::is_none")] pub _type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub _version: Option, #[serde(skip_serializing_if = "Option::is_none")] pub abr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub acodec: Option, #[serde(skip_serializing_if = "Option::is_none")] pub age_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub artists: Option, #[serde(skip_serializing_if = "Option::is_none")] pub aspect_ratio: Option, #[serde(skip_serializing_if = "Option::is_none")] pub asr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub audio_channels: Option, #[serde(skip_serializing_if = "Option::is_none")] pub audio_ext: Option, #[serde(skip_serializing_if = "Option::is_none")] pub automatic_captions: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub availability: Option, #[serde(skip_serializing_if = "Option::is_none")] pub average_rating: Option, #[serde(skip_serializing_if = "Option::is_none")] pub categories: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub channel: Option, #[serde(skip_serializing_if = "Option::is_none")] pub channel_follower_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub channel_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub channel_is_verified: Option, #[serde(skip_serializing_if = "Option::is_none")] pub channel_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub chapters: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub comment_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub comments: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub concurrent_view_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub container: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub direct: Option, #[serde(skip_serializing_if = "Option::is_none")] pub display_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub downloader_options: Option, #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, #[serde(skip_serializing_if = "Option::is_none")] pub duration_string: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dynamic_range: Option, #[serde(skip_serializing_if = "Option::is_none")] pub entries: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub episode: Option, #[serde(skip_serializing_if = "Option::is_none")] pub episode_number: Option, #[serde(skip_serializing_if = "Option::is_none")] pub epoch: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ext: Option, #[serde(skip_serializing_if = "Option::is_none")] pub extractor: Option, #[serde(skip_serializing_if = "Option::is_none")] pub extractor_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub filename: Option, #[serde(skip_serializing_if = "Option::is_none")] pub filesize: Option, #[serde(skip_serializing_if = "Option::is_none")] pub filesize_approx: Option, #[serde(skip_serializing_if = "Option::is_none")] pub format: Option, #[serde(skip_serializing_if = "Option::is_none")] pub format_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub format_index: Option, #[serde(skip_serializing_if = "Option::is_none")] pub format_note: Option, #[serde(skip_serializing_if = "Option::is_none")] pub formats: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub fps: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fulltitle: Option, #[serde(skip_serializing_if = "Option::is_none")] pub genre: Option, #[serde(skip_serializing_if = "Option::is_none")] pub genres: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub has_drm: Option, #[serde(skip_serializing_if = "Option::is_none")] pub heatmap: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub height: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hls_aes: Option, #[serde(skip_serializing_if = "Option::is_none")] pub http_headers: Option, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ie_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub is_live: Option, #[serde(skip_serializing_if = "Option::is_none")] pub language: Option, #[serde(skip_serializing_if = "Option::is_none")] pub language_preference: Option, #[serde(skip_serializing_if = "Option::is_none")] pub license: Option, #[serde(skip_serializing_if = "Option::is_none")] pub like_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub live_status: Option, #[serde(skip_serializing_if = "Option::is_none")] pub location: Option, #[serde(skip_serializing_if = "Option::is_none")] pub manifest_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub media_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub modified_date: Option, #[serde(skip_serializing_if = "Option::is_none")] pub n_entries: Option, #[serde(skip_serializing_if = "Option::is_none")] pub original_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playable_in_embed: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_autonumber: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_channel: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_channel_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_index: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_title: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_uploader: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_uploader_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub playlist_webpage_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub preference: Option, #[serde(skip_serializing_if = "Option::is_none")] pub protocol: Option, #[serde(skip_serializing_if = "Option::is_none")] pub quality: Option, #[serde(skip_serializing_if = "Option::is_none")] pub release_date: Option, #[serde(skip_serializing_if = "Option::is_none")] pub release_timestamp: Option, #[serde(skip_serializing_if = "Option::is_none")] pub release_year: Option, #[serde(skip_serializing_if = "Option::is_none")] pub repost_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub requested_downloads: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub requested_entries: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub requested_formats: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub requested_subtitles: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub resolution: Option, #[serde(skip_serializing_if = "Option::is_none")] pub season: Option, #[serde(skip_serializing_if = "Option::is_none")] pub season_number: Option, #[serde(skip_serializing_if = "Option::is_none")] pub series: Option, #[serde(skip_serializing_if = "Option::is_none")] pub source_preference: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sponsorblock_chapters: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub stretched_ratio: Option, #[serde(skip_serializing_if = "Option::is_none")] pub subtitles: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub tags: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tbr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumbnail: Option, #[serde(skip_serializing_if = "Option::is_none")] pub thumbnails: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(skip_serializing_if = "Option::is_none")] pub upload_date: Option, #[serde(skip_serializing_if = "Option::is_none")] pub uploader: Option, #[serde(skip_serializing_if = "Option::is_none")] pub uploader_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub uploader_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vbr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vcodec: Option, #[serde(skip_serializing_if = "Option::is_none")] pub video_ext: Option, #[serde(skip_serializing_if = "Option::is_none")] pub view_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub was_live: Option, #[serde(skip_serializing_if = "Option::is_none")] pub webpage_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub webpage_url_basename: Option, #[serde(skip_serializing_if = "Option::is_none")] pub webpage_url_domain: Option, #[serde(skip_serializing_if = "Option::is_none")] pub width: Option, } #[derive(Debug, Deserialize, Serialize, PartialEq)] #[serde(deny_unknown_fields)] #[allow(missing_copy_implementations)] pub struct FilesToMove {} #[derive(Debug, Deserialize, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct RequestedDownloads { pub __files_to_merge: Option>, pub __finaldir: PathBuf, pub __infojson_filename: PathBuf, pub __postprocessors: Vec, pub __real_download: bool, pub __write_download_archive: bool, pub _filename: PathBuf, pub _type: InfoType, pub _version: Version, pub abr: f64, pub acodec: String, pub aspect_ratio: Option, pub asr: Option, pub audio_channels: Option, pub audio_ext: Option, pub chapters: Option>, pub duration: Option, pub dynamic_range: Option, pub ext: String, pub filename: PathBuf, pub filepath: PathBuf, pub filesize_approx: Option, pub format: String, pub format_id: String, pub format_note: Option, pub fps: Option, pub has_drm: Option, pub height: Option, pub http_headers: Option, pub infojson_filename: PathBuf, pub language: Option, pub manifest_url: Option, pub protocol: String, pub quality: Option, pub requested_formats: Option>, pub resolution: String, pub tbr: f64, pub url: Option, pub vbr: f64, pub vcodec: String, pub video_ext: Option, pub width: Option, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Subtitle { pub ext: SubtitleExt, pub filepath: PathBuf, pub filesize: Option, pub fragment_base_url: Option, pub fragments: Option>, pub manifest_url: Option, pub name: Option, pub protocol: Option, pub url: Url, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)] pub enum SubtitleExt { #[serde(alias = "vtt")] Vtt, #[serde(alias = "mp4")] Mp4, #[serde(alias = "json")] Json, #[serde(alias = "json3")] Json3, #[serde(alias = "ttml")] Ttml, #[serde(alias = "srv1")] Srv1, #[serde(alias = "srv2")] Srv2, #[serde(alias = "srv3")] Srv3, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Caption { pub ext: SubtitleExt, pub filepath: Option, pub filesize: Option, pub fragments: Option>, pub fragment_base_url: Option, pub manifest_url: Option, pub name: Option, pub protocol: Option, pub url: String, pub video_id: Option, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct SubtitleFragment { path: PathBuf, duration: Option, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Chapter { pub end_time: f64, pub start_time: f64, pub title: String, } #[derive(Debug, Deserialize, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct SponsorblockChapter { /// This is an utterly useless field, and should thus be ignored pub _categories: Option>>, pub categories: Option>, pub category: Option, pub category_names: Option>, pub end_time: f64, pub name: Option, pub r#type: Option, pub start_time: f64, pub title: String, } pub fn get_none<'de, D, T>(_: D) -> Result, D::Error> where D: Deserializer<'de>, { Ok(None) } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)] #[serde(deny_unknown_fields)] pub enum SponsorblockChapterType { #[serde(alias = "skip")] Skip, #[serde(alias = "chapter")] Chapter, #[serde(alias = "poi")] Poi, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)] #[serde(deny_unknown_fields)] pub enum SponsorblockChapterCategory { #[serde(alias = "filler")] Filler, #[serde(alias = "interaction")] Interaction, #[serde(alias = "music_offtopic")] MusicOfftopic, #[serde(alias = "poi_highlight")] PoiHighlight, #[serde(alias = "preview")] Preview, #[serde(alias = "sponsor")] Sponsor, #[serde(alias = "selfpromo")] SelfPromo, #[serde(alias = "chapter")] Chapter, #[serde(alias = "intro")] Intro, #[serde(alias = "outro")] Outro, } #[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, Clone, Copy)] #[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, } #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)] #[serde(deny_unknown_fields)] pub struct Version { pub current_git_head: Option, pub release_git_head: String, pub repository: String, pub version: String, } #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] #[serde(from = "String")] #[serde(deny_unknown_fields)] pub enum Parent { Root, Id(String), } impl Parent { #[must_use] pub fn id(&self) -> Option<&str> { if let Self::Id(id) = self { Some(id) } else { None } } } impl From for Parent { fn from(value: String) -> Self { if value == "root" { Self::Root } else { Self::Id(value) } } } #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)] #[serde(from = "String")] #[serde(deny_unknown_fields)] pub struct Id { pub id: String, } impl From for Id { fn from(value: String) -> Self { Self { // Take the last element if the string is split with dots, otherwise take the full id id: value.split('.').last().unwrap_or(&value).to_owned(), } } } #[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, #[serde(default = "zero")] pub like_count: u32, pub is_pinned: bool, pub author_id: String, #[serde(default = "unknown")] pub author: String, pub author_is_verified: bool, pub author_thumbnail: Url, pub parent: Parent, #[serde(deserialize_with = "edited_from_time_text", alias = "_time_text")] pub edited: bool, // Can't also be deserialized, as it's already used in 'edited' // _time_text: String, pub timestamp: i64, pub author_url: Option, pub author_is_uploader: bool, pub is_favorited: bool, } fn unknown() -> String { "".to_string() } fn zero() -> u32 { 0 } fn edited_from_time_text<'de, D>(d: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(d)?; if s.contains(" (edited)") { Ok(true) } else { Ok(false) } } #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(deny_unknown_fields)] pub struct ThumbNail { pub id: Option, pub preference: Option, /// in the form of "[`height`]x[`width`]" pub resolution: Option, pub url: Url, pub width: Option, pub height: Option, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Format { pub __needs_testing: Option, pub __working: Option, pub abr: Option, pub acodec: Option, pub aspect_ratio: Option, pub asr: Option, pub audio_channels: Option, pub audio_ext: Option, pub columns: Option, pub container: Option, pub downloader_options: Option, pub dynamic_range: Option, pub ext: String, pub filepath: Option, pub filesize: Option, pub filesize_approx: Option, pub format: Option, pub format_id: String, pub format_index: Option, pub format_note: Option, pub fps: Option, pub fragment_base_url: Option, pub fragments: Option>, pub has_drm: Option, pub height: Option, pub http_headers: Option, pub is_dash_periods: Option, pub language: Option, pub language_preference: Option, pub manifest_stream_number: Option, pub manifest_url: Option, pub preference: Option, pub protocol: Option, pub quality: Option, pub resolution: Option, pub rows: Option, pub source_preference: Option, pub tbr: Option, pub url: Url, pub vbr: Option, pub vcodec: String, pub video_ext: Option, pub width: Option, } #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)] #[serde(deny_unknown_fields)] #[allow(missing_copy_implementations)] pub struct DownloaderOptions { http_chunk_size: u64, } #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)] #[serde(deny_unknown_fields)] pub struct HttpHeader { #[serde(alias = "User-Agent")] pub user_agent: Option, #[serde(alias = "Accept")] pub accept: Option, #[serde(alias = "X-Forwarded-For")] pub x_forwarded_for: Option, #[serde(alias = "Accept-Language")] pub accept_language: Option, #[serde(alias = "Sec-Fetch-Mode")] pub sec_fetch_mode: Option, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Fragment { pub url: Option, pub duration: Option, pub path: Option, } impl InfoJson { pub fn to_py_dict(self, py: Python<'_>) -> PyResult> { let output: Bound<'_, PyDict> = json_loads_str(py, self)?; Ok(output) } }