// yt - A fully featured command line YouTube client // // Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Yt. // // You should have received a copy of the License along with this program. // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. use std::{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; // 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 { pub __last_playlist_index: Option<u32>, pub __post_extractor: Option<String>, pub __x_forwarded_for_ip: Option<String>, pub _filename: Option<PathBuf>, pub _format_sort_fields: Option<Vec<String>>, pub _has_drm: Option<Todo>, pub _type: Option<InfoType>, pub _version: Option<Version>, pub abr: Option<f64>, pub acodec: Option<String>, pub age_limit: Option<u32>, pub aspect_ratio: Option<f64>, pub asr: Option<u32>, pub audio_channels: Option<u32>, pub audio_ext: Option<String>, pub automatic_captions: Option<HashMap<String, Vec<Caption>>>, pub availability: Option<String>, pub average_rating: Option<String>, pub categories: Option<Vec<String>>, pub channel: Option<String>, pub channel_follower_count: Option<u32>, pub channel_id: Option<String>, pub channel_is_verified: Option<bool>, pub channel_url: Option<String>, pub chapters: Option<Vec<Chapter>>, pub comment_count: Option<u32>, pub comments: Option<Vec<Comment>>, pub concurrent_view_count: Option<u32>, pub description: Option<String>, pub display_id: Option<String>, pub downloader_options: Option<DownloaderOptions>, pub duration: Option<f64>, pub duration_string: Option<String>, pub dynamic_range: Option<String>, pub entries: Option<Vec<InfoJson>>, pub episode: Option<String>, pub episode_number: Option<u32>, pub epoch: Option<u32>, pub ext: Option<String>, pub extractor: Option<Extractor>, pub extractor_key: Option<ExtractorKey>, pub filename: Option<PathBuf>, pub filesize: Option<u64>, pub filesize_approx: Option<u64>, pub format: Option<String>, pub format_id: Option<String>, pub format_note: Option<String>, pub formats: Option<Vec<Format>>, pub fps: Option<f64>, pub fulltitle: Option<String>, pub has_drm: Option<bool>, pub heatmap: Option<Vec<HeatMapEntry>>, pub height: Option<u32>, pub http_headers: Option<HttpHeader>, pub id: Option<String>, pub ie_key: Option<ExtractorKey>, pub is_live: Option<bool>, pub language: Option<String>, pub language_preference: Option<i32>, pub license: Option<Todo>, pub like_count: Option<u32>, pub live_status: Option<String>, pub location: Option<Todo>, pub modified_date: Option<String>, pub n_entries: Option<u32>, pub original_url: Option<String>, pub playable_in_embed: Option<bool>, pub playlist: Option<Todo>, pub playlist_autonumber: Option<u32>, pub playlist_channel: Option<Todo>, pub playlist_channel_id: Option<Todo>, pub playlist_count: Option<u32>, pub playlist_id: Option<Todo>, pub playlist_index: Option<u64>, pub playlist_title: Option<Todo>, pub playlist_uploader: Option<Todo>, pub playlist_uploader_id: Option<Todo>, pub preference: Option<Todo>, pub protocol: Option<String>, pub quality: Option<f64>, pub release_date: Option<String>, pub release_timestamp: Option<u64>, pub release_year: Option<u32>, pub requested_downloads: Option<Vec<RequestedDownloads>>, pub requested_entries: Option<Vec<u32>>, pub requested_formats: Option<Vec<Format>>, pub requested_subtitles: Option<HashMap<String, Subtitle>>, pub resolution: Option<String>, pub season: Option<String>, pub season_number: Option<u32>, pub series: Option<String>, pub source_preference: Option<i32>, pub sponsorblock_chapters: Option<Vec<SponsorblockChapter>>, pub stretched_ratio: Option<Todo>, pub subtitles: Option<HashMap<String, Vec<Caption>>>, pub tags: Option<Vec<String>>, pub tbr: Option<f64>, pub thumbnail: Option<Url>, pub thumbnails: Option<Vec<ThumbNail>>, pub timestamp: Option<u64>, pub title: Option<String>, pub upload_date: Option<String>, pub uploader: Option<String>, pub uploader_id: Option<String>, pub uploader_url: Option<String>, pub url: Option<Url>, pub vbr: Option<f64>, pub vcodec: Option<String>, pub video_ext: Option<String>, pub view_count: Option<u32>, pub was_live: Option<bool>, pub webpage_url: Option<Url>, pub webpage_url_basename: Option<String>, pub webpage_url_domain: Option<String>, pub width: Option<u32>, } #[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: f64, pub asr: u32, pub audio_channels: u32, pub chapters: Option<Vec<SponsorblockChapter>>, pub duration: Option<f64>, pub dynamic_range: String, pub ext: String, pub filename: PathBuf, pub filepath: PathBuf, pub filesize_approx: u64, pub format: String, pub format_id: String, pub format_note: String, pub fps: f64, pub height: u32, pub infojson_filename: PathBuf, pub language: Option<String>, pub protocol: String, pub requested_formats: Vec<Format>, pub resolution: String, pub tbr: f64, pub vbr: f64, pub vcodec: String, pub width: u32, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)] #[serde(deny_unknown_fields)] pub struct Subtitle { pub ext: SubtitleExt, pub filepath: PathBuf, pub name: String, pub url: Url, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)] #[serde(deny_unknown_fields)] pub enum SubtitleExt { #[serde(alias = "vtt")] Vtt, #[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, Eq, PartialOrd, Ord)] #[serde(deny_unknown_fields)] pub struct Caption { pub ext: SubtitleExt, pub name: Option<String>, pub protocol: Option<String>, pub url: String, pub filepath: Option<PathBuf>, pub video_id: Option<String>, } #[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)] #[serde(deny_unknown_fields)] pub enum SponsorblockChapterType { #[serde(alias = "skip")] Skip, #[serde(alias = "chapter")] Chapter, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)] #[serde(deny_unknown_fields)] pub enum SponsorblockChapterCategory { #[serde(alias = "filler")] Filler, #[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)] pub struct HeatMapEntry { pub start_time: f64, pub end_time: f64, pub value: f64, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)] #[serde(deny_unknown_fields)] pub enum Extractor { #[serde(alias = "generic")] Generic, #[serde(alias = "SVTSeries")] SVTSeries, #[serde(alias = "youtube")] YouTube, #[serde(alias = "youtube:tab")] YouTubeTab, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)] #[serde(deny_unknown_fields)] pub enum ExtractorKey { #[serde(alias = "Generic")] Generic, #[serde(alias = "SVTSeries")] SVTSeries, #[serde(alias = "Youtube")] YouTube, #[serde(alias = "YoutubeTab")] YouTubeTab, } #[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)] #[serde(deny_unknown_fields)] pub enum InfoType { #[serde(alias = "playlist")] Playlist, #[serde(alias = "url")] Url, #[serde(alias = "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 { 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)] 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: 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)] 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 = "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) } }