406 lines
11 KiB
Rust
406 lines
11 KiB
Rust
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<ActionWrap>,
|
|
}
|
|
|
|
#[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<MenuSectionRenderer>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct MultiPageMenuRenderer<T> {
|
|
sections: Vec<MenuSectionRendererWrap<T>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct MenuSectionRendererWrap<T> {
|
|
multi_page_menu_section_renderer: T,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct MenuSectionRenderer {
|
|
#[serde_as(as = "VecSkipError<_>")]
|
|
items: Vec<CompactLinkRendererWrap>,
|
|
}
|
|
|
|
#[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<MenuAction>,
|
|
}
|
|
|
|
#[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<T> {
|
|
signal_service_endpoint: SignalServiceEndpoint<T>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct SignalServiceEndpoint<T> {
|
|
actions: Vec<T>,
|
|
}
|
|
|
|
#[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<ItemSectionRenderer>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct ItemSectionRenderer {
|
|
items: Vec<LanguageItemWrap>,
|
|
}
|
|
|
|
#[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<LanguageCountryAction>,
|
|
}
|
|
|
|
#[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<String, String> =
|
|
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<Self, Self::Err> {
|
|
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::<BTreeMap<_, _>>();
|
|
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::<BTreeMap<_, _>>();
|
|
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<String, String>, BTreeMap<String, String>) {
|
|
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::<LanguageMenu>().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<String, String> {
|
|
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()
|
|
}
|