use std::collections::BTreeMap; use std::fmt::Write; use std::fs::File; use std::io::BufReader; use path_macro::path; use reqwest::header; use reqwest::Client; use serde::Deserialize; use serde_with::serde_as; use serde_with::VecSkipError; use crate::model::Text; use crate::util::DICT_DIR; use crate::util::SRC_DIR; #[serde_as] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct LanguageMenu { #[serde_as(as = "VecSkipError<_>")] actions: Vec, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ActionWrap { open_popup_action: OpenPopupAction, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct OpenPopupAction { popup: Popup, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct Popup { multi_page_menu_renderer: MultiPageMenuRenderer, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MultiPageMenuRenderer { sections: Vec>, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MenuSectionRendererWrap { multi_page_menu_section_renderer: T, } #[serde_as] #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MenuSectionRenderer { #[serde_as(as = "VecSkipError<_>")] items: Vec, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct CompactLinkRendererWrap { compact_link_renderer: CompactLinkRenderer, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct CompactLinkRenderer { icon: Icon, service_endpoint: ServiceEndpoint, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Icon { pub icon_type: String, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ServiceEndpoint { signal_service_endpoint: SignalServiceEndpoint, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct SignalServiceEndpoint { actions: Vec, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MenuAction { get_multi_page_menu_action: MultiPageMenuAction, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct MultiPageMenuAction { menu: Menu, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct Menu { multi_page_menu_renderer: MultiPageMenuRenderer, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ItemSectionRenderer { items: Vec, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct LanguageItemWrap { compact_link_renderer: LanguageItem, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct LanguageItem { title: Text, service_endpoint: ServiceEndpoint, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct LanguageCountryAction { #[serde(alias = "selectCountryCommand")] select_language_command: LanguageCountryCommand, } #[derive(Clone, Debug, Deserialize)] struct LanguageCountryCommand { #[serde(alias = "gl")] hl: String, } pub async fn generate_locales() { let (languages, countries) = get_locales().await; let json_path = path!(*DICT_DIR / "lang_names.json"); let json_file = File::open(json_path).unwrap(); let lang_names: BTreeMap = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let code_head = r#"// This file is automatically generated. DO NOT EDIT. //! Languages and countries use std::str::FromStr; use serde::{Deserialize, Serialize}; use crate::error::Error; "#; let code_foot = r#"impl FromStr for Language { type Err = Error; fn from_str(s: &str) -> Result { let mut sub = s; loop { if let Ok(v) = serde_plain::from_str(sub) { return Ok(v); } match sub.rfind('-') { Some(pos) => { sub = &sub[..pos]; } None => return Err(Error::Other("could not parse language `{s}`".into())), } } } } serde_plain::derive_display_from_serialize!(Language); serde_plain::derive_fromstr_from_deserialize!(Country, Error); serde_plain::derive_display_from_serialize!(Country); "#; let mut code_langs = r#"/// Available languages #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum Language { "# .to_owned(); let mut code_countries = r#"/// Available countries #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "UPPERCASE")] #[non_exhaustive] pub enum Country { "# .to_owned(); let mut code_lang_array = format!( r#"/// Array of all available languages /// The languages are sorted by their native names. This array can be used to display /// a language selection or to get the language code from a language name using binary search. pub const LANGUAGES: [Language; {}] = [ "#, languages.len() ); let mut code_country_array = format!( r#"/// Array of all available countries /// /// The countries are sorted by their english names. This array can be used to display /// a country selection or to get the country code from a country name using binary search. pub const COUNTRIES: [Country; {}] = [ "#, countries.len() ); let mut code_lang_names = r#"impl Language { /// Get the native name of the language /// /// Examples: "English (US)", "Deutsch", "中文 (简体)" pub fn name(&self) -> &str { match self { "# .to_owned(); let mut code_country_names = r#"impl Country { /// Get the English name of the country /// /// Examples: "United States", "Germany" pub fn name(&self) -> &str { match self { "# .to_owned(); for (code, native_name) in &languages { let enum_name = code.split('-').fold(String::new(), |mut output, c| { let _ = write!( output, "{}{}", c[0..1].to_owned().to_uppercase(), c[1..].to_owned().to_lowercase() ); output }); let en_name = lang_names.get(code).expect(code); // Language enum if en_name == native_name || code.starts_with("en") { write!(code_langs, " /// {native_name}\n ").unwrap(); } else { write!(code_langs, " /// {en_name} / {native_name}\n ").unwrap(); } if code.contains('-') { write!(code_langs, "#[serde(rename = \"{code}\")]\n ").unwrap(); } code_langs += &enum_name; code_langs += ",\n"; // Language names writeln!( code_lang_names, " Language::{enum_name} => \"{native_name}\"," ) .unwrap(); } code_langs += "}\n"; // Language array let languages_by_name = languages .iter() .map(|(k, v)| (v, k)) .collect::>(); for code in languages_by_name.values() { let enum_name = code.split('-').fold(String::new(), |mut output, c| { let _ = write!( output, "{}{}", c[0..1].to_owned().to_uppercase(), c[1..].to_owned().to_lowercase() ); output }); writeln!(code_lang_array, " Language::{enum_name},").unwrap(); } for (c, n) in &countries { let enum_name = c[0..1].to_owned().to_uppercase() + &c[1..].to_owned().to_lowercase(); // Country enum writeln!(code_countries, " /// {n}").unwrap(); writeln!(code_countries, " {enum_name},").unwrap(); // Country names writeln!( code_country_names, " Country::{enum_name} => \"{n}\"," ) .unwrap(); } // Country array let countries_by_name = countries .iter() .map(|(k, v)| (v, k)) .collect::>(); for c in countries_by_name.values() { let enum_name = c[0..1].to_owned().to_uppercase() + &c[1..].to_owned().to_lowercase(); writeln!(code_country_array, " Country::{enum_name},").unwrap(); } // Add Country::Zz / Global code_countries += " /// Global (can only be used for music charts)\n"; code_countries += " Zz,\n"; code_country_names += " Country::Zz => \"Global\",\n"; code_countries += "}\n"; code_lang_array += "];\n"; code_country_array += "];\n"; code_lang_names += " }\n }\n}\n"; code_country_names += " }\n }\n}\n"; let code = format!( "{code_head}\n{code_langs}\n{code_countries}\n{code_lang_array}\n{code_country_array}\n{code_lang_names}\n{code_country_names}\n{code_foot}" ); let target_path = path!(*SRC_DIR / "param" / "locale.rs"); std::fs::write(target_path, code).unwrap(); } async fn get_locales() -> (BTreeMap, BTreeMap) { let client = Client::new(); let resp = client .post("https://www.youtube.com/youtubei/v1/account/account_menu?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false") .header(header::CONTENT_TYPE, "application/json") .body( r#"{"context":{"client":{"clientName":"WEB","clientVersion":"2.20220914.06.00","platform":"DESKTOP","originalUrl":"https://www.youtube.com/","hl":"en","gl":"US"},"request":{"internalExperimentFlags":[],"useSsl":true},"user":{"lockedSafetyMode":false}}}"# ) .send().await .unwrap() .error_for_status() .unwrap(); let language_menu = resp.json::().await.unwrap(); let lm_section = &language_menu.actions[0] .open_popup_action .popup .multi_page_menu_renderer .sections .iter() .find(|s| s.multi_page_menu_section_renderer.items.len() >= 2) .unwrap(); let lang_section = lm_section .multi_page_menu_section_renderer .items .iter() .find(|s| s.compact_link_renderer.icon.icon_type == "TRANSLATE") .unwrap(); let country_section = lm_section .multi_page_menu_section_renderer .items .iter() .find(|s| s.compact_link_renderer.icon.icon_type == "LANGUAGE") .unwrap(); let languages = map_language_section(lang_section); let countries = map_language_section(country_section); (languages, countries) } fn map_language_section(section: &CompactLinkRendererWrap) -> BTreeMap { section .compact_link_renderer .service_endpoint .signal_service_endpoint .actions[0] .get_multi_page_menu_action .menu .multi_page_menu_renderer .sections[0] .multi_page_menu_section_renderer .items .iter() .map(|i| { ( i.compact_link_renderer .service_endpoint .signal_service_endpoint .actions[0] .select_language_command .hl .clone(), i.compact_link_renderer.title.text.clone(), ) }) .collect() }