// 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>. // `yt_dlp` named them like this. #![allow(clippy::pub_underscore_fields)] use std::{collections::HashMap, path::PathBuf}; use pyo3::{Bound, PyResult, Python, types::PyDict}; 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<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>>, #[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>, } #[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<Vec<Todo>>, pub __finaldir: PathBuf, pub __infojson_filename: PathBuf, pub __postprocessors: Vec<Todo>, 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<f64>, pub asr: Option<u32>, pub audio_channels: Option<u32>, pub audio_ext: Option<String>, pub chapters: Option<Vec<SponsorblockChapter>>, pub duration: Option<f64>, pub dynamic_range: Option<String>, pub ext: String, pub filename: PathBuf, pub filepath: PathBuf, pub filesize_approx: Option<u64>, pub format: String, pub format_id: String, pub format_note: Option<String>, pub fps: Option<f64>, pub has_drm: Option<bool>, pub height: Option<u32>, pub http_headers: Option<HttpHeader>, pub infojson_filename: PathBuf, 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, pub url: Option<Url>, pub vbr: f64, pub vcodec: String, pub video_ext: Option<String>, pub width: Option<u32>, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Subtitle { pub ext: SubtitleExt, pub filepath: PathBuf, pub filesize: Option<u64>, pub fragment_base_url: Option<Url>, pub fragments: Option<Vec<Fragment>>, pub manifest_url: Option<Url>, pub name: Option<String>, pub protocol: Option<Todo>, 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<PathBuf>, pub filesize: Option<u64>, pub fragments: Option<Vec<SubtitleFragment>>, pub fragment_base_url: Option<Url>, pub manifest_url: Option<Url>, pub name: Option<String>, pub protocol: Option<String>, pub url: String, pub video_id: Option<String>, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct SubtitleFragment { path: PathBuf, duration: Option<f64>, } #[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<Vec<Vec<Value>>>, pub categories: Option<Vec<SponsorblockChapterCategory>>, pub category: Option<SponsorblockChapterCategory>, pub category_names: Option<Vec<String>>, pub end_time: f64, pub name: Option<String>, pub r#type: Option<SponsorblockChapterType>, pub start_time: f64, pub title: String, } pub fn get_none<'de, D, T>(_: D) -> Result<Option<T>, 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<String>, 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<String> 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<String> 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<Url>, pub author_is_uploader: bool, pub is_favorited: bool, } fn unknown() -> String { "<Unknown>".to_string() } fn zero() -> u32 { 0 } fn edited_from_time_text<'de, D>(d: D) -> Result<bool, D::Error> 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<String>, pub preference: Option<i32>, /// in the form of "[`height`]x[`width`]" pub resolution: Option<String>, pub url: Url, pub width: Option<u32>, pub height: Option<u32>, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Format { pub __needs_testing: Option<bool>, pub __working: Option<bool>, pub abr: Option<f64>, pub acodec: Option<String>, pub aspect_ratio: Option<f64>, pub asr: Option<f64>, pub audio_channels: Option<u32>, pub audio_ext: Option<String>, pub columns: Option<u32>, pub container: Option<String>, pub downloader_options: Option<DownloaderOptions>, pub dynamic_range: Option<String>, pub ext: String, pub filepath: Option<PathBuf>, pub filesize: Option<u64>, pub filesize_approx: Option<u64>, pub format: Option<String>, pub format_id: String, pub format_index: Option<String>, pub format_note: Option<String>, pub fps: Option<f64>, pub fragment_base_url: Option<Todo>, pub fragments: Option<Vec<Fragment>>, pub has_drm: Option<bool>, pub height: Option<u32>, pub http_headers: Option<HttpHeader>, pub is_dash_periods: Option<bool>, pub language: Option<String>, pub language_preference: Option<i32>, pub manifest_stream_number: Option<u32>, pub manifest_url: Option<Url>, pub preference: Option<i32>, pub protocol: Option<String>, pub quality: Option<f64>, pub resolution: Option<String>, pub rows: Option<u32>, pub source_preference: Option<i32>, pub tbr: Option<f64>, pub url: Url, pub vbr: Option<f64>, pub vcodec: String, pub video_ext: Option<String>, pub width: Option<u32>, } #[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<String>, #[serde(alias = "Accept")] pub accept: Option<String>, #[serde(alias = "X-Forwarded-For")] pub x_forwarded_for: Option<String>, #[serde(alias = "Accept-Language")] pub accept_language: Option<String>, #[serde(alias = "Sec-Fetch-Mode")] pub sec_fetch_mode: Option<String>, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)] #[serde(deny_unknown_fields)] pub struct Fragment { pub url: Option<Url>, pub duration: Option<f64>, pub path: Option<PathBuf>, } impl InfoJson { pub fn to_py_dict(self, py: Python<'_>) -> PyResult<Bound<'_, PyDict>> { let output: Bound<'_, PyDict> = json_loads_str(py, self)?; Ok(output) } }