Compare commits
7 commits
5b8c3d646a
...
513bf1dc9c
Author | SHA1 | Date | |
---|---|---|---|
513bf1dc9c | |||
500ea77788 | |||
cc63477406 | |||
2bba9064fc | |||
17094d121b | |||
9da166304a | |||
346406c1c8 |
41 changed files with 58339 additions and 9327 deletions
|
@ -29,6 +29,7 @@ futures = "0.3.21"
|
||||||
indicatif = "0.17.0"
|
indicatif = "0.17.0"
|
||||||
filenamify = "0.1.0"
|
filenamify = "0.1.0"
|
||||||
ress = "0.11.4"
|
ress = "0.11.4"
|
||||||
|
phf = "0.11.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
|
@ -37,3 +38,6 @@ rstest = "0.15.0"
|
||||||
temp_testdir = "0.2.3"
|
temp_testdir = "0.2.3"
|
||||||
insta = "1.17.1"
|
insta = "1.17.1"
|
||||||
velcro = "0.5.3"
|
velcro = "0.5.3"
|
||||||
|
unic-langid = "0.9.0"
|
||||||
|
intl_pluralrules = "7.0.1"
|
||||||
|
phf_codegen = "0.11.1"
|
||||||
|
|
|
@ -59,7 +59,7 @@ async fn download_single_video(
|
||||||
|
|
||||||
let res = async {
|
let res = async {
|
||||||
let player_data = rt
|
let player_data = rt
|
||||||
.get_player(video_id.as_str(), ClientType::Desktop)
|
.get_player(video_id.as_str(), ClientType::TvHtml5Embed)
|
||||||
.await
|
.await
|
||||||
.context(format!(
|
.context(format!(
|
||||||
"Failed to fetch player data for video {}",
|
"Failed to fetch player data for video {}",
|
||||||
|
|
5699
notes/language_menu.json
Normal file
5699
notes/language_menu.json
Normal file
File diff suppressed because it is too large
Load diff
4790
notes/next/comment_replies.json
Normal file
4790
notes/next/comment_replies.json
Normal file
File diff suppressed because it is too large
Load diff
9699
notes/next/next_comments_latest.json
Normal file
9699
notes/next/next_comments_latest.json
Normal file
File diff suppressed because it is too large
Load diff
7184
notes/next/next_comments_latest_nouser.json
Normal file
7184
notes/next/next_comments_latest_nouser.json
Normal file
File diff suppressed because it is too large
Load diff
10899
notes/next/next_mv.json
Normal file
10899
notes/next/next_mv.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -26,3 +26,25 @@ Throttling issue: Y8JFxS1HlDo
|
||||||
495 Songs: PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ
|
495 Songs: PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ
|
||||||
78 Videos: PL2_OBreMn7FpDFj9lWfoZ8OQJvZkQa3yG
|
78 Videos: PL2_OBreMn7FpDFj9lWfoZ8OQJvZkQa3yG
|
||||||
66 Videos: PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe
|
66 Videos: PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe
|
||||||
|
4.657 Songs: PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX
|
||||||
|
186 Songs: PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi
|
||||||
|
|
||||||
|
Playlist update dates (05.09.2022):
|
||||||
|
today: RDCLAK5uy_kj3rhiar1LINmyDcuFnXihEO0K1NQa2jI
|
||||||
|
yesterday: PL4C44E2875308A280
|
||||||
|
2 days ago: PL7zsB-C3aNu2yRY2869T0zj1FhtRIu5am
|
||||||
|
5 days ago: PL3-sRm8xAzY9sDilvaWjCwCI0TkUzYdOG
|
||||||
|
7 days ago: PLHr0jWPfopte182N54r1ra7tkRJC1fmPu
|
||||||
|
|
||||||
|
Jan PL1J-6JOckZtHxTA3hN5SK7gBQaFfKzeXr 01.01.2016
|
||||||
|
Feb PL1J-6JOckZtETrbzwZE7mRIIK6BzWNLAs 07.02.2016
|
||||||
|
Mar PL1J-6JOckZtG3AVdvBXhMO64mB2k3BtKi 09.03.2015
|
||||||
|
Apr PL1J-6JOckZtE_rUpK24S6X5hOE4eQoprN 02.04.2017
|
||||||
|
May PL1J-6JOckZtG1ThBxoSLFL-Jg4sa2iX_a 22.05.2014
|
||||||
|
Jun PL1J-6JOckZtF_wSzkXBl91pit9d6Fh0QF 28.07.2014
|
||||||
|
Jul PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe 02.07.2014
|
||||||
|
Aug PL1J-6JOckZtFFQeWx-ZC0ubpJCEWmGWRx 23.08.2015
|
||||||
|
Sep PL1J-6JOckZtHVs0JhBW_qfsW-dtXuM0mQ 16.09.2018
|
||||||
|
Oct PL1J-6JOckZtE4g-XgZkL_N0kkoKui5Eys 31.10.2014
|
||||||
|
Nov PL1J-6JOckZtEzjMUEyPyPpG836pjeIapw 03.11.2016
|
||||||
|
Dec PL1J-6JOckZtHo91uApeb10Qlf2XhkfM-9 24.12.2021
|
||||||
|
|
41
src/client/channel.rs
Normal file
41
src/client/channel.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::{response, ClientType, ContextYT, RustyTube};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct QChannel {
|
||||||
|
context: ContextYT,
|
||||||
|
browse_id: String,
|
||||||
|
params: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustyTube {
|
||||||
|
async fn get_channel_response(&self, channel_id: &str) -> Result<response::Channel> {
|
||||||
|
let client = self.get_ytclient(ClientType::Desktop);
|
||||||
|
let context = client.get_context(true).await;
|
||||||
|
|
||||||
|
let request_body = QChannel {
|
||||||
|
context,
|
||||||
|
browse_id: channel_id.to_owned(),
|
||||||
|
params: "EgZ2aWRlb3PyBgQKAjoA".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.request_builder(Method::POST, "browse")
|
||||||
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(resp.json::<response::Channel>().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod playlist;
|
pub mod playlist;
|
||||||
|
pub mod video;
|
||||||
|
|
||||||
mod response;
|
mod response;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -15,7 +17,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cache::{Cache, ClientData},
|
cache::{Cache, ClientData},
|
||||||
model::Locale,
|
model::{Country, Language},
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -68,10 +70,8 @@ struct ClientInfo {
|
||||||
platform: String,
|
platform: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
original_url: Option<String>,
|
original_url: Option<String>,
|
||||||
/// Language (`en`, `de`)
|
hl: Language,
|
||||||
hl: String,
|
gl: Country,
|
||||||
/// Country (`US`, `DE`)
|
|
||||||
gl: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
@ -127,7 +127,7 @@ const ANDROID_API_KEY: &str = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w";
|
||||||
const IOS_API_KEY: &str = "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc";
|
const IOS_API_KEY: &str = "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc";
|
||||||
const IOS_DEVICE_MODEL: &str = "iPhone14,5";
|
const IOS_DEVICE_MODEL: &str = "iPhone14,5";
|
||||||
|
|
||||||
const CLIENT_VERSION_REGEXES: Lazy<[Regex; 3]> = Lazy::new(|| {
|
static CLIENT_VERSION_REGEXES: Lazy<[Regex; 3]> = Lazy::new(|| {
|
||||||
[
|
[
|
||||||
Regex::new("INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"").unwrap(),
|
Regex::new("INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"").unwrap(),
|
||||||
Regex::new("innertube_context_client_version\":\"([0-9\\.]+?)\"").unwrap(),
|
Regex::new("innertube_context_client_version\":\"([0-9\\.]+?)\"").unwrap(),
|
||||||
|
@ -136,7 +136,7 @@ const CLIENT_VERSION_REGEXES: Lazy<[Regex; 3]> = Lazy::new(|| {
|
||||||
});
|
});
|
||||||
|
|
||||||
pub struct RustyTube {
|
pub struct RustyTube {
|
||||||
pub locale: Arc<Locale>,
|
localization: Arc<Localization>,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
desktop_client: Arc<DesktopClient>,
|
desktop_client: Arc<DesktopClient>,
|
||||||
desktop_music_client: Arc<DesktopMusicClient>,
|
desktop_music_client: Arc<DesktopMusicClient>,
|
||||||
|
@ -145,17 +145,30 @@ pub struct RustyTube {
|
||||||
tvhtml5embed_client: Arc<TvHtml5EmbedClient>,
|
tvhtml5embed_client: Arc<TvHtml5EmbedClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Localization {
|
||||||
|
language: Language,
|
||||||
|
content_country: Country,
|
||||||
|
}
|
||||||
|
|
||||||
impl RustyTube {
|
impl RustyTube {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::new_with_ua("en", "US", Some("rusty-tube.json".to_owned()))
|
Self::new_with_ua(
|
||||||
|
Language::En,
|
||||||
|
Country::Us,
|
||||||
|
Some("rusty-tube.json".to_owned()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new_with_ua(lang: &str, country: &str, cache_file: Option<String>) -> Self {
|
pub fn new_with_ua(
|
||||||
let locale = Arc::new(Locale {
|
language: Language,
|
||||||
lang: lang.to_owned(),
|
content_country: Country,
|
||||||
country: country.to_owned(),
|
cache_file: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
let loc = Arc::new(Localization {
|
||||||
|
language,
|
||||||
|
content_country,
|
||||||
});
|
});
|
||||||
|
|
||||||
let cache = match cache_file.as_ref() {
|
let cache = match cache_file.as_ref() {
|
||||||
|
@ -164,17 +177,17 @@ impl RustyTube {
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
locale: locale.clone(),
|
localization: loc.clone(),
|
||||||
cache: cache.clone(),
|
cache: cache.clone(),
|
||||||
desktop_client: Arc::new(DesktopClient::new(locale.clone(), cache.clone())),
|
desktop_client: Arc::new(DesktopClient::new(loc.clone(), cache.clone())),
|
||||||
desktop_music_client: Arc::new(DesktopMusicClient::new(locale.clone(), cache)),
|
desktop_music_client: Arc::new(DesktopMusicClient::new(loc.clone(), cache)),
|
||||||
android_client: Arc::new(AndroidClient::new(locale.clone())),
|
android_client: Arc::new(AndroidClient::new(loc.clone())),
|
||||||
ios_client: Arc::new(IosClient::new(locale.clone())),
|
ios_client: Arc::new(IosClient::new(loc.clone())),
|
||||||
tvhtml5embed_client: Arc::new(TvHtml5EmbedClient::new(locale)),
|
tvhtml5embed_client: Arc::new(TvHtml5EmbedClient::new(loc)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ytclient(&self, client_type: ClientType) -> Arc<dyn YTClient> {
|
pub(crate) fn get_ytclient(&self, client_type: ClientType) -> Arc<dyn YTClient> {
|
||||||
match client_type {
|
match client_type {
|
||||||
ClientType::Desktop => self.desktop_client.clone(),
|
ClientType::Desktop => self.desktop_client.clone(),
|
||||||
ClientType::DesktopMusic => self.desktop_music_client.clone(),
|
ClientType::DesktopMusic => self.desktop_music_client.clone(),
|
||||||
|
@ -202,7 +215,7 @@ async fn exec_request_text(http: Client, request: Request) -> Result<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DesktopClient {
|
pub struct DesktopClient {
|
||||||
locale: Arc<Locale>,
|
localization: Arc<Localization>,
|
||||||
http: Client,
|
http: Client,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
consent_cookie: String,
|
consent_cookie: String,
|
||||||
|
@ -220,12 +233,12 @@ impl YTClient for DesktopClient {
|
||||||
platform: "DESKTOP".to_owned(),
|
platform: "DESKTOP".to_owned(),
|
||||||
original_url: Some("https://www.youtube.com/".to_owned()),
|
original_url: Some("https://www.youtube.com/".to_owned()),
|
||||||
hl: match localized {
|
hl: match localized {
|
||||||
true => self.locale.lang.to_owned(),
|
true => self.localization.language,
|
||||||
false => "en".to_owned(),
|
false => Language::En,
|
||||||
},
|
},
|
||||||
gl: match localized {
|
gl: match localized {
|
||||||
true => self.locale.country.to_owned(),
|
true => self.localization.content_country,
|
||||||
false => "US".to_owned(),
|
false => Country::Us,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
request: Some(RequestYT::default()),
|
request: Some(RequestYT::default()),
|
||||||
|
@ -260,7 +273,7 @@ impl YTClient for DesktopClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DesktopClient {
|
impl DesktopClient {
|
||||||
fn new(locale: Arc<Locale>, cache: Cache) -> Self {
|
fn new(localization: Arc<Localization>, cache: Cache) -> Self {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
let http = ClientBuilder::new()
|
let http = ClientBuilder::new()
|
||||||
|
@ -271,7 +284,7 @@ impl DesktopClient {
|
||||||
.expect("unable to build the HTTP client");
|
.expect("unable to build the HTTP client");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
locale,
|
localization,
|
||||||
http,
|
http,
|
||||||
cache,
|
cache,
|
||||||
consent_cookie: format!(
|
consent_cookie: format!(
|
||||||
|
@ -329,7 +342,7 @@ impl DesktopClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AndroidClient {
|
pub struct AndroidClient {
|
||||||
locale: Arc<Locale>,
|
localization: Arc<Localization>,
|
||||||
http: Client,
|
http: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,12 +358,12 @@ impl YTClient for AndroidClient {
|
||||||
platform: "MOBILE".to_owned(),
|
platform: "MOBILE".to_owned(),
|
||||||
original_url: None,
|
original_url: None,
|
||||||
hl: match localized {
|
hl: match localized {
|
||||||
true => self.locale.lang.to_owned(),
|
true => self.localization.language,
|
||||||
false => "en".to_owned(),
|
false => Language::En,
|
||||||
},
|
},
|
||||||
gl: match localized {
|
gl: match localized {
|
||||||
true => self.locale.country.to_owned(),
|
true => self.localization.content_country,
|
||||||
false => "US".to_owned(),
|
false => Country::Us,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
request: None,
|
request: None,
|
||||||
|
@ -384,22 +397,22 @@ impl YTClient for AndroidClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AndroidClient {
|
impl AndroidClient {
|
||||||
fn new(locale: Arc<Locale>) -> Self {
|
fn new(localization: Arc<Localization>) -> Self {
|
||||||
let http = ClientBuilder::new()
|
let http = ClientBuilder::new()
|
||||||
.user_agent(format!(
|
.user_agent(format!(
|
||||||
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
|
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
|
||||||
MOBILE_CLIENT_VERSION, locale.country
|
MOBILE_CLIENT_VERSION, localization.content_country
|
||||||
))
|
))
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.build()
|
.build()
|
||||||
.expect("unable to build the HTTP client");
|
.expect("unable to build the HTTP client");
|
||||||
|
|
||||||
Self { locale, http }
|
Self { localization, http }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct IosClient {
|
pub struct IosClient {
|
||||||
locale: Arc<Locale>,
|
localization: Arc<Localization>,
|
||||||
http: Client,
|
http: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -415,12 +428,12 @@ impl YTClient for IosClient {
|
||||||
platform: "MOBILE".to_owned(),
|
platform: "MOBILE".to_owned(),
|
||||||
original_url: None,
|
original_url: None,
|
||||||
hl: match localized {
|
hl: match localized {
|
||||||
true => self.locale.lang.to_owned(),
|
true => self.localization.language,
|
||||||
false => "en".to_owned(),
|
false => Language::En,
|
||||||
},
|
},
|
||||||
gl: match localized {
|
gl: match localized {
|
||||||
true => self.locale.country.to_owned(),
|
true => self.localization.content_country,
|
||||||
false => "US".to_owned(),
|
false => Country::Us,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
request: None,
|
request: None,
|
||||||
|
@ -451,22 +464,22 @@ impl YTClient for IosClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IosClient {
|
impl IosClient {
|
||||||
fn new(locale: Arc<Locale>) -> Self {
|
fn new(localization: Arc<Localization>) -> Self {
|
||||||
let http = ClientBuilder::new()
|
let http = ClientBuilder::new()
|
||||||
.user_agent(format!(
|
.user_agent(format!(
|
||||||
"com.google.ios.youtube/{} ({}; U; CPU iOS 15_4 like Mac OS X; {})",
|
"com.google.ios.youtube/{} ({}; U; CPU iOS 15_4 like Mac OS X; {})",
|
||||||
MOBILE_CLIENT_VERSION, IOS_DEVICE_MODEL, locale.country
|
MOBILE_CLIENT_VERSION, IOS_DEVICE_MODEL, localization.content_country
|
||||||
))
|
))
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.build()
|
.build()
|
||||||
.expect("unable to build the HTTP client");
|
.expect("unable to build the HTTP client");
|
||||||
|
|
||||||
Self { locale, http }
|
Self { localization, http }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TvHtml5EmbedClient {
|
pub struct TvHtml5EmbedClient {
|
||||||
locale: Arc<Locale>,
|
localization: Arc<Localization>,
|
||||||
http: Client,
|
http: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,12 +495,12 @@ impl YTClient for TvHtml5EmbedClient {
|
||||||
platform: "TV".to_owned(),
|
platform: "TV".to_owned(),
|
||||||
original_url: None,
|
original_url: None,
|
||||||
hl: match localized {
|
hl: match localized {
|
||||||
true => self.locale.lang.to_owned(),
|
true => self.localization.language,
|
||||||
false => "en".to_owned(),
|
false => Language::En,
|
||||||
},
|
},
|
||||||
gl: match localized {
|
gl: match localized {
|
||||||
true => self.locale.country.to_owned(),
|
true => self.localization.content_country,
|
||||||
false => "US".to_owned(),
|
false => Country::Us,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
request: Some(RequestYT::default()),
|
request: Some(RequestYT::default()),
|
||||||
|
@ -523,7 +536,7 @@ impl YTClient for TvHtml5EmbedClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TvHtml5EmbedClient {
|
impl TvHtml5EmbedClient {
|
||||||
fn new(locale: Arc<Locale>) -> Self {
|
fn new(localization: Arc<Localization>) -> Self {
|
||||||
let http = ClientBuilder::new()
|
let http = ClientBuilder::new()
|
||||||
.user_agent(DEFAULT_UA)
|
.user_agent(DEFAULT_UA)
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
|
@ -531,12 +544,12 @@ impl TvHtml5EmbedClient {
|
||||||
.build()
|
.build()
|
||||||
.expect("unable to build the HTTP client");
|
.expect("unable to build the HTTP client");
|
||||||
|
|
||||||
Self { locale, http }
|
Self { localization, http }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DesktopMusicClient {
|
pub struct DesktopMusicClient {
|
||||||
locale: Arc<Locale>,
|
localization: Arc<Localization>,
|
||||||
http: Client,
|
http: Client,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
consent_cookie: String,
|
consent_cookie: String,
|
||||||
|
@ -554,12 +567,12 @@ impl YTClient for DesktopMusicClient {
|
||||||
platform: "DESKTOP".to_owned(),
|
platform: "DESKTOP".to_owned(),
|
||||||
original_url: Some("https://music.youtube.com/".to_owned()),
|
original_url: Some("https://music.youtube.com/".to_owned()),
|
||||||
hl: match localized {
|
hl: match localized {
|
||||||
true => self.locale.lang.to_owned(),
|
true => self.localization.language,
|
||||||
false => "en".to_owned(),
|
false => Language::En,
|
||||||
},
|
},
|
||||||
gl: match localized {
|
gl: match localized {
|
||||||
true => self.locale.country.to_owned(),
|
true => self.localization.content_country,
|
||||||
false => "US".to_owned(),
|
false => Country::Us,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
request: Some(RequestYT::default()),
|
request: Some(RequestYT::default()),
|
||||||
|
@ -597,7 +610,7 @@ impl YTClient for DesktopMusicClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DesktopMusicClient {
|
impl DesktopMusicClient {
|
||||||
fn new(locale: Arc<Locale>, cache: Cache) -> Self {
|
fn new(localization: Arc<Localization>, cache: Cache) -> Self {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
let http = ClientBuilder::new()
|
let http = ClientBuilder::new()
|
||||||
|
@ -608,7 +621,7 @@ impl DesktopMusicClient {
|
||||||
.expect("unable to build the HTTP client");
|
.expect("unable to build the HTTP client");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
locale,
|
localization,
|
||||||
http,
|
http,
|
||||||
cache,
|
cache,
|
||||||
consent_cookie: format!(
|
consent_cookie: format!(
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use chrono::{DateTime, NaiveDateTime, NaiveTime, Utc};
|
use chrono::{NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
@ -16,8 +16,6 @@ use url::Url;
|
||||||
use super::{response, ClientType, ContextYT, RustyTube, YTClient};
|
use super::{response, ClientType, ContextYT, RustyTube, YTClient};
|
||||||
use crate::{client::response::player, deobfuscate::Deobfuscator, model::*, util};
|
use crate::{client::response::player, deobfuscate::Deobfuscator, model::*, util};
|
||||||
|
|
||||||
// REQUEST
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QPlayer {
|
struct QPlayer {
|
||||||
|
@ -369,7 +367,7 @@ fn map_player_data(response: response::Player, deobf: &Deobfuscator) -> Result<V
|
||||||
},
|
},
|
||||||
publish_date: microformat.as_ref().map(|m| {
|
publish_date: microformat.as_ref().map(|m| {
|
||||||
let ndt = NaiveDateTime::new(m.publish_date, NaiveTime::from_hms(0, 0, 0));
|
let ndt = NaiveDateTime::new(m.publish_date, NaiveTime::from_hms(0, 0, 0));
|
||||||
DateTime::from_utc(ndt, Utc)
|
Utc.from_local_datetime(&ndt).unwrap()
|
||||||
}),
|
}),
|
||||||
view_count: video_details.view_count,
|
view_count: video_details.view_count,
|
||||||
keywords: video_details
|
keywords: video_details
|
||||||
|
@ -532,9 +530,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assert equality within 10% margin
|
/// Assert equality within 10% margin
|
||||||
fn assert_approx(left: u32, right: u32) {
|
fn assert_approx(left: f64, right: f64) {
|
||||||
if left != right {
|
if left != right {
|
||||||
let f = left as f64 / right as f64;
|
let f = left / right;
|
||||||
assert!(
|
assert!(
|
||||||
0.9 < f && f < 1.1,
|
0.9 < f && f < 1.1,
|
||||||
"{} not within 10% margin of {}",
|
"{} not within 10% margin of {}",
|
||||||
|
@ -595,7 +593,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Bitrates may change between requests
|
// Bitrates may change between requests
|
||||||
assert_approx(video.bitrate, 1507068);
|
assert_approx(video.bitrate as f64, 1507068.0);
|
||||||
assert_eq!(video.average_bitrate, 1345149);
|
assert_eq!(video.average_bitrate, 1345149);
|
||||||
assert_eq!(video.size, 43553412);
|
assert_eq!(video.size, 43553412);
|
||||||
assert_eq!(video.width, 1280);
|
assert_eq!(video.width, 1280);
|
||||||
|
@ -607,7 +605,7 @@ mod tests {
|
||||||
assert_eq!(video.format, VideoFormat::Webm);
|
assert_eq!(video.format, VideoFormat::Webm);
|
||||||
assert_eq!(video.codec, VideoCodec::Vp9);
|
assert_eq!(video.codec, VideoCodec::Vp9);
|
||||||
|
|
||||||
assert_approx(audio.bitrate, 130685);
|
assert_approx(audio.bitrate as f64, 130685.0);
|
||||||
assert_eq!(audio.average_bitrate, 129496);
|
assert_eq!(audio.average_bitrate, 129496);
|
||||||
assert_eq!(audio.size, 4193863);
|
assert_eq!(audio.size, 4193863);
|
||||||
assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\"");
|
assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\"");
|
||||||
|
@ -625,9 +623,9 @@ mod tests {
|
||||||
.find(|s| s.itag == 251)
|
.find(|s| s.itag == 251)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_approx(video.bitrate, 1340829);
|
assert_approx(video.bitrate as f64, 1340829.0);
|
||||||
assert_eq!(video.average_bitrate, 1233444);
|
assert_approx(video.average_bitrate as f64, 1233444.0);
|
||||||
assert_eq!(video.size, 39936630);
|
assert_approx(video.size as f64, 39936630.0);
|
||||||
assert_eq!(video.width, 1280);
|
assert_eq!(video.width, 1280);
|
||||||
assert_eq!(video.height, 720);
|
assert_eq!(video.height, 720);
|
||||||
assert_eq!(video.fps, 30);
|
assert_eq!(video.fps, 30);
|
||||||
|
@ -638,9 +636,9 @@ mod tests {
|
||||||
assert_eq!(video.codec, VideoCodec::Av01);
|
assert_eq!(video.codec, VideoCodec::Av01);
|
||||||
assert_eq!(video.throttled, false);
|
assert_eq!(video.throttled, false);
|
||||||
|
|
||||||
assert_approx(audio.bitrate, 142718);
|
assert_approx(audio.bitrate as f64, 142718.0);
|
||||||
assert_eq!(audio.average_bitrate, 130708);
|
assert_approx(audio.average_bitrate as f64, 130708.0);
|
||||||
assert_eq!(audio.size, 4232344);
|
assert_approx(audio.size as f64, 4232344.0);
|
||||||
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
||||||
assert_eq!(audio.format, AudioFormat::Webm);
|
assert_eq!(audio.format, AudioFormat::Webm);
|
||||||
assert_eq!(audio.codec, AudioCodec::Opus);
|
assert_eq!(audio.codec, AudioCodec::Opus);
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
// REQUEST
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
@ -18,10 +16,11 @@ struct QPlaylist {
|
||||||
browse_id: String,
|
browse_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct TmpEntry {
|
#[serde(rename_all = "camelCase")]
|
||||||
pub title: String,
|
struct QPlaylistCont {
|
||||||
pub video_id: String,
|
context: ContextYT,
|
||||||
|
continuation: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustyTube {
|
impl RustyTube {
|
||||||
|
@ -46,6 +45,51 @@ impl RustyTube {
|
||||||
|
|
||||||
map_playlist(&playlist_response)
|
map_playlist(&playlist_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_playlist_cont(&self, playlist: &mut Playlist) -> Result<()> {
|
||||||
|
match &playlist.ctoken {
|
||||||
|
Some(ctoken) => {
|
||||||
|
let client = self.get_ytclient(ClientType::Desktop);
|
||||||
|
let context = client.get_context(true).await;
|
||||||
|
|
||||||
|
let request_body = QPlaylistCont {
|
||||||
|
context,
|
||||||
|
continuation: ctoken.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.request_builder(Method::POST, "browse")
|
||||||
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
let cont_response = resp.json::<response::playlist::PlaylistCont>().await?;
|
||||||
|
|
||||||
|
let action = some_or_bail!(
|
||||||
|
cont_response
|
||||||
|
.on_response_received_actions
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.append_continuation_items_action.target_id == playlist.id),
|
||||||
|
Err(anyhow!("no continuation action"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let (mut videos, ctoken) =
|
||||||
|
map_playlist_items(&action.append_continuation_items_action.continuation_items);
|
||||||
|
|
||||||
|
playlist.videos.append(&mut videos);
|
||||||
|
playlist.ctoken = ctoken;
|
||||||
|
|
||||||
|
if playlist.ctoken.is_none() {
|
||||||
|
playlist.n_videos = playlist.videos.len() as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
None => Err(anyhow!("no ctoken")),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
||||||
|
@ -74,49 +118,7 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
||||||
.playlist_video_list_renderer
|
.playlist_video_list_renderer
|
||||||
.contents;
|
.contents;
|
||||||
|
|
||||||
let mut ctoken: Option<String> = None;
|
let (videos, ctoken) = map_playlist_items(video_items);
|
||||||
let videos = video_items
|
|
||||||
.iter()
|
|
||||||
.filter_map(|it| match it {
|
|
||||||
response::playlist::PlaylistVideoItem::PlaylistVideoRenderer { video } => {
|
|
||||||
match &video.channel {
|
|
||||||
TextLink::Browse {
|
|
||||||
text,
|
|
||||||
page_type,
|
|
||||||
browse_id,
|
|
||||||
} => match page_type {
|
|
||||||
PageType::Channel => Some(Video {
|
|
||||||
id: video.video_id.to_owned(),
|
|
||||||
title: video.title.to_owned(),
|
|
||||||
length: video.length_seconds,
|
|
||||||
thumbnails: video
|
|
||||||
.thumbnail
|
|
||||||
.thumbnails
|
|
||||||
.iter()
|
|
||||||
.map(|t| Thumbnail {
|
|
||||||
url: t.url.to_owned(),
|
|
||||||
width: t.width,
|
|
||||||
height: t.height,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
channel: Channel {
|
|
||||||
id: browse_id.to_string(),
|
|
||||||
name: text.to_owned(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response::playlist::PlaylistVideoItem::ContinuationItemRenderer {
|
|
||||||
continuation_endpoint,
|
|
||||||
} => {
|
|
||||||
ctoken = Some(continuation_endpoint.continuation_command.token.to_owned());
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let thumbnail_renderer = some_or_bail!(
|
let thumbnail_renderer = some_or_bail!(
|
||||||
response
|
response
|
||||||
|
@ -151,7 +153,7 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
||||||
match &response.header.playlist_header_renderer.num_videos_text {
|
match &response.header.playlist_header_renderer.num_videos_text {
|
||||||
Text::Multiple { runs } =>
|
Text::Multiple { runs } =>
|
||||||
if runs.len() == 2 && runs[1] == " videos" {
|
if runs.len() == 2 && runs[1] == " videos" {
|
||||||
runs[0].parse().ok()
|
runs[0].replace(",", "").replace(".", "").parse().ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
@ -175,6 +177,11 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let id = response
|
||||||
|
.header
|
||||||
|
.playlist_header_renderer
|
||||||
|
.playlist_id
|
||||||
|
.to_owned();
|
||||||
let name = response.header.playlist_header_renderer.title.to_owned();
|
let name = response.header.playlist_header_renderer.title.to_owned();
|
||||||
let description = response
|
let description = response
|
||||||
.header
|
.header
|
||||||
|
@ -201,16 +208,65 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Playlist {
|
Ok(Playlist {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
videos,
|
videos,
|
||||||
n_videos,
|
n_videos,
|
||||||
ctoken,
|
ctoken,
|
||||||
name,
|
|
||||||
thumbnails,
|
thumbnails,
|
||||||
description,
|
description,
|
||||||
channel,
|
channel,
|
||||||
|
last_update: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_playlist_items(
|
||||||
|
items: &Vec<response::VideoListItem<response::playlist::PlaylistVideo>>,
|
||||||
|
) -> (Vec<Video>, Option<String>) {
|
||||||
|
let mut ctoken: Option<String> = None;
|
||||||
|
let videos = items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|it| match it {
|
||||||
|
response::VideoListItem::GridVideoRenderer { video } => match &video.channel {
|
||||||
|
TextLink::Browse {
|
||||||
|
text,
|
||||||
|
page_type,
|
||||||
|
browse_id,
|
||||||
|
} => match page_type {
|
||||||
|
PageType::Channel => Some(Video {
|
||||||
|
id: video.video_id.to_owned(),
|
||||||
|
title: video.title.to_owned(),
|
||||||
|
length: video.length_seconds,
|
||||||
|
thumbnails: video
|
||||||
|
.thumbnail
|
||||||
|
.thumbnails
|
||||||
|
.iter()
|
||||||
|
.map(|t| Thumbnail {
|
||||||
|
url: t.url.to_owned(),
|
||||||
|
width: t.width,
|
||||||
|
height: t.height,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
channel: Channel {
|
||||||
|
id: browse_id.to_string(),
|
||||||
|
name: text.to_owned(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
response::VideoListItem::ContinuationItemRenderer {
|
||||||
|
continuation_endpoint,
|
||||||
|
} => {
|
||||||
|
ctoken = Some(continuation_endpoint.continuation_command.token.to_owned());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(videos, ctoken)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
@ -299,6 +355,7 @@ mod tests {
|
||||||
let rt = RustyTube::new();
|
let rt = RustyTube::new();
|
||||||
let playlist = rt.get_playlist(id).await.unwrap();
|
let playlist = rt.get_playlist(id).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(playlist.id, id);
|
||||||
assert_eq!(playlist.name, name);
|
assert_eq!(playlist.name, name);
|
||||||
assert!(!playlist.videos.is_empty());
|
assert!(!playlist.videos.is_empty());
|
||||||
assert_eq!(playlist.ctoken.is_some(), is_long);
|
assert_eq!(playlist.ctoken.is_some(), is_long);
|
||||||
|
@ -323,4 +380,19 @@ mod tests {
|
||||||
let playlist_data = map_playlist(&playlist).unwrap();
|
let playlist_data = map_playlist(&playlist).unwrap();
|
||||||
insta::assert_yaml_snapshot!(format!("map_playlist_data_{}", name), playlist_data);
|
insta::assert_yaml_snapshot!(format!("map_playlist_data_{}", name), playlist_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_log::test(tokio::test)]
|
||||||
|
async fn t_playlist_cont() {
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
let mut playlist = rt
|
||||||
|
.get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
while playlist.ctoken.is_some() {
|
||||||
|
rt.get_playlist_cont(&mut playlist).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(playlist.videos.len() > 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
74
src/client/response/channel.rs
Normal file
74
src/client/response/channel.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_with::serde_as;
|
||||||
|
use serde_with::VecSkipError;
|
||||||
|
|
||||||
|
use super::TimeOverlay;
|
||||||
|
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Channel {
|
||||||
|
pub contents: Contents,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Contents {
|
||||||
|
pub two_column_browse_results_renderer: TabsRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TabsRenderer {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub tabs: Vec<TabRendererWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TabRendererWrap {
|
||||||
|
pub tab_renderer: ContentRenderer<SectionListRendererWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SectionListRendererWrap {
|
||||||
|
pub section_list_renderer: ContentsRenderer<ItemSectionRendererWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ItemSectionRendererWrap {
|
||||||
|
pub item_section_renderer: ContentsRenderer<GridRendererWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GridRendererWrap {
|
||||||
|
pub grid_renderer: GridRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GridRenderer {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub items: Vec<VideoListItem<ChannelVideo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChannelVideo {
|
||||||
|
pub video_id: String,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||||
|
pub published_time_text: Option<String>,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub view_count_text: String,
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub thumbnail_overlays: Vec<TimeOverlay>,
|
||||||
|
}
|
|
@ -1,10 +1,16 @@
|
||||||
|
pub mod channel;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod playlist;
|
pub mod playlist;
|
||||||
pub mod playlist_music;
|
pub mod playlist_music;
|
||||||
|
pub mod video;
|
||||||
|
|
||||||
|
pub use channel::Channel;
|
||||||
pub use player::Player;
|
pub use player::Player;
|
||||||
pub use playlist::Playlist;
|
pub use playlist::Playlist;
|
||||||
pub use playlist_music::PlaylistMusic;
|
pub use playlist_music::PlaylistMusic;
|
||||||
|
pub use video::Video;
|
||||||
|
pub use video::VideoComments;
|
||||||
|
pub use video::VideoRecommendations;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||||
|
@ -44,10 +50,19 @@ pub struct Thumbnail {
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ContinuationItemRenderer {
|
pub enum VideoListItem<T> {
|
||||||
pub continuation_endpoint: ContinuationEndpoint,
|
#[serde(alias = "playlistVideoRenderer", alias = "compactVideoRenderer")]
|
||||||
|
GridVideoRenderer {
|
||||||
|
#[serde(flatten)]
|
||||||
|
video: T,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ContinuationItemRenderer {
|
||||||
|
continuation_endpoint: ContinuationEndpoint,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
@ -62,6 +77,77 @@ pub struct ContinuationCommand {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Icon {
|
||||||
|
pub icon_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoOwner {
|
||||||
|
pub video_owner_renderer: VideoOwnerRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoOwnerRenderer {
|
||||||
|
#[serde_as(as = "crate::serializer::text::TextLink")]
|
||||||
|
pub title: TextLink,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||||
|
pub subscriber_count_text: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub badges: Vec<UserBadge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UserBadge {
|
||||||
|
pub metadata_badge_renderer: UserBadgeRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UserBadgeRenderer {
|
||||||
|
pub style: UserBadgeStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum UserBadgeStyle {
|
||||||
|
BadgeStyleTypeVerified,
|
||||||
|
BadgeStyleTypeVerifiedArtist,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TimeOverlay {
|
||||||
|
pub thumbnail_overlay_time_status_renderer: TimeOverlayRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TimeOverlayRenderer {
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub text: String,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
|
pub style: TimeOverlayStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum TimeOverlayStyle {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Live,
|
||||||
|
Shorts,
|
||||||
|
}
|
||||||
|
|
||||||
// YouTube Music
|
// YouTube Music
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -72,10 +158,8 @@ pub struct MusicItem {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
pub playlist_item_data: Option<PlaylistItemData>,
|
pub playlist_item_data: Option<PlaylistItemData>,
|
||||||
#[serde(default)]
|
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub flex_columns: Vec<MusicColumn>,
|
pub flex_columns: Vec<MusicColumn>,
|
||||||
#[serde(default)]
|
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub fixed_columns: Vec<MusicColumn>,
|
pub fixed_columns: Vec<MusicColumn>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,27 +24,15 @@ pub enum PlayabilityStatus {
|
||||||
Ok { live_streamability: Option<Empty> },
|
Ok { live_streamability: Option<Empty> },
|
||||||
/// Video cant be played because of DRM / Geoblock
|
/// Video cant be played because of DRM / Geoblock
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Unplayable {
|
Unplayable { reason: String },
|
||||||
reason: String,
|
|
||||||
// error_screen: Option<ErrorScreen>,
|
|
||||||
},
|
|
||||||
/// Age limit / Private video
|
/// Age limit / Private video
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
LoginRequired {
|
LoginRequired { reason: String },
|
||||||
reason: String,
|
|
||||||
// error_screen: Option<ErrorScreen>
|
|
||||||
},
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
LiveStreamOffline {
|
LiveStreamOffline { reason: String },
|
||||||
reason: String,
|
|
||||||
// error_screen: Option<ErrorScreen>
|
|
||||||
},
|
|
||||||
/// Video was censored / deleted
|
/// Video was censored / deleted
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Error {
|
Error { reason: String },
|
||||||
reason: String,
|
|
||||||
// error_screen: Option<ErrorScreen>
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
|
|
@ -4,7 +4,9 @@ use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use crate::serializer::text::{Text, TextLink};
|
use crate::serializer::text::{Text, TextLink};
|
||||||
|
|
||||||
use super::{ContentRenderer, ContentsRenderer, ContinuationEndpoint, Thumbnails, ThumbnailsWrap};
|
use super::{
|
||||||
|
ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem, VideoOwner,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -14,6 +16,14 @@ pub struct Playlist {
|
||||||
pub sidebar: Sidebar,
|
pub sidebar: Sidebar,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PlaylistCont {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub on_response_received_actions: Vec<OnResponseReceivedAction>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Contents {
|
pub struct Contents {
|
||||||
|
@ -49,21 +59,7 @@ pub struct PlaylistVideoListRenderer {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PlaylistVideoList {
|
pub struct PlaylistVideoList {
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub contents: Vec<PlaylistVideoItem>,
|
pub contents: Vec<VideoListItem<PlaylistVideo>>,
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum PlaylistVideoItem {
|
|
||||||
PlaylistVideoRenderer {
|
|
||||||
#[serde(flatten)]
|
|
||||||
video: PlaylistVideo,
|
|
||||||
},
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
ContinuationItemRenderer {
|
|
||||||
continuation_endpoint: ContinuationEndpoint,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -79,7 +75,6 @@ pub struct PlaylistVideo {
|
||||||
pub channel: TextLink,
|
pub channel: TextLink,
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "JsonString")]
|
||||||
pub length_seconds: u32,
|
pub length_seconds: u32,
|
||||||
pub is_playable: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
@ -128,7 +123,7 @@ pub enum SidebarRendererItem {
|
||||||
// stats: Vec<Text>,
|
// stats: Vec<Text>,
|
||||||
},
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
PlaylistSidebarSecondaryInfoRenderer { video_owner: VideoOwnerWrap },
|
PlaylistSidebarSecondaryInfoRenderer { video_owner: VideoOwner },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
@ -141,14 +136,15 @@ pub struct PlaylistThumbnailRenderer {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VideoOwnerWrap {
|
pub struct OnResponseReceivedAction {
|
||||||
pub video_owner_renderer: VideoOwner,
|
pub append_continuation_items_action: AppendAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VideoOwner {
|
pub struct AppendAction {
|
||||||
#[serde_as(as = "crate::serializer::text::TextLink")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub title: TextLink,
|
pub continuation_items: Vec<VideoListItem<PlaylistVideo>>,
|
||||||
|
pub target_id: String,
|
||||||
}
|
}
|
||||||
|
|
432
src/client/response/video.rs
Normal file
432
src/client/response/video.rs
Normal file
|
@ -0,0 +1,432 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_with::serde_as;
|
||||||
|
use serde_with::{DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
|
use crate::serializer::text::TextLink;
|
||||||
|
|
||||||
|
use super::{ContinuationEndpoint, Icon, Thumbnails, VideoListItem, VideoOwner};
|
||||||
|
|
||||||
|
/// Video info response
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Video {
|
||||||
|
pub contents: Contents,
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub engagement_panels: Vec<EngagementPanel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Video recommendations response
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoRecommendations {
|
||||||
|
pub on_response_received_endpoints: Vec<RecommendationsContItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Video comments response
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoComments {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub on_response_received_endpoints: Vec<CommentsContItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Contents {
|
||||||
|
pub two_column_watch_next_results: TwoColumnWatchNextResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TwoColumnWatchNextResults {
|
||||||
|
pub results: VideoResultsWrap,
|
||||||
|
pub secondary_results: RecommendationResultsWrap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoResultsWrap {
|
||||||
|
pub results: VideoResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoResults {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub contents: Vec<VideoResultsItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum VideoResultsItem {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
VideoPrimaryInfoRenderer {
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
title: String,
|
||||||
|
view_count: ViewCountWrap,
|
||||||
|
video_actions: VideoActions,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
date_text: String,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
VideoSecondaryInfoRenderer {
|
||||||
|
owner: VideoOwner,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
|
metadata_row_container: Option<MetadataRowContainer>,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ItemSectionRenderer {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
contents: Vec<ItemSection>,
|
||||||
|
section_identifier: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ViewCountWrap {
|
||||||
|
pub video_view_count_renderer: ViewCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ViewCount {
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub view_count: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoActions {
|
||||||
|
pub menu_renderer: VideoActionsMenu,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoActionsMenu {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub top_level_buttons: Vec<ToggleButtonWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ToggleButtonWrap {
|
||||||
|
pub toggle_button_renderer: ToggleButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ToggleButton {
|
||||||
|
pub default_icon: Icon,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub default_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MetadataRowContainer {
|
||||||
|
pub metadata_row_container_renderer: MetadataRowContainerRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MetadataRowContainerRenderer {
|
||||||
|
pub rows: Vec<MetadataRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MetadataRow {
|
||||||
|
pub metadata_row_renderer: MetadataRowRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MetadataRowRenderer {
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde_as(as = "Vec<crate::serializer::text::TextLinks>")]
|
||||||
|
pub contents: Vec<Vec<TextLink>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum ItemSection {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
CommentsEntryPointHeaderRenderer {
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
header_text: String,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
comment_count: String,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ContinuationItemRenderer {
|
||||||
|
continuation_endpoint: ContinuationEndpoint,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecommendationResultsWrap {
|
||||||
|
pub secondary_results: RecommendationResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecommendationResults {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub results: Vec<VideoListItem<RecommendedVideo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecommendedVideo {
|
||||||
|
pub video_id: String,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(rename = "shortBylineText")]
|
||||||
|
#[serde_as(as = "crate::serializer::text::TextLink")]
|
||||||
|
pub channel: TextLink,
|
||||||
|
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||||
|
pub length_text: Option<String>,
|
||||||
|
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||||
|
pub published_time_text: Option<String>,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub view_count_text: String,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub badges: Vec<VideoBadge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoBadge {
|
||||||
|
pub metadata_badge_renderer: VideoBadgeRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VideoBadgeRenderer {
|
||||||
|
pub style: VideoBadgeStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum VideoBadgeStyle {
|
||||||
|
BadgeStyleTypeLiveNow,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EngagementPanel {
|
||||||
|
pub engagement_panel_section_list_renderer: EngagementPanelRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EngagementPanelRenderer {
|
||||||
|
pub header: EngagementPanelHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EngagementPanelHeader {
|
||||||
|
pub engagement_panel_title_header_renderer: EngagementPanelHeaderRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EngagementPanelHeaderRenderer {
|
||||||
|
pub menu: EngagementPanelMenu,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EngagementPanelMenu {
|
||||||
|
pub sort_filter_sub_menu_renderer: EngagementPanelMenuRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EngagementPanelMenuRenderer {
|
||||||
|
pub sub_menu_items: Vec<EngagementPanelMenuItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EngagementPanelMenuItem {
|
||||||
|
pub service_endpoint: ContinuationEndpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecommendationsContItem {
|
||||||
|
pub append_continuation_items_action: AppendRecommendations,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppendRecommendations {
|
||||||
|
pub continuation_items: Vec<VideoListItem<RecommendedVideo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CommentsContItem {
|
||||||
|
#[serde(alias = "reloadContinuationItemsCommand")]
|
||||||
|
pub append_continuation_items_action: AppendComments,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppendComments {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub continuation_items: Vec<CommentListItem>,
|
||||||
|
pub target_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum CommentListItem {
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
CommentThreadRenderer {
|
||||||
|
comment: Comment,
|
||||||
|
#[serde(default)]
|
||||||
|
replies: Replies,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
|
rendering_priority: CommentPriority,
|
||||||
|
},
|
||||||
|
CommentRenderer {
|
||||||
|
#[serde(flatten)]
|
||||||
|
comment: CommentRenderer,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ContinuationItemRenderer {
|
||||||
|
continuation_endpoint: ContinuationEndpoint,
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: TMP
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
CommentsHeaderRenderer { count_text: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Comment {
|
||||||
|
pub comment_renderer: CommentRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CommentRenderer {
|
||||||
|
// There may be comments with missing authors (possibly deleted users?)
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
|
||||||
|
pub author_text: Option<String>,
|
||||||
|
pub author_thumbnail: Thumbnails,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "DefaultOnError")]
|
||||||
|
pub author_endpoint: Option<AuthorEndpoint>,
|
||||||
|
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub content_text: String,
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
pub published_time_text: String,
|
||||||
|
pub comment_id: String,
|
||||||
|
pub author_is_channel_owner: bool,
|
||||||
|
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||||
|
pub vote_count: Option<String>,
|
||||||
|
pub author_comment_badge: Option<AuthorCommentBadge>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub reply_count: u32,
|
||||||
|
pub action_buttons: CommentActionButtons,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AuthorEndpoint {
|
||||||
|
pub browse_endpoint: BrowseEndpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BrowseEndpoint {
|
||||||
|
pub browse_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum CommentPriority {
|
||||||
|
#[default]
|
||||||
|
RenderingPriorityUnknown,
|
||||||
|
RenderingPriorityPinnedComment,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Replies {
|
||||||
|
pub comment_replies_renderer: RepliesRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Default, Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RepliesRenderer {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub contents: Vec<CommentListItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CommentActionButtons {
|
||||||
|
pub comment_action_buttons_renderer: CommentActionButtonsRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CommentActionButtonsRenderer {
|
||||||
|
pub creator_heart: Option<CreatorHeart>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreatorHeart {
|
||||||
|
pub creator_heart_renderer: CreatorHeartRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreatorHeartRenderer {
|
||||||
|
pub is_hearted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AuthorCommentBadge {
|
||||||
|
pub author_comment_badge_renderer: AuthorCommentBadgeRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AuthorCommentBadgeRenderer {
|
||||||
|
pub icon: Icon,
|
||||||
|
}
|
|
@ -2,6 +2,8 @@
|
||||||
source: src/client/playlist.rs
|
source: src/client/playlist.rs
|
||||||
expression: playlist_data
|
expression: playlist_data
|
||||||
---
|
---
|
||||||
|
id: PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ
|
||||||
|
name: Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022
|
||||||
videos:
|
videos:
|
||||||
- id: Bkj3IVIO2Os
|
- id: Bkj3IVIO2Os
|
||||||
title: Stereoact feat. Kerstin Ott - Die Immer Lacht (Official Video HD)
|
title: Stereoact feat. Kerstin Ott - Die Immer Lacht (Official Video HD)
|
||||||
|
@ -1905,7 +1907,6 @@ videos:
|
||||||
name: KMNGANG
|
name: KMNGANG
|
||||||
n_videos: 495
|
n_videos: 495
|
||||||
ctoken: 4qmFsgJhEiRWTFBMNWREeDY4MVQ0YlI3WkYxSXVXek92MW9tbFJiRTdQaUoaFENBRjZCbEJVT2tOSFdRJTNEJTNEmgIiUEw1ZER4NjgxVDRiUjdaRjFJdVd6T3Yxb21sUmJFN1BpSg%3D%3D
|
ctoken: 4qmFsgJhEiRWTFBMNWREeDY4MVQ0YlI3WkYxSXVXek92MW9tbFJiRTdQaUoaFENBRjZCbEJVT2tOSFdRJTNEJTNEmgIiUEw1ZER4NjgxVDRiUjdaRjFJdVd6T3Yxb21sUmJFN1BpSg%3D%3D
|
||||||
name: Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022
|
|
||||||
thumbnails:
|
thumbnails:
|
||||||
- url: "https://i.ytimg.com/vi/Bkj3IVIO2Os/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLBShlXfy9oWvTy2ntoHxmuhBlZP3g"
|
- url: "https://i.ytimg.com/vi/Bkj3IVIO2Os/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLBShlXfy9oWvTy2ntoHxmuhBlZP3g"
|
||||||
width: 168
|
width: 168
|
||||||
|
@ -1923,4 +1924,5 @@ description: ~
|
||||||
channel:
|
channel:
|
||||||
id: UCIekuFeMaV78xYfvpmoCnPg
|
id: UCIekuFeMaV78xYfvpmoCnPg
|
||||||
name: Best Music
|
name: Best Music
|
||||||
|
last_update: ~
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
source: src/client/playlist.rs
|
source: src/client/playlist.rs
|
||||||
expression: playlist_data
|
expression: playlist_data
|
||||||
---
|
---
|
||||||
|
id: PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe
|
||||||
|
name: Minecraft SHINE
|
||||||
videos:
|
videos:
|
||||||
- id: X82TrticM4A
|
- id: X82TrticM4A
|
||||||
title: Minecraft SHINE (Trailer)
|
title: Minecraft SHINE (Trailer)
|
||||||
|
@ -1259,7 +1261,6 @@ videos:
|
||||||
name: Chaosflo44
|
name: Chaosflo44
|
||||||
n_videos: 66
|
n_videos: 66
|
||||||
ctoken: ~
|
ctoken: ~
|
||||||
name: Minecraft SHINE
|
|
||||||
thumbnails:
|
thumbnails:
|
||||||
- url: "https://i.ytimg.com/vi/X82TrticM4A/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLBeSNDZEaBoP0KX9_Ayn3VO3X8rkw"
|
- url: "https://i.ytimg.com/vi/X82TrticM4A/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLBeSNDZEaBoP0KX9_Ayn3VO3X8rkw"
|
||||||
width: 168
|
width: 168
|
||||||
|
@ -1277,4 +1278,5 @@ description: "SHINE - Survival Hardcore in New Environment: Auf einem Server mac
|
||||||
channel:
|
channel:
|
||||||
id: UCQM0bS4_04-Y4JuYrgmnpZQ
|
id: UCQM0bS4_04-Y4JuYrgmnpZQ
|
||||||
name: Chaosflo44
|
name: Chaosflo44
|
||||||
|
last_update: ~
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
source: src/client/playlist.rs
|
source: src/client/playlist.rs
|
||||||
expression: playlist_data
|
expression: playlist_data
|
||||||
---
|
---
|
||||||
|
id: RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk
|
||||||
|
name: Easy Pop
|
||||||
videos:
|
videos:
|
||||||
- id: psuRGfAaju4
|
- id: psuRGfAaju4
|
||||||
title: Owl City - Fireflies (Official Music Video)
|
title: Owl City - Fireflies (Official Music Video)
|
||||||
|
@ -1848,7 +1850,6 @@ videos:
|
||||||
name: Diaven - Topic
|
name: Diaven - Topic
|
||||||
n_videos: 97
|
n_videos: 97
|
||||||
ctoken: ~
|
ctoken: ~
|
||||||
name: Easy Pop
|
|
||||||
thumbnails:
|
thumbnails:
|
||||||
- url: "https://i9.ytimg.com/s_p/RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk/mqdefault.jpg?sqp=CPDmr5gGir7X7AMGCLaHmpgG&rs=AOn4CLDg9xLeJDPeMtbWfp19VFd6vCQzqQ&v=1661371318"
|
- url: "https://i9.ytimg.com/s_p/RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk/mqdefault.jpg?sqp=CPDmr5gGir7X7AMGCLaHmpgG&rs=AOn4CLDg9xLeJDPeMtbWfp19VFd6vCQzqQ&v=1661371318"
|
||||||
width: 180
|
width: 180
|
||||||
|
@ -1861,4 +1862,5 @@ thumbnails:
|
||||||
height: 1200
|
height: 1200
|
||||||
description: ~
|
description: ~
|
||||||
channel: ~
|
channel: ~
|
||||||
|
last_update: ~
|
||||||
|
|
||||||
|
|
111
src/client/video.rs
Normal file
111
src/client/video.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::{response, ClientType, ContextYT, RustyTube};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
struct QVideo {
|
||||||
|
context: ContextYT,
|
||||||
|
/// YouTube video ID
|
||||||
|
video_id: String,
|
||||||
|
/// Set to true to allow extraction of streams with sensitive content
|
||||||
|
content_check_ok: bool,
|
||||||
|
/// Probably refers to allowing sensitive content, too
|
||||||
|
racy_check_ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
struct QVideoCont {
|
||||||
|
context: ContextYT,
|
||||||
|
continuation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustyTube {
|
||||||
|
pub async fn get_video_response(&self, video_id: &str) -> Result<response::Video> {
|
||||||
|
let client = self.get_ytclient(ClientType::Desktop);
|
||||||
|
let context = client.get_context(true).await;
|
||||||
|
let request_body = QVideo {
|
||||||
|
context,
|
||||||
|
video_id: video_id.to_owned(),
|
||||||
|
content_check_ok: true,
|
||||||
|
racy_check_ok: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.request_builder(Method::POST, "next")
|
||||||
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(resp.json::<response::Video>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_comments_response(&self, ctoken: &str) -> Result<response::VideoComments> {
|
||||||
|
let client = self.get_ytclient(ClientType::Desktop);
|
||||||
|
let context = client.get_context(true).await;
|
||||||
|
let request_body = QVideoCont {
|
||||||
|
context,
|
||||||
|
continuation: ctoken.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.request_builder(Method::POST, "next")
|
||||||
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(resp.json::<response::VideoComments>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_recommendations_response(
|
||||||
|
&self,
|
||||||
|
ctoken: &str,
|
||||||
|
) -> Result<response::VideoRecommendations> {
|
||||||
|
let client = self.get_ytclient(ClientType::Desktop);
|
||||||
|
let context = client.get_context(true).await;
|
||||||
|
let request_body = QVideoCont {
|
||||||
|
context,
|
||||||
|
continuation: ctoken.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.request_builder(Method::POST, "next")
|
||||||
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(resp.json::<response::VideoRecommendations>().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn t_get_video_response() {
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
// rt.get_video("ZeerrnuLi5E").await.unwrap();
|
||||||
|
dbg!(rt.get_video_response("iQfSvIgIs_M").await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn t_get_comments_response() {
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
// rt.get_comments("Eg0SC2lRZlN2SWdJc19NGAYyJSIRIgtpUWZTdklnSXNfTTAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D").await.unwrap();
|
||||||
|
dbg!(rt.get_comments_response("Eg0SC2lRZlN2SWdJc19NGAYychpFEhRVZ2lnVGJVTEZ6Qk5FWGdDb0FFQyICCAAqGFVDWFgwUldPSUJqdDRvM3ppSHUtNmE1QTILaVFmU3ZJZ0lzX01AAUgKQiljb21tZW50LXJlcGxpZXMtaXRlbS1VZ2lnVGJVTEZ6Qk5FWGdDb0FFQw%3D%3D").await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn t_get_recommendations_response() {
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
dbg!(rt.get_recommendations_response("CBQSExILaVFmU3ZJZ0lzX03AAQHIAQEYACqkBjJzNkw2d3pVQkFyUkJBb0Q4ajRBQ2c3Q1Bnc0lvWXlRejhLZnRZUGNBUW9EOGo0QUNnN0NQZ3NJeElEX2w0YjFtNnUtQVFvRDhqNEFDZzNDUGdvSXg5Ykx3WUNKenFwX0NnUHlQZ0FLRGNJLUNnaW83T2pqZzVPTHZEOEtBX0ktQUFvTndqNEtDTE9venZmQThybVhXd29EOGo0QUNnM0NQZ29JdzZETV9vSFk0cHRCQ2dQeVBnQUtEc0ktQ3dqbW9QbURpcHVPel80QkNnUHlQZ0FLRGNJLUNnalY4THpEazlfOTRCWUtBX0ktQUFvT3dqNExDTXVZNU9YZzE3ejV2d0VLQV9JLUFBb053ajRLQ1A3eHZiSGswTnVuYWdvRDhqNEFDZzdDUGdzSXFQYVU5ZGp2Ml96S0FRb0Q4ajRBQ2c3Q1Bnc0lfSW1acUtQOTlfQ09BUW9EOGo0QUNnM0NQZ29JeGRtNzlZS3prcUFqQ2dQeVBnQUtEY0ktQ2dpZ3FJMkg0UENRX2s0S0FfSS1BQW9Pd2o0TENQV0V5NV9ZeDhERl9nRUtBX0ktQUFvT3dqNExDTzJid3VuV3BPX3ppd0VLQV9JLUFBb2gwajRlQ2h4U1JFTk5WVU5ZV0RCU1YwOUpRbXAwTkc4emVtbElkUzAyWVRWQkNnUHlQZ0FLRGNJLUNnaXpqcXZwcDh5MWwwMEtBX0ktQUFvTndqNEtDTFhWbl83dHhfWDJOUW9EOGo0QUNnN0NQZ3NJNWR5ZWc1NjZyUGUwQVJJVUFBSUVCZ2dLREE0UUVoUVdHQm9jSGlBaUpDWWFCQWdBRUFFYUJBZ0NFQU1hQkFnRUVBVWFCQWdHRUFjYUJBZ0lFQWthQkFnS0VBc2FCQWdNRUEwYUJBZ09FQThhQkFnUUVCRWFCQWdTRUJNYUJBZ1VFQlVhQkFnV0VCY2FCQWdZRUJrYUJBZ2FFQnNhQkFnY0VCMGFCQWdlRUI4YUJBZ2dFQ0VhQkFnaUVDTWFCQWdrRUNVYUJBZ21FQ2NxRkFBQ0JBWUlDZ3dPRUJJVUZoZ2FIQjRnSWlRbWoPd2F0Y2gtbmV4dC1mZWVk").await.unwrap());
|
||||||
|
}
|
||||||
|
}
|
102
src/codegen/gen_dictionary.rs
Normal file
102
src/codegen/gen_dictionary.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
#![cfg(test)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
fmt::Debug,
|
||||||
|
fs::File,
|
||||||
|
io::{BufReader},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{model::Language, timeago::TimeUnit};
|
||||||
|
use fancy_regex::Regex;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
const DICT_PATH: &str = "testfiles/date/dictionary.json";
|
||||||
|
const TARGET_FILE: &str = "src/dictionary.rs";
|
||||||
|
|
||||||
|
type Dictionary = BTreeMap<Language, DictEntry>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct DictEntry {
|
||||||
|
#[serde(default)]
|
||||||
|
equivalent: Vec<Language>,
|
||||||
|
#[serde(default)]
|
||||||
|
by_char: bool,
|
||||||
|
timeago_tokens: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tu(tu: &str) -> (u8, Option<TimeUnit>) {
|
||||||
|
static TU_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\d*)(\w?)$").unwrap());
|
||||||
|
match TU_PATTERN.captures(tu).unwrap() {
|
||||||
|
Some(cap) => (
|
||||||
|
cap.get(1).unwrap().as_str().parse().unwrap_or(1),
|
||||||
|
match cap.get(2).unwrap().as_str() {
|
||||||
|
"s" => Some(TimeUnit::Second),
|
||||||
|
"m" => Some(TimeUnit::Minute),
|
||||||
|
"h" => Some(TimeUnit::Hour),
|
||||||
|
"D" => Some(TimeUnit::Day),
|
||||||
|
"W" => Some(TimeUnit::Week),
|
||||||
|
"M" => Some(TimeUnit::Month),
|
||||||
|
"Y" => Some(TimeUnit::Year),
|
||||||
|
"" => None,
|
||||||
|
_ => panic!("invalid time unit: {}", tu),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
None => panic!("invalid time unit: {}", tu),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_dict() -> Dictionary {
|
||||||
|
let json_file = File::open(DICT_PATH).unwrap();
|
||||||
|
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
fn generate_dictionary() {
|
||||||
|
let dict = read_dict();
|
||||||
|
|
||||||
|
let code_head = r#"// This file is automatically generated. DO NOT EDIT.
|
||||||
|
use crate::{
|
||||||
|
model::Language,
|
||||||
|
timeago::{TaToken, TimeUnit},
|
||||||
|
};
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut code_timeago_tokens = r#"#[rustfmt::skip]
|
||||||
|
pub(crate) fn get_timeago_tokens(lang: Language) -> phf::Map<&'static str, TaToken> {
|
||||||
|
match lang {
|
||||||
|
"#
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
dict.iter().for_each(|(lang, entry)| {
|
||||||
|
// Create a map for the language
|
||||||
|
let mut map = phf_codegen::Map::<&str>::new();
|
||||||
|
|
||||||
|
entry.timeago_tokens.iter().for_each(|(txt, tu_str)| {
|
||||||
|
let (n, unit) = parse_tu(&tu_str);
|
||||||
|
match unit {
|
||||||
|
Some(unit) => map.entry(
|
||||||
|
&txt,
|
||||||
|
&format!("TaToken {{ n: {}, unit: Some(TimeUnit::{:?}) }}", n, unit),
|
||||||
|
),
|
||||||
|
None => map.entry(&txt, &format!("TaToken {{ n: {}, unit: None }}", n)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut selector = format!("Language::{:?}", lang);
|
||||||
|
entry.equivalent.iter().for_each(|eq| {
|
||||||
|
selector += &format!(" | Language::{:?}", eq);
|
||||||
|
});
|
||||||
|
|
||||||
|
let code_map = &map.build().to_string().replace('\n', "\n ");
|
||||||
|
|
||||||
|
code_timeago_tokens += &format!("{} => {},\n ", selector, code_map);
|
||||||
|
});
|
||||||
|
|
||||||
|
code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n";
|
||||||
|
|
||||||
|
let code = format!("{}\n{}", code_head, code_timeago_tokens);
|
||||||
|
|
||||||
|
std::fs::write(TARGET_FILE, code).unwrap();
|
||||||
|
}
|
351
src/codegen/gen_locales.rs
Normal file
351
src/codegen/gen_locales.rs
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
#![cfg(test)]
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::serde_as;
|
||||||
|
use serde_with::VecSkipError;
|
||||||
|
|
||||||
|
use crate::client::{ClientType, ContextYT, RustyTube};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct QLanguageMenu {
|
||||||
|
context: ContextYT,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct LanguageItem {
|
||||||
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
|
title: String,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[test_log::test(tokio::test)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn generate_locales() {
|
||||||
|
let (languages, countries) = get_locales().await;
|
||||||
|
|
||||||
|
let code_head = r#"// This file is automatically generated. DO NOT EDIT.
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let code_foot = r#"impl Display for Language {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(
|
||||||
|
&serde_json::to_string(self).map_or("".to_owned(), |s| s[1..s.len() - 1].to_owned()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Country {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(
|
||||||
|
&serde_json::to_string(self).map_or("".to_owned(), |s| s[1..s.len() - 1].to_owned()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Language {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_str(&format!("\"{}\"", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Country {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_str(&format!("\"{}\"", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut code_langs = r#"#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Language {
|
||||||
|
"#.to_owned();
|
||||||
|
|
||||||
|
let mut code_countries = r#"#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum Country {
|
||||||
|
"#.to_owned();
|
||||||
|
|
||||||
|
let mut code_lang_array = format!("pub const LANGUAGES: [Language; {}] = [\n", languages.len());
|
||||||
|
let mut code_country_array = format!("pub const COUNTRIES: [Country; {}] = [\n", countries.len());
|
||||||
|
|
||||||
|
let mut code_lang_names = r#"impl Language {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
"#
|
||||||
|
.to_owned();
|
||||||
|
let mut code_country_names = r#"impl Country {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
"#
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
languages.iter().for_each(|(c, n)| {
|
||||||
|
let enum_name = c
|
||||||
|
.split('-')
|
||||||
|
.map(|c| {
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
c[0..1].to_owned().to_uppercase(),
|
||||||
|
c[1..].to_owned().to_lowercase()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// Language enum
|
||||||
|
code_langs += &format!(" /// {}\n ", n);
|
||||||
|
if c.contains('-') {
|
||||||
|
code_langs += &format!("#[serde(rename = \"{}\")]\n ", c);
|
||||||
|
}
|
||||||
|
code_langs += &enum_name;
|
||||||
|
code_langs += ",\n";
|
||||||
|
|
||||||
|
// Language array
|
||||||
|
code_lang_array += &format!(" Language::{},\n", enum_name);
|
||||||
|
|
||||||
|
// Language names
|
||||||
|
code_lang_names += &format!(" Language::{} => \"{}\",\n", enum_name, n);
|
||||||
|
});
|
||||||
|
code_langs += "}\n";
|
||||||
|
|
||||||
|
countries.iter().for_each(|(c, n)| {
|
||||||
|
let enum_name = c[0..1].to_owned().to_uppercase() + &c[1..].to_owned().to_lowercase();
|
||||||
|
|
||||||
|
// Country enum
|
||||||
|
code_countries += &format!(" /// {}\n", n);
|
||||||
|
code_countries += &format!(" {},\n", enum_name);
|
||||||
|
|
||||||
|
// Country array
|
||||||
|
code_country_array += &format!(" Country::{},\n", enum_name);
|
||||||
|
|
||||||
|
// Country names
|
||||||
|
code_country_names += &format!(" Country::{} => \"{}\",\n", enum_name, 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!(
|
||||||
|
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
|
||||||
|
code_head,
|
||||||
|
code_langs,
|
||||||
|
code_countries,
|
||||||
|
code_lang_array,
|
||||||
|
code_country_array,
|
||||||
|
code_lang_names,
|
||||||
|
code_country_names,
|
||||||
|
code_foot,
|
||||||
|
);
|
||||||
|
|
||||||
|
let locale_path = Path::new("src/model/locale.rs");
|
||||||
|
std::fs::write(locale_path, code).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_locales() -> (BTreeMap<String, String>, BTreeMap<String, String>) {
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
let client = rt.get_ytclient(ClientType::Desktop);
|
||||||
|
let context = client.get_context(true).await;
|
||||||
|
|
||||||
|
let request_body = QLanguageMenu { context };
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.request_builder(Method::POST, "account/account_menu")
|
||||||
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.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
|
||||||
|
.to_owned(),
|
||||||
|
i.compact_link_renderer.title.to_owned(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>()
|
||||||
|
}
|
3
src/codegen/mod.rs
Normal file
3
src/codegen/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#![cfg(test)]
|
||||||
|
mod gen_dictionary;
|
||||||
|
mod gen_locales;
|
|
@ -103,9 +103,10 @@ fn get_sig_fn(player_js: &str) -> Result<String> {
|
||||||
.as_str()
|
.as_str()
|
||||||
+ ";";
|
+ ";";
|
||||||
|
|
||||||
let helper_object_name_pattern = Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap();
|
static HELPER_OBJECT_NAME_PATTERN: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap());
|
||||||
let helper_object_name = some_or_bail!(
|
let helper_object_name = some_or_bail!(
|
||||||
helper_object_name_pattern
|
HELPER_OBJECT_NAME_PATTERN
|
||||||
.captures(&deobfuscate_function)
|
.captures(&deobfuscate_function)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten(),
|
.flatten(),
|
||||||
|
@ -145,12 +146,13 @@ fn deobfuscate_sig(sig: &str, sig_fn: &str) -> Result<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_nsig_fn_name(player_js: &str) -> Result<String> {
|
fn get_nsig_fn_name(player_js: &str) -> Result<String> {
|
||||||
let function_name_pattern =
|
static FUNCTION_NAME_PATTERN: Lazy<Regex> = Lazy::new(|| {
|
||||||
Regex::new("\\.get\\(\"n\"\\)\\)&&\\(b=([a-zA-Z0-9$]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9]\\)")
|
Regex::new("\\.get\\(\"n\"\\)\\)&&\\(b=([a-zA-Z0-9$]+)(?:\\[(\\d+)])?\\([a-zA-Z0-9]\\)")
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
let fname_match = some_or_bail!(
|
let fname_match = some_or_bail!(
|
||||||
function_name_pattern.captures(player_js).ok().flatten(),
|
FUNCTION_NAME_PATTERN.captures(player_js).ok().flatten(),
|
||||||
Err(anyhow!("could not find n_deobf function"))
|
Err(anyhow!("could not find n_deobf function"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -315,11 +317,12 @@ async fn get_player_js_url(http: &Client) -> Result<String> {
|
||||||
.error_for_status()?;
|
.error_for_status()?;
|
||||||
let text = resp.text().await?;
|
let text = resp.text().await?;
|
||||||
|
|
||||||
let player_hash_pattern =
|
static PLAYER_HASH_PATTERN: Lazy<Regex> = Lazy::new(|| {
|
||||||
Regex::new(r#"https:\\\/\\\/www\.youtube\.com\\\/s\\\/player\\\/([a-z0-9]{8})\\\/"#)
|
Regex::new(r#"https:\\\/\\\/www\.youtube\.com\\\/s\\\/player\\\/([a-z0-9]{8})\\\/"#)
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
});
|
||||||
let player_hash = some_or_bail!(
|
let player_hash = some_or_bail!(
|
||||||
player_hash_pattern.captures(&text)?,
|
PLAYER_HASH_PATTERN.captures(&text)?,
|
||||||
Err(anyhow!("could not find player hash"))
|
Err(anyhow!("could not find player hash"))
|
||||||
)
|
)
|
||||||
.get(1)
|
.get(1)
|
||||||
|
@ -338,10 +341,11 @@ async fn get_response(http: &Client, url: &str) -> Result<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sts(player_js: &str) -> Result<String> {
|
fn get_sts(player_js: &str) -> Result<String> {
|
||||||
let sts_pattern = Regex::new("signatureTimestamp[=:](\\d+)").unwrap();
|
static STS_PATTERN: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap());
|
||||||
|
|
||||||
Ok(some_or_bail!(
|
Ok(some_or_bail!(
|
||||||
sts_pattern.captures(&player_js)?,
|
STS_PATTERN.captures(&player_js)?,
|
||||||
Err(anyhow!("could not find sts"))
|
Err(anyhow!("could not find sts"))
|
||||||
)
|
)
|
||||||
.get(1)
|
.get(1)
|
||||||
|
@ -357,7 +361,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use test_log::test;
|
use test_log::test;
|
||||||
|
|
||||||
const TEST_JS: Lazy<String> = Lazy::new(|| {
|
static TEST_JS: Lazy<String> = Lazy::new(|| {
|
||||||
let js_path = Path::new("testfiles/deobf/dummy_player.js");
|
let js_path = Path::new("testfiles/deobf/dummy_player.js");
|
||||||
std::fs::read_to_string(js_path).unwrap()
|
std::fs::read_to_string(js_path).unwrap()
|
||||||
});
|
});
|
||||||
|
|
1682
src/dictionary.rs
Normal file
1682
src/dictionary.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@ use tokio::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{stream_filter::Filter, AudioCodec, FileFormat, VideoPlayer, VideoCodec},
|
model::{stream_filter::Filter, AudioCodec, FileFormat, VideoCodec, VideoPlayer},
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,16 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod codegen;
|
||||||
|
|
||||||
mod cache;
|
mod cache;
|
||||||
mod deobfuscate;
|
mod deobfuscate;
|
||||||
|
mod dictionary;
|
||||||
mod serializer;
|
mod serializer;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
pub mod timeago;
|
||||||
|
|
842
src/model/locale.rs
Normal file
842
src/model/locale.rs
Normal file
|
@ -0,0 +1,842 @@
|
||||||
|
// This file is automatically generated. DO NOT EDIT.
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Language {
|
||||||
|
/// Afrikaans
|
||||||
|
Af,
|
||||||
|
/// አማርኛ
|
||||||
|
Am,
|
||||||
|
/// العربية
|
||||||
|
Ar,
|
||||||
|
/// অসমীয়া
|
||||||
|
As,
|
||||||
|
/// Azərbaycan
|
||||||
|
Az,
|
||||||
|
/// Беларуская
|
||||||
|
Be,
|
||||||
|
/// Български
|
||||||
|
Bg,
|
||||||
|
/// বাংলা
|
||||||
|
Bn,
|
||||||
|
/// Bosanski
|
||||||
|
Bs,
|
||||||
|
/// Català
|
||||||
|
Ca,
|
||||||
|
/// Čeština
|
||||||
|
Cs,
|
||||||
|
/// Dansk
|
||||||
|
Da,
|
||||||
|
/// Deutsch
|
||||||
|
De,
|
||||||
|
/// Ελληνικά
|
||||||
|
El,
|
||||||
|
/// English (US)
|
||||||
|
En,
|
||||||
|
/// English (UK)
|
||||||
|
#[serde(rename = "en-GB")]
|
||||||
|
EnGb,
|
||||||
|
/// English (India)
|
||||||
|
#[serde(rename = "en-IN")]
|
||||||
|
EnIn,
|
||||||
|
/// Español (España)
|
||||||
|
Es,
|
||||||
|
/// Español (Latinoamérica)
|
||||||
|
#[serde(rename = "es-419")]
|
||||||
|
Es419,
|
||||||
|
/// Español (US)
|
||||||
|
#[serde(rename = "es-US")]
|
||||||
|
EsUs,
|
||||||
|
/// Eesti
|
||||||
|
Et,
|
||||||
|
/// Euskara
|
||||||
|
Eu,
|
||||||
|
/// فارسی
|
||||||
|
Fa,
|
||||||
|
/// Suomi
|
||||||
|
Fi,
|
||||||
|
/// Filipino
|
||||||
|
Fil,
|
||||||
|
/// Français
|
||||||
|
Fr,
|
||||||
|
/// Français (Canada)
|
||||||
|
#[serde(rename = "fr-CA")]
|
||||||
|
FrCa,
|
||||||
|
/// Galego
|
||||||
|
Gl,
|
||||||
|
/// ગુજરાતી
|
||||||
|
Gu,
|
||||||
|
/// हिन्दी
|
||||||
|
Hi,
|
||||||
|
/// Hrvatski
|
||||||
|
Hr,
|
||||||
|
/// Magyar
|
||||||
|
Hu,
|
||||||
|
/// Հայերեն
|
||||||
|
Hy,
|
||||||
|
/// Bahasa Indonesia
|
||||||
|
Id,
|
||||||
|
/// Íslenska
|
||||||
|
Is,
|
||||||
|
/// Italiano
|
||||||
|
It,
|
||||||
|
/// עברית
|
||||||
|
Iw,
|
||||||
|
/// 日本語
|
||||||
|
Ja,
|
||||||
|
/// ქართული
|
||||||
|
Ka,
|
||||||
|
/// Қазақ Тілі
|
||||||
|
Kk,
|
||||||
|
/// ខ្មែរ
|
||||||
|
Km,
|
||||||
|
/// ಕನ್ನಡ
|
||||||
|
Kn,
|
||||||
|
/// 한국어
|
||||||
|
Ko,
|
||||||
|
/// Кыргызча
|
||||||
|
Ky,
|
||||||
|
/// ລາວ
|
||||||
|
Lo,
|
||||||
|
/// Lietuvių
|
||||||
|
Lt,
|
||||||
|
/// Latviešu valoda
|
||||||
|
Lv,
|
||||||
|
/// Македонски
|
||||||
|
Mk,
|
||||||
|
/// മലയാളം
|
||||||
|
Ml,
|
||||||
|
/// Монгол
|
||||||
|
Mn,
|
||||||
|
/// मराठी
|
||||||
|
Mr,
|
||||||
|
/// Bahasa Malaysia
|
||||||
|
Ms,
|
||||||
|
/// ဗမာ
|
||||||
|
My,
|
||||||
|
/// नेपाली
|
||||||
|
Ne,
|
||||||
|
/// Nederlands
|
||||||
|
Nl,
|
||||||
|
/// Norsk
|
||||||
|
No,
|
||||||
|
/// ଓଡ଼ିଆ
|
||||||
|
Or,
|
||||||
|
/// ਪੰਜਾਬੀ
|
||||||
|
Pa,
|
||||||
|
/// Polski
|
||||||
|
Pl,
|
||||||
|
/// Português (Brasil)
|
||||||
|
Pt,
|
||||||
|
/// Português
|
||||||
|
#[serde(rename = "pt-PT")]
|
||||||
|
PtPt,
|
||||||
|
/// Română
|
||||||
|
Ro,
|
||||||
|
/// Русский
|
||||||
|
Ru,
|
||||||
|
/// සිංහල
|
||||||
|
Si,
|
||||||
|
/// Slovenčina
|
||||||
|
Sk,
|
||||||
|
/// Slovenščina
|
||||||
|
Sl,
|
||||||
|
/// Shqip
|
||||||
|
Sq,
|
||||||
|
/// Српски
|
||||||
|
Sr,
|
||||||
|
/// Srpski
|
||||||
|
#[serde(rename = "sr-Latn")]
|
||||||
|
SrLatn,
|
||||||
|
/// Svenska
|
||||||
|
Sv,
|
||||||
|
/// Kiswahili
|
||||||
|
Sw,
|
||||||
|
/// தமிழ்
|
||||||
|
Ta,
|
||||||
|
/// తెలుగు
|
||||||
|
Te,
|
||||||
|
/// ภาษาไทย
|
||||||
|
Th,
|
||||||
|
/// Türkçe
|
||||||
|
Tr,
|
||||||
|
/// Українська
|
||||||
|
Uk,
|
||||||
|
/// اردو
|
||||||
|
Ur,
|
||||||
|
/// O‘zbek
|
||||||
|
Uz,
|
||||||
|
/// Tiếng Việt
|
||||||
|
Vi,
|
||||||
|
/// 中文 (简体)
|
||||||
|
#[serde(rename = "zh-CN")]
|
||||||
|
ZhCn,
|
||||||
|
/// 中文 (香港)
|
||||||
|
#[serde(rename = "zh-HK")]
|
||||||
|
ZhHk,
|
||||||
|
/// 中文 (繁體)
|
||||||
|
#[serde(rename = "zh-TW")]
|
||||||
|
ZhTw,
|
||||||
|
/// IsiZulu
|
||||||
|
Zu,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum Country {
|
||||||
|
/// United Arab Emirates
|
||||||
|
Ae,
|
||||||
|
/// Argentina
|
||||||
|
Ar,
|
||||||
|
/// Austria
|
||||||
|
At,
|
||||||
|
/// Australia
|
||||||
|
Au,
|
||||||
|
/// Azerbaijan
|
||||||
|
Az,
|
||||||
|
/// Bosnia and Herzegovina
|
||||||
|
Ba,
|
||||||
|
/// Bangladesh
|
||||||
|
Bd,
|
||||||
|
/// Belgium
|
||||||
|
Be,
|
||||||
|
/// Bulgaria
|
||||||
|
Bg,
|
||||||
|
/// Bahrain
|
||||||
|
Bh,
|
||||||
|
/// Bolivia
|
||||||
|
Bo,
|
||||||
|
/// Brazil
|
||||||
|
Br,
|
||||||
|
/// Belarus
|
||||||
|
By,
|
||||||
|
/// Canada
|
||||||
|
Ca,
|
||||||
|
/// Switzerland
|
||||||
|
Ch,
|
||||||
|
/// Chile
|
||||||
|
Cl,
|
||||||
|
/// Colombia
|
||||||
|
Co,
|
||||||
|
/// Costa Rica
|
||||||
|
Cr,
|
||||||
|
/// Cyprus
|
||||||
|
Cy,
|
||||||
|
/// Czechia
|
||||||
|
Cz,
|
||||||
|
/// Germany
|
||||||
|
De,
|
||||||
|
/// Denmark
|
||||||
|
Dk,
|
||||||
|
/// Dominican Republic
|
||||||
|
Do,
|
||||||
|
/// Algeria
|
||||||
|
Dz,
|
||||||
|
/// Ecuador
|
||||||
|
Ec,
|
||||||
|
/// Estonia
|
||||||
|
Ee,
|
||||||
|
/// Egypt
|
||||||
|
Eg,
|
||||||
|
/// Spain
|
||||||
|
Es,
|
||||||
|
/// Finland
|
||||||
|
Fi,
|
||||||
|
/// France
|
||||||
|
Fr,
|
||||||
|
/// United Kingdom
|
||||||
|
Gb,
|
||||||
|
/// Georgia
|
||||||
|
Ge,
|
||||||
|
/// Ghana
|
||||||
|
Gh,
|
||||||
|
/// Greece
|
||||||
|
Gr,
|
||||||
|
/// Guatemala
|
||||||
|
Gt,
|
||||||
|
/// Hong Kong
|
||||||
|
Hk,
|
||||||
|
/// Honduras
|
||||||
|
Hn,
|
||||||
|
/// Croatia
|
||||||
|
Hr,
|
||||||
|
/// Hungary
|
||||||
|
Hu,
|
||||||
|
/// Indonesia
|
||||||
|
Id,
|
||||||
|
/// Ireland
|
||||||
|
Ie,
|
||||||
|
/// Israel
|
||||||
|
Il,
|
||||||
|
/// India
|
||||||
|
In,
|
||||||
|
/// Iraq
|
||||||
|
Iq,
|
||||||
|
/// Iceland
|
||||||
|
Is,
|
||||||
|
/// Italy
|
||||||
|
It,
|
||||||
|
/// Jamaica
|
||||||
|
Jm,
|
||||||
|
/// Jordan
|
||||||
|
Jo,
|
||||||
|
/// Japan
|
||||||
|
Jp,
|
||||||
|
/// Kenya
|
||||||
|
Ke,
|
||||||
|
/// Cambodia
|
||||||
|
Kh,
|
||||||
|
/// South Korea
|
||||||
|
Kr,
|
||||||
|
/// Kuwait
|
||||||
|
Kw,
|
||||||
|
/// Kazakhstan
|
||||||
|
Kz,
|
||||||
|
/// Laos
|
||||||
|
La,
|
||||||
|
/// Lebanon
|
||||||
|
Lb,
|
||||||
|
/// Liechtenstein
|
||||||
|
Li,
|
||||||
|
/// Sri Lanka
|
||||||
|
Lk,
|
||||||
|
/// Lithuania
|
||||||
|
Lt,
|
||||||
|
/// Luxembourg
|
||||||
|
Lu,
|
||||||
|
/// Latvia
|
||||||
|
Lv,
|
||||||
|
/// Libya
|
||||||
|
Ly,
|
||||||
|
/// Morocco
|
||||||
|
Ma,
|
||||||
|
/// Montenegro
|
||||||
|
Me,
|
||||||
|
/// North Macedonia
|
||||||
|
Mk,
|
||||||
|
/// Malta
|
||||||
|
Mt,
|
||||||
|
/// Mexico
|
||||||
|
Mx,
|
||||||
|
/// Malaysia
|
||||||
|
My,
|
||||||
|
/// Nigeria
|
||||||
|
Ng,
|
||||||
|
/// Nicaragua
|
||||||
|
Ni,
|
||||||
|
/// Netherlands
|
||||||
|
Nl,
|
||||||
|
/// Norway
|
||||||
|
No,
|
||||||
|
/// Nepal
|
||||||
|
Np,
|
||||||
|
/// New Zealand
|
||||||
|
Nz,
|
||||||
|
/// Oman
|
||||||
|
Om,
|
||||||
|
/// Panama
|
||||||
|
Pa,
|
||||||
|
/// Peru
|
||||||
|
Pe,
|
||||||
|
/// Papua New Guinea
|
||||||
|
Pg,
|
||||||
|
/// Philippines
|
||||||
|
Ph,
|
||||||
|
/// Pakistan
|
||||||
|
Pk,
|
||||||
|
/// Poland
|
||||||
|
Pl,
|
||||||
|
/// Puerto Rico
|
||||||
|
Pr,
|
||||||
|
/// Portugal
|
||||||
|
Pt,
|
||||||
|
/// Paraguay
|
||||||
|
Py,
|
||||||
|
/// Qatar
|
||||||
|
Qa,
|
||||||
|
/// Romania
|
||||||
|
Ro,
|
||||||
|
/// Serbia
|
||||||
|
Rs,
|
||||||
|
/// Russia
|
||||||
|
Ru,
|
||||||
|
/// Saudi Arabia
|
||||||
|
Sa,
|
||||||
|
/// Sweden
|
||||||
|
Se,
|
||||||
|
/// Singapore
|
||||||
|
Sg,
|
||||||
|
/// Slovenia
|
||||||
|
Si,
|
||||||
|
/// Slovakia
|
||||||
|
Sk,
|
||||||
|
/// Senegal
|
||||||
|
Sn,
|
||||||
|
/// El Salvador
|
||||||
|
Sv,
|
||||||
|
/// Thailand
|
||||||
|
Th,
|
||||||
|
/// Tunisia
|
||||||
|
Tn,
|
||||||
|
/// Turkey
|
||||||
|
Tr,
|
||||||
|
/// Taiwan
|
||||||
|
Tw,
|
||||||
|
/// Tanzania
|
||||||
|
Tz,
|
||||||
|
/// Ukraine
|
||||||
|
Ua,
|
||||||
|
/// Uganda
|
||||||
|
Ug,
|
||||||
|
/// United States
|
||||||
|
Us,
|
||||||
|
/// Uruguay
|
||||||
|
Uy,
|
||||||
|
/// Venezuela
|
||||||
|
Ve,
|
||||||
|
/// Vietnam
|
||||||
|
Vn,
|
||||||
|
/// Yemen
|
||||||
|
Ye,
|
||||||
|
/// South Africa
|
||||||
|
Za,
|
||||||
|
/// Zimbabwe
|
||||||
|
Zw,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const LANGUAGES: [Language; 83] = [
|
||||||
|
Language::Af,
|
||||||
|
Language::Am,
|
||||||
|
Language::Ar,
|
||||||
|
Language::As,
|
||||||
|
Language::Az,
|
||||||
|
Language::Be,
|
||||||
|
Language::Bg,
|
||||||
|
Language::Bn,
|
||||||
|
Language::Bs,
|
||||||
|
Language::Ca,
|
||||||
|
Language::Cs,
|
||||||
|
Language::Da,
|
||||||
|
Language::De,
|
||||||
|
Language::El,
|
||||||
|
Language::En,
|
||||||
|
Language::EnGb,
|
||||||
|
Language::EnIn,
|
||||||
|
Language::Es,
|
||||||
|
Language::Es419,
|
||||||
|
Language::EsUs,
|
||||||
|
Language::Et,
|
||||||
|
Language::Eu,
|
||||||
|
Language::Fa,
|
||||||
|
Language::Fi,
|
||||||
|
Language::Fil,
|
||||||
|
Language::Fr,
|
||||||
|
Language::FrCa,
|
||||||
|
Language::Gl,
|
||||||
|
Language::Gu,
|
||||||
|
Language::Hi,
|
||||||
|
Language::Hr,
|
||||||
|
Language::Hu,
|
||||||
|
Language::Hy,
|
||||||
|
Language::Id,
|
||||||
|
Language::Is,
|
||||||
|
Language::It,
|
||||||
|
Language::Iw,
|
||||||
|
Language::Ja,
|
||||||
|
Language::Ka,
|
||||||
|
Language::Kk,
|
||||||
|
Language::Km,
|
||||||
|
Language::Kn,
|
||||||
|
Language::Ko,
|
||||||
|
Language::Ky,
|
||||||
|
Language::Lo,
|
||||||
|
Language::Lt,
|
||||||
|
Language::Lv,
|
||||||
|
Language::Mk,
|
||||||
|
Language::Ml,
|
||||||
|
Language::Mn,
|
||||||
|
Language::Mr,
|
||||||
|
Language::Ms,
|
||||||
|
Language::My,
|
||||||
|
Language::Ne,
|
||||||
|
Language::Nl,
|
||||||
|
Language::No,
|
||||||
|
Language::Or,
|
||||||
|
Language::Pa,
|
||||||
|
Language::Pl,
|
||||||
|
Language::Pt,
|
||||||
|
Language::PtPt,
|
||||||
|
Language::Ro,
|
||||||
|
Language::Ru,
|
||||||
|
Language::Si,
|
||||||
|
Language::Sk,
|
||||||
|
Language::Sl,
|
||||||
|
Language::Sq,
|
||||||
|
Language::Sr,
|
||||||
|
Language::SrLatn,
|
||||||
|
Language::Sv,
|
||||||
|
Language::Sw,
|
||||||
|
Language::Ta,
|
||||||
|
Language::Te,
|
||||||
|
Language::Th,
|
||||||
|
Language::Tr,
|
||||||
|
Language::Uk,
|
||||||
|
Language::Ur,
|
||||||
|
Language::Uz,
|
||||||
|
Language::Vi,
|
||||||
|
Language::ZhCn,
|
||||||
|
Language::ZhHk,
|
||||||
|
Language::ZhTw,
|
||||||
|
Language::Zu,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const COUNTRIES: [Country; 109] = [
|
||||||
|
Country::Ae,
|
||||||
|
Country::Ar,
|
||||||
|
Country::At,
|
||||||
|
Country::Au,
|
||||||
|
Country::Az,
|
||||||
|
Country::Ba,
|
||||||
|
Country::Bd,
|
||||||
|
Country::Be,
|
||||||
|
Country::Bg,
|
||||||
|
Country::Bh,
|
||||||
|
Country::Bo,
|
||||||
|
Country::Br,
|
||||||
|
Country::By,
|
||||||
|
Country::Ca,
|
||||||
|
Country::Ch,
|
||||||
|
Country::Cl,
|
||||||
|
Country::Co,
|
||||||
|
Country::Cr,
|
||||||
|
Country::Cy,
|
||||||
|
Country::Cz,
|
||||||
|
Country::De,
|
||||||
|
Country::Dk,
|
||||||
|
Country::Do,
|
||||||
|
Country::Dz,
|
||||||
|
Country::Ec,
|
||||||
|
Country::Ee,
|
||||||
|
Country::Eg,
|
||||||
|
Country::Es,
|
||||||
|
Country::Fi,
|
||||||
|
Country::Fr,
|
||||||
|
Country::Gb,
|
||||||
|
Country::Ge,
|
||||||
|
Country::Gh,
|
||||||
|
Country::Gr,
|
||||||
|
Country::Gt,
|
||||||
|
Country::Hk,
|
||||||
|
Country::Hn,
|
||||||
|
Country::Hr,
|
||||||
|
Country::Hu,
|
||||||
|
Country::Id,
|
||||||
|
Country::Ie,
|
||||||
|
Country::Il,
|
||||||
|
Country::In,
|
||||||
|
Country::Iq,
|
||||||
|
Country::Is,
|
||||||
|
Country::It,
|
||||||
|
Country::Jm,
|
||||||
|
Country::Jo,
|
||||||
|
Country::Jp,
|
||||||
|
Country::Ke,
|
||||||
|
Country::Kh,
|
||||||
|
Country::Kr,
|
||||||
|
Country::Kw,
|
||||||
|
Country::Kz,
|
||||||
|
Country::La,
|
||||||
|
Country::Lb,
|
||||||
|
Country::Li,
|
||||||
|
Country::Lk,
|
||||||
|
Country::Lt,
|
||||||
|
Country::Lu,
|
||||||
|
Country::Lv,
|
||||||
|
Country::Ly,
|
||||||
|
Country::Ma,
|
||||||
|
Country::Me,
|
||||||
|
Country::Mk,
|
||||||
|
Country::Mt,
|
||||||
|
Country::Mx,
|
||||||
|
Country::My,
|
||||||
|
Country::Ng,
|
||||||
|
Country::Ni,
|
||||||
|
Country::Nl,
|
||||||
|
Country::No,
|
||||||
|
Country::Np,
|
||||||
|
Country::Nz,
|
||||||
|
Country::Om,
|
||||||
|
Country::Pa,
|
||||||
|
Country::Pe,
|
||||||
|
Country::Pg,
|
||||||
|
Country::Ph,
|
||||||
|
Country::Pk,
|
||||||
|
Country::Pl,
|
||||||
|
Country::Pr,
|
||||||
|
Country::Pt,
|
||||||
|
Country::Py,
|
||||||
|
Country::Qa,
|
||||||
|
Country::Ro,
|
||||||
|
Country::Rs,
|
||||||
|
Country::Ru,
|
||||||
|
Country::Sa,
|
||||||
|
Country::Se,
|
||||||
|
Country::Sg,
|
||||||
|
Country::Si,
|
||||||
|
Country::Sk,
|
||||||
|
Country::Sn,
|
||||||
|
Country::Sv,
|
||||||
|
Country::Th,
|
||||||
|
Country::Tn,
|
||||||
|
Country::Tr,
|
||||||
|
Country::Tw,
|
||||||
|
Country::Tz,
|
||||||
|
Country::Ua,
|
||||||
|
Country::Ug,
|
||||||
|
Country::Us,
|
||||||
|
Country::Uy,
|
||||||
|
Country::Ve,
|
||||||
|
Country::Vn,
|
||||||
|
Country::Ye,
|
||||||
|
Country::Za,
|
||||||
|
Country::Zw,
|
||||||
|
];
|
||||||
|
|
||||||
|
impl Language {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Language::Af => "Afrikaans",
|
||||||
|
Language::Am => "አማርኛ",
|
||||||
|
Language::Ar => "العربية",
|
||||||
|
Language::As => "অসমীয়া",
|
||||||
|
Language::Az => "Azərbaycan",
|
||||||
|
Language::Be => "Беларуская",
|
||||||
|
Language::Bg => "Български",
|
||||||
|
Language::Bn => "বাংলা",
|
||||||
|
Language::Bs => "Bosanski",
|
||||||
|
Language::Ca => "Català",
|
||||||
|
Language::Cs => "Čeština",
|
||||||
|
Language::Da => "Dansk",
|
||||||
|
Language::De => "Deutsch",
|
||||||
|
Language::El => "Ελληνικά",
|
||||||
|
Language::En => "English (US)",
|
||||||
|
Language::EnGb => "English (UK)",
|
||||||
|
Language::EnIn => "English (India)",
|
||||||
|
Language::Es => "Español (España)",
|
||||||
|
Language::Es419 => "Español (Latinoamérica)",
|
||||||
|
Language::EsUs => "Español (US)",
|
||||||
|
Language::Et => "Eesti",
|
||||||
|
Language::Eu => "Euskara",
|
||||||
|
Language::Fa => "فارسی",
|
||||||
|
Language::Fi => "Suomi",
|
||||||
|
Language::Fil => "Filipino",
|
||||||
|
Language::Fr => "Français",
|
||||||
|
Language::FrCa => "Français (Canada)",
|
||||||
|
Language::Gl => "Galego",
|
||||||
|
Language::Gu => "ગુજરાતી",
|
||||||
|
Language::Hi => "हिन्दी",
|
||||||
|
Language::Hr => "Hrvatski",
|
||||||
|
Language::Hu => "Magyar",
|
||||||
|
Language::Hy => "Հայերեն",
|
||||||
|
Language::Id => "Bahasa Indonesia",
|
||||||
|
Language::Is => "Íslenska",
|
||||||
|
Language::It => "Italiano",
|
||||||
|
Language::Iw => "עברית",
|
||||||
|
Language::Ja => "日本語",
|
||||||
|
Language::Ka => "ქართული",
|
||||||
|
Language::Kk => "Қазақ Тілі",
|
||||||
|
Language::Km => "ខ្មែរ",
|
||||||
|
Language::Kn => "ಕನ್ನಡ",
|
||||||
|
Language::Ko => "한국어",
|
||||||
|
Language::Ky => "Кыргызча",
|
||||||
|
Language::Lo => "ລາວ",
|
||||||
|
Language::Lt => "Lietuvių",
|
||||||
|
Language::Lv => "Latviešu valoda",
|
||||||
|
Language::Mk => "Македонски",
|
||||||
|
Language::Ml => "മലയാളം",
|
||||||
|
Language::Mn => "Монгол",
|
||||||
|
Language::Mr => "मराठी",
|
||||||
|
Language::Ms => "Bahasa Malaysia",
|
||||||
|
Language::My => "ဗမာ",
|
||||||
|
Language::Ne => "नेपाली",
|
||||||
|
Language::Nl => "Nederlands",
|
||||||
|
Language::No => "Norsk",
|
||||||
|
Language::Or => "ଓଡ଼ିଆ",
|
||||||
|
Language::Pa => "ਪੰਜਾਬੀ",
|
||||||
|
Language::Pl => "Polski",
|
||||||
|
Language::Pt => "Português (Brasil)",
|
||||||
|
Language::PtPt => "Português",
|
||||||
|
Language::Ro => "Română",
|
||||||
|
Language::Ru => "Русский",
|
||||||
|
Language::Si => "සිංහල",
|
||||||
|
Language::Sk => "Slovenčina",
|
||||||
|
Language::Sl => "Slovenščina",
|
||||||
|
Language::Sq => "Shqip",
|
||||||
|
Language::Sr => "Српски",
|
||||||
|
Language::SrLatn => "Srpski",
|
||||||
|
Language::Sv => "Svenska",
|
||||||
|
Language::Sw => "Kiswahili",
|
||||||
|
Language::Ta => "தமிழ்",
|
||||||
|
Language::Te => "తెలుగు",
|
||||||
|
Language::Th => "ภาษาไทย",
|
||||||
|
Language::Tr => "Türkçe",
|
||||||
|
Language::Uk => "Українська",
|
||||||
|
Language::Ur => "اردو",
|
||||||
|
Language::Uz => "O‘zbek",
|
||||||
|
Language::Vi => "Tiếng Việt",
|
||||||
|
Language::ZhCn => "中文 (简体)",
|
||||||
|
Language::ZhHk => "中文 (香港)",
|
||||||
|
Language::ZhTw => "中文 (繁體)",
|
||||||
|
Language::Zu => "IsiZulu",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Country {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Country::Ae => "United Arab Emirates",
|
||||||
|
Country::Ar => "Argentina",
|
||||||
|
Country::At => "Austria",
|
||||||
|
Country::Au => "Australia",
|
||||||
|
Country::Az => "Azerbaijan",
|
||||||
|
Country::Ba => "Bosnia and Herzegovina",
|
||||||
|
Country::Bd => "Bangladesh",
|
||||||
|
Country::Be => "Belgium",
|
||||||
|
Country::Bg => "Bulgaria",
|
||||||
|
Country::Bh => "Bahrain",
|
||||||
|
Country::Bo => "Bolivia",
|
||||||
|
Country::Br => "Brazil",
|
||||||
|
Country::By => "Belarus",
|
||||||
|
Country::Ca => "Canada",
|
||||||
|
Country::Ch => "Switzerland",
|
||||||
|
Country::Cl => "Chile",
|
||||||
|
Country::Co => "Colombia",
|
||||||
|
Country::Cr => "Costa Rica",
|
||||||
|
Country::Cy => "Cyprus",
|
||||||
|
Country::Cz => "Czechia",
|
||||||
|
Country::De => "Germany",
|
||||||
|
Country::Dk => "Denmark",
|
||||||
|
Country::Do => "Dominican Republic",
|
||||||
|
Country::Dz => "Algeria",
|
||||||
|
Country::Ec => "Ecuador",
|
||||||
|
Country::Ee => "Estonia",
|
||||||
|
Country::Eg => "Egypt",
|
||||||
|
Country::Es => "Spain",
|
||||||
|
Country::Fi => "Finland",
|
||||||
|
Country::Fr => "France",
|
||||||
|
Country::Gb => "United Kingdom",
|
||||||
|
Country::Ge => "Georgia",
|
||||||
|
Country::Gh => "Ghana",
|
||||||
|
Country::Gr => "Greece",
|
||||||
|
Country::Gt => "Guatemala",
|
||||||
|
Country::Hk => "Hong Kong",
|
||||||
|
Country::Hn => "Honduras",
|
||||||
|
Country::Hr => "Croatia",
|
||||||
|
Country::Hu => "Hungary",
|
||||||
|
Country::Id => "Indonesia",
|
||||||
|
Country::Ie => "Ireland",
|
||||||
|
Country::Il => "Israel",
|
||||||
|
Country::In => "India",
|
||||||
|
Country::Iq => "Iraq",
|
||||||
|
Country::Is => "Iceland",
|
||||||
|
Country::It => "Italy",
|
||||||
|
Country::Jm => "Jamaica",
|
||||||
|
Country::Jo => "Jordan",
|
||||||
|
Country::Jp => "Japan",
|
||||||
|
Country::Ke => "Kenya",
|
||||||
|
Country::Kh => "Cambodia",
|
||||||
|
Country::Kr => "South Korea",
|
||||||
|
Country::Kw => "Kuwait",
|
||||||
|
Country::Kz => "Kazakhstan",
|
||||||
|
Country::La => "Laos",
|
||||||
|
Country::Lb => "Lebanon",
|
||||||
|
Country::Li => "Liechtenstein",
|
||||||
|
Country::Lk => "Sri Lanka",
|
||||||
|
Country::Lt => "Lithuania",
|
||||||
|
Country::Lu => "Luxembourg",
|
||||||
|
Country::Lv => "Latvia",
|
||||||
|
Country::Ly => "Libya",
|
||||||
|
Country::Ma => "Morocco",
|
||||||
|
Country::Me => "Montenegro",
|
||||||
|
Country::Mk => "North Macedonia",
|
||||||
|
Country::Mt => "Malta",
|
||||||
|
Country::Mx => "Mexico",
|
||||||
|
Country::My => "Malaysia",
|
||||||
|
Country::Ng => "Nigeria",
|
||||||
|
Country::Ni => "Nicaragua",
|
||||||
|
Country::Nl => "Netherlands",
|
||||||
|
Country::No => "Norway",
|
||||||
|
Country::Np => "Nepal",
|
||||||
|
Country::Nz => "New Zealand",
|
||||||
|
Country::Om => "Oman",
|
||||||
|
Country::Pa => "Panama",
|
||||||
|
Country::Pe => "Peru",
|
||||||
|
Country::Pg => "Papua New Guinea",
|
||||||
|
Country::Ph => "Philippines",
|
||||||
|
Country::Pk => "Pakistan",
|
||||||
|
Country::Pl => "Poland",
|
||||||
|
Country::Pr => "Puerto Rico",
|
||||||
|
Country::Pt => "Portugal",
|
||||||
|
Country::Py => "Paraguay",
|
||||||
|
Country::Qa => "Qatar",
|
||||||
|
Country::Ro => "Romania",
|
||||||
|
Country::Rs => "Serbia",
|
||||||
|
Country::Ru => "Russia",
|
||||||
|
Country::Sa => "Saudi Arabia",
|
||||||
|
Country::Se => "Sweden",
|
||||||
|
Country::Sg => "Singapore",
|
||||||
|
Country::Si => "Slovenia",
|
||||||
|
Country::Sk => "Slovakia",
|
||||||
|
Country::Sn => "Senegal",
|
||||||
|
Country::Sv => "El Salvador",
|
||||||
|
Country::Th => "Thailand",
|
||||||
|
Country::Tn => "Tunisia",
|
||||||
|
Country::Tr => "Turkey",
|
||||||
|
Country::Tw => "Taiwan",
|
||||||
|
Country::Tz => "Tanzania",
|
||||||
|
Country::Ua => "Ukraine",
|
||||||
|
Country::Ug => "Uganda",
|
||||||
|
Country::Us => "United States",
|
||||||
|
Country::Uy => "Uruguay",
|
||||||
|
Country::Ve => "Venezuela",
|
||||||
|
Country::Vn => "Vietnam",
|
||||||
|
Country::Ye => "Yemen",
|
||||||
|
Country::Za => "South Africa",
|
||||||
|
Country::Zw => "Zimbabwe",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Language {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(
|
||||||
|
&serde_json::to_string(self).map_or("".to_owned(), |s| s[1..s.len() - 1].to_owned()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Country {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(
|
||||||
|
&serde_json::to_string(self).map_or("".to_owned(), |s| s[1..s.len() - 1].to_owned()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Language {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_str(&format!("\"{}\"", s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Country {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_str(&format!("\"{}\"", s))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
|
mod locale;
|
||||||
mod ordering;
|
mod ordering;
|
||||||
pub mod stream_filter;
|
pub mod stream_filter;
|
||||||
|
|
||||||
|
pub use locale::{Country, Language};
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
@ -22,13 +25,15 @@ pub struct VideoPlayer {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
pub videos: Vec<Video>,
|
pub videos: Vec<Video>,
|
||||||
pub n_videos: u32,
|
pub n_videos: u32,
|
||||||
pub ctoken: Option<String>,
|
pub ctoken: Option<String>,
|
||||||
pub name: String,
|
|
||||||
pub thumbnails: Vec<Thumbnail>,
|
pub thumbnails: Vec<Thumbnail>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub channel: Option<Channel>,
|
pub channel: Option<Channel>,
|
||||||
|
pub last_update: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
@ -176,12 +181,6 @@ pub struct Subtitle {
|
||||||
pub auto_generated: bool,
|
pub auto_generated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct Locale {
|
|
||||||
pub lang: String,
|
|
||||||
pub country: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct Video {
|
pub struct Video {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
|
@ -331,14 +331,14 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use velcro::hash_set;
|
use velcro::hash_set;
|
||||||
|
|
||||||
const PLAYER_ML: Lazy<VideoPlayer> = Lazy::new(|| {
|
static PLAYER_ML: Lazy<VideoPlayer> = Lazy::new(|| {
|
||||||
let json_path = Path::new("testfiles/player_model/multilanguage.json");
|
let json_path = Path::new("testfiles/player_model/multilanguage.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
const PLAYER_HDR: Lazy<VideoPlayer> = Lazy::new(|| {
|
static PLAYER_HDR: Lazy<VideoPlayer> = Lazy::new(|| {
|
||||||
let json_path = Path::new("testfiles/player_model/hdr.json");
|
let json_path = Path::new("testfiles/player_model/hdr.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
|
@ -356,8 +356,7 @@ mod tests {
|
||||||
#[case::noformat(Filter::default().audio_formats(hash_set!()).to_owned(), None)]
|
#[case::noformat(Filter::default().audio_formats(hash_set!()).to_owned(), None)]
|
||||||
#[case::nocodec(Filter::default().audio_codecs(hash_set!()).to_owned(), None)]
|
#[case::nocodec(Filter::default().audio_codecs(hash_set!()).to_owned(), None)]
|
||||||
fn t_select_audio_stream(#[case] filter: Filter, #[case] expect_url: Option<&str>) {
|
fn t_select_audio_stream(#[case] filter: Filter, #[case] expect_url: Option<&str>) {
|
||||||
let player_data = PLAYER_ML;
|
let selection = PLAYER_ML.select_audio_stream(&filter);
|
||||||
let selection = player_data.select_audio_stream(&filter);
|
|
||||||
|
|
||||||
match expect_url {
|
match expect_url {
|
||||||
Some(expect_url) => assert_eq!(selection.unwrap().url, expect_url),
|
Some(expect_url) => assert_eq!(selection.unwrap().url, expect_url),
|
||||||
|
@ -376,8 +375,7 @@ mod tests {
|
||||||
#[case::noformat(Filter::default().video_formats(hash_set!()).to_owned(), None)]
|
#[case::noformat(Filter::default().video_formats(hash_set!()).to_owned(), None)]
|
||||||
#[case::nocodec(Filter::default().video_codecs(hash_set!()).to_owned(), None)]
|
#[case::nocodec(Filter::default().video_codecs(hash_set!()).to_owned(), None)]
|
||||||
fn t_select_video_only_stream(#[case] filter: Filter, #[case] expect_url: Option<&str>) {
|
fn t_select_video_only_stream(#[case] filter: Filter, #[case] expect_url: Option<&str>) {
|
||||||
let player_data = PLAYER_HDR;
|
let selection = PLAYER_HDR.select_video_only_stream(&filter);
|
||||||
let selection = player_data.select_video_only_stream(&filter);
|
|
||||||
|
|
||||||
match expect_url {
|
match expect_url {
|
||||||
Some(expect_url) => assert_eq!(selection.unwrap().url, expect_url),
|
Some(expect_url) => assert_eq!(selection.unwrap().url, expect_url),
|
||||||
|
@ -412,8 +410,7 @@ mod tests {
|
||||||
#[case] expect_video_url: Option<&str>,
|
#[case] expect_video_url: Option<&str>,
|
||||||
#[case] expect_audio_url: Option<&str>,
|
#[case] expect_audio_url: Option<&str>,
|
||||||
) {
|
) {
|
||||||
let player_data = PLAYER_HDR;
|
let (video, audio) = PLAYER_HDR.select_video_audio_stream(&filter);
|
||||||
let (video, audio) = player_data.select_video_audio_stream(&filter);
|
|
||||||
|
|
||||||
match expect_video_url {
|
match expect_video_url {
|
||||||
Some(expect_url) => assert_eq!(video.unwrap().url, expect_url),
|
Some(expect_url) => assert_eq!(video.unwrap().url, expect_url),
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
pub mod range;
|
pub mod range;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
// pub mod renderer;
|
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use serde::{de::Visitor, Deserialize, Deserializer};
|
|
||||||
use serde_with::{serde_as, DeserializeAs, rust::maps_duplicate_key_is_error::deserialize};
|
|
||||||
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// itemSectionRenderer": {
|
|
||||||
/// "contents": [
|
|
||||||
/// {
|
|
||||||
/// "playlistVideoListRenderer": {
|
|
||||||
/// "contents": [
|
|
||||||
/// {
|
|
||||||
/// "playlistVideoRenderer": { ... }
|
|
||||||
/// },
|
|
||||||
/// {
|
|
||||||
/// "playlistVideoRenderer": { ... }
|
|
||||||
/// },
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ]
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Renderer names:
|
|
||||||
///
|
|
||||||
/// 1 content element:
|
|
||||||
/// - tabRenderer > content
|
|
||||||
///
|
|
||||||
/// 1 content element (array):
|
|
||||||
/// - twoColumnBrowseResultsRenderer > tabs
|
|
||||||
/// - sectionListRenderer > contents
|
|
||||||
/// - itemSectionRenderer > contents
|
|
||||||
///
|
|
||||||
/// n content elements:
|
|
||||||
/// - playlistVideoListRenderer > contents
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(untagged, bound = "for<'de2> T: Deserialize<'de2>")]
|
|
||||||
pub enum Renderer<T> where for<'de2> T: Deserialize<'de2> {
|
|
||||||
Single {
|
|
||||||
#[serde_as(as = "crate::serializer::renderer::Renderer<T>")]
|
|
||||||
content: T,
|
|
||||||
},
|
|
||||||
Multiple {
|
|
||||||
#[serde(alias = "tabs")]
|
|
||||||
#[serde_as(as = "crate::serializer::renderer::Renderer<T>")]
|
|
||||||
contents: Vec<T>,
|
|
||||||
},
|
|
||||||
Content {
|
|
||||||
#[serde(flatten)]
|
|
||||||
inner: T,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub struct Renderer<T>(PhantomData<T>);
|
|
||||||
|
|
||||||
impl<'de, T> DeserializeAs<'de, T> for Renderer<T> {
|
|
||||||
fn deserialize_as<D>(deserializer: D) -> Result<T, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, T> DeserializeAs<'de, Vec<T>> for Renderer<T> {
|
|
||||||
fn deserialize_as<D>(deserializer: D) -> Result<Vec<T>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
struct RendererVisitor<T, U>(PhantomData<T>, PhantomData<U>);
|
|
||||||
|
|
||||||
impl<'de, T, U> Visitor<'de> for RendererVisitor<T, U>
|
|
||||||
where
|
|
||||||
U: DeserializeAs<'de, T>,
|
|
||||||
{
|
|
||||||
type Value = T;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("a yt renderer")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>, {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -65,12 +65,16 @@ pub enum TextLink {
|
||||||
page_type: PageType,
|
page_type: PageType,
|
||||||
browse_id: String,
|
browse_id: String,
|
||||||
},
|
},
|
||||||
|
Web {
|
||||||
|
text: String,
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
None {
|
None {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextLinks {}
|
pub struct TextLinks;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct TextLinkInternal {
|
struct TextLinkInternal {
|
||||||
|
@ -97,6 +101,9 @@ struct NavigationEndpoint {
|
||||||
browse_endpoint: Option<BrowseEndpoint>,
|
browse_endpoint: Option<BrowseEndpoint>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
|
url_endpoint: Option<UrlEndpoint>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
command_metadata: Option<CommandMetadata>,
|
command_metadata: Option<CommandMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,6 +120,12 @@ struct BrowseEndpoint {
|
||||||
browse_endpoint_context_supported_configs: Option<BrowseEndpointConfig>,
|
browse_endpoint_context_supported_configs: Option<BrowseEndpointConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct UrlEndpoint {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct BrowseEndpointConfig {
|
struct BrowseEndpointConfig {
|
||||||
|
@ -173,8 +186,14 @@ fn map_text_linkrun(lr: &TextLinkRun) -> Option<TextLink> {
|
||||||
},
|
},
|
||||||
browse_id: b.browse_id.to_owned(),
|
browse_id: b.browse_id.to_owned(),
|
||||||
},
|
},
|
||||||
|
None => match &nav.url_endpoint {
|
||||||
|
Some(u) => TextLink::Web {
|
||||||
|
text,
|
||||||
|
url: u.url.to_owned(),
|
||||||
|
},
|
||||||
None => TextLink::None { text },
|
None => TextLink::None { text },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,6 +417,42 @@ mod tests {
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_link_web() {
|
||||||
|
let test_json = r#"{
|
||||||
|
"ln": {
|
||||||
|
"runs": [
|
||||||
|
{
|
||||||
|
"text": "Creative Commons",
|
||||||
|
"navigationEndpoint": {
|
||||||
|
"clickTrackingParams": "CJsBEM2rARgBIhMImKz9y6Oc-QIVTJpVCh3VrAYM",
|
||||||
|
"commandMetadata": {
|
||||||
|
"webCommandMetadata": {
|
||||||
|
"url": "https://www.youtube.com/t/creative_commons",
|
||||||
|
"webPageType": "WEB_PAGE_TYPE_UNKNOWN",
|
||||||
|
"rootVe": 83769
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"urlEndpoint": {
|
||||||
|
"url": "https://www.youtube.com/t/creative_commons"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let res = serde_json::from_str::<SLink>(&test_json).unwrap();
|
||||||
|
insta::assert_debug_snapshot!(res, @r###"
|
||||||
|
SLink {
|
||||||
|
ln: Web {
|
||||||
|
text: "Creative Commons",
|
||||||
|
url: "https://www.youtube.com/t/creative_commons",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_links_artists() {
|
fn t_links_artists() {
|
||||||
let test_json = r#"{
|
let test_json = r#"{
|
||||||
|
|
326
src/timeago.rs
Normal file
326
src/timeago.rs
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{dictionary, model::Language, util};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq)]
|
||||||
|
pub struct TimeAgo {
|
||||||
|
pub n: u8,
|
||||||
|
pub unit: TimeUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct TaToken {
|
||||||
|
pub n: u8,
|
||||||
|
pub unit: Option<TimeUnit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum TimeUnit {
|
||||||
|
Second,
|
||||||
|
Minute,
|
||||||
|
Hour,
|
||||||
|
Day,
|
||||||
|
Week,
|
||||||
|
Month,
|
||||||
|
Year,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeUnit {
|
||||||
|
fn seconds(&self) -> u64 {
|
||||||
|
match self {
|
||||||
|
TimeUnit::Second => 1,
|
||||||
|
TimeUnit::Minute => 60,
|
||||||
|
TimeUnit::Hour => 3600,
|
||||||
|
TimeUnit::Day => 24 * 3600,
|
||||||
|
TimeUnit::Week => 7 * 24 * 3600,
|
||||||
|
TimeUnit::Month => 30 * 24 * 3600,
|
||||||
|
TimeUnit::Year => 365 * 24 * 3600,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeAgo {
|
||||||
|
fn seconds(&self) -> u64 {
|
||||||
|
self.n as u64 * self.unit.seconds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for TimeAgo {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.seconds() == other.seconds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for TimeAgo {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.seconds().cmp(&other.seconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for TimeAgo {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(lang: Language, textual_date: &str) -> Option<TimeAgo> {
|
||||||
|
let mappings = dictionary::get_timeago_tokens(lang);
|
||||||
|
|
||||||
|
let filtered_str = textual_date
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c != &'\u{200b}' && !c.is_ascii_digit())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let mut qu: u8 = util::parse_numeric(&textual_date).unwrap_or(1);
|
||||||
|
|
||||||
|
match lang {
|
||||||
|
Language::Ja | Language::ZhCn | Language::ZhHk | Language::ZhTw => {
|
||||||
|
filtered_str.chars().find_map(|word| {
|
||||||
|
mappings
|
||||||
|
.get(&word.to_string())
|
||||||
|
.map(|t| match t.unit {
|
||||||
|
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
|
||||||
|
None => {
|
||||||
|
qu = t.n;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => filtered_str.split(' ').find_map(|word| {
|
||||||
|
mappings
|
||||||
|
.get(word)
|
||||||
|
.map(|t| match t.unit {
|
||||||
|
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
|
||||||
|
None => {
|
||||||
|
qu = t.n;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_testfile() {
|
||||||
|
let json_path = Path::new("testfiles/date/timeago_samples.json");
|
||||||
|
|
||||||
|
let expect = [
|
||||||
|
TimeAgo {
|
||||||
|
n: 10,
|
||||||
|
unit: TimeUnit::Minute,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 20,
|
||||||
|
unit: TimeUnit::Minute,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 1,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 2,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 7,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 8,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 9,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 10,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 11,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 12,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 13,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 14,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 15,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 3,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 4,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 4,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 5,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 6,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 6,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 20,
|
||||||
|
unit: TimeUnit::Hour,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 2,
|
||||||
|
unit: TimeUnit::Day,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 3,
|
||||||
|
unit: TimeUnit::Day,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 5,
|
||||||
|
unit: TimeUnit::Day,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 6,
|
||||||
|
unit: TimeUnit::Day,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 8,
|
||||||
|
unit: TimeUnit::Day,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 10,
|
||||||
|
unit: TimeUnit::Day,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 12,
|
||||||
|
unit: TimeUnit::Day,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 2,
|
||||||
|
unit: TimeUnit::Week,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 3,
|
||||||
|
unit: TimeUnit::Week,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 4,
|
||||||
|
unit: TimeUnit::Week,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 1,
|
||||||
|
unit: TimeUnit::Month,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 8,
|
||||||
|
unit: TimeUnit::Month,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 11,
|
||||||
|
unit: TimeUnit::Month,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 1,
|
||||||
|
unit: TimeUnit::Year,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 2,
|
||||||
|
unit: TimeUnit::Year,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 3,
|
||||||
|
unit: TimeUnit::Year,
|
||||||
|
},
|
||||||
|
TimeAgo {
|
||||||
|
n: 4,
|
||||||
|
unit: TimeUnit::Year,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
let strings_map: BTreeMap<Language, Vec<String>> =
|
||||||
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
|
|
||||||
|
strings_map.iter().for_each(|(lang, strings)| {
|
||||||
|
assert_eq!(strings.len(), expect.len());
|
||||||
|
strings.iter().enumerate().for_each(|(n, s)| {
|
||||||
|
assert_eq!(
|
||||||
|
parse(*lang, s),
|
||||||
|
Some(expect[n]),
|
||||||
|
"Language: {}, n: {}",
|
||||||
|
lang,
|
||||||
|
n
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_timeago_table() {
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct TimeagoTable {
|
||||||
|
entries: BTreeMap<Language, BTreeMap<TimeUnit, TimeagoTableEntry>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct TimeagoTableEntry {
|
||||||
|
cases: BTreeMap<String, u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_path = Path::new("testfiles/date/timeago_table.json");
|
||||||
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
let timeago_table: TimeagoTable =
|
||||||
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
|
let mut n_cases = 0;
|
||||||
|
|
||||||
|
timeago_table.entries.iter().for_each(|(lang, entries)| {
|
||||||
|
entries.iter().for_each(|(t, entry)| {
|
||||||
|
entry.cases.iter().for_each(|(txt, n)| {
|
||||||
|
let timeago = parse(*lang, txt);
|
||||||
|
assert_eq!(
|
||||||
|
timeago,
|
||||||
|
Some(TimeAgo { n: *n, unit: *t }),
|
||||||
|
"lang: {}, txt: {}",
|
||||||
|
lang,
|
||||||
|
txt
|
||||||
|
);
|
||||||
|
|
||||||
|
n_cases += 1;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(n_cases, 1065)
|
||||||
|
}
|
||||||
|
}
|
41
src/util.rs
41
src/util.rs
|
@ -1,7 +1,8 @@
|
||||||
use std::collections::BTreeMap;
|
use std::{collections::BTreeMap, str::FromStr};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -18,26 +19,28 @@ where
|
||||||
.map(|c| c.get(cg).unwrap().as_str().to_owned())
|
.map(|c| c.get(cg).unwrap().as_str().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a random string with given length and byte charset.
|
/// Generate a random string with given length and byte charset.
|
||||||
fn random_string(charset: &[u8], length: usize) -> String {
|
fn random_string(charset: &[u8], length: usize) -> String {
|
||||||
let mut result = String::with_capacity(length);
|
let mut result = String::with_capacity(length);
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
unsafe {
|
|
||||||
for _ in 0..length {
|
for _ in 0..length {
|
||||||
result.push(char::from(
|
result.push(char::from(charset[rng.gen_range(0..charset.len())]));
|
||||||
*charset.get_unchecked(rng.gen_range(0..charset.len())),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a 16 characters long random string used as a CPN (Content Playback Nonce)
|
||||||
pub fn generate_content_playback_nonce() -> String {
|
pub fn generate_content_playback_nonce() -> String {
|
||||||
random_string(CONTENT_PLAYBACK_NONCE_ALPHABET, 16)
|
random_string(CONTENT_PLAYBACK_NONCE_ALPHABET, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Split an URL into its base string and parameter map
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// `example.com/api?k1=v1&k2=v2 => example.com/api; {k1: v1, k2: v2}`
|
||||||
pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>)> {
|
pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>)> {
|
||||||
let parsed_url = Url::parse(url)?;
|
let parsed_url = Url::parse(url)?;
|
||||||
let url_params: BTreeMap<String, String> = parsed_url
|
let url_params: BTreeMap<String, String> = parsed_url
|
||||||
|
@ -50,3 +53,27 @@ pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>)> {
|
||||||
|
|
||||||
Ok((url_base.to_string(), url_params))
|
Ok((url_base.to_string(), url_params))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a string after removing all non-numeric characters
|
||||||
|
pub fn parse_numeric<F>(string: &str) -> Result<F, F::Err>
|
||||||
|
where
|
||||||
|
F: FromStr,
|
||||||
|
{
|
||||||
|
static NUM_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new("\\D+").unwrap());
|
||||||
|
NUM_PATTERN.replace_all(string, "").parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case("1.000", 1000)]
|
||||||
|
#[case("4 Hello World 2", 42)]
|
||||||
|
fn t_parse_num(#[case] string: &str, #[case] expect: u32) {
|
||||||
|
let n = parse_numeric::<u32>(string).unwrap();
|
||||||
|
assert_eq!(n, expect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
902
testfiles/date/cldr_pluralrules_cardinals.json
Normal file
902
testfiles/date/cldr_pluralrules_cardinals.json
Normal file
|
@ -0,0 +1,902 @@
|
||||||
|
{
|
||||||
|
"supplemental": {
|
||||||
|
"version": {
|
||||||
|
"_unicodeVersion": "13.0.0",
|
||||||
|
"_cldrVersion": "37"
|
||||||
|
},
|
||||||
|
"plurals-type-cardinal": {
|
||||||
|
"af": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ak": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"am": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"an": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ar": {
|
||||||
|
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-few": "n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …",
|
||||||
|
"pluralRule-count-many": "n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ars": {
|
||||||
|
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-few": "n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …",
|
||||||
|
"pluralRule-count-many": "n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"as": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"asa": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ast": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"az": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"be": {
|
||||||
|
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …",
|
||||||
|
"pluralRule-count-few": "n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …",
|
||||||
|
"pluralRule-count-many": "n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …"
|
||||||
|
},
|
||||||
|
"bem": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"bez": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"bg": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"bho": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"bm": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"bn": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"bo": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"br": {
|
||||||
|
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …",
|
||||||
|
"pluralRule-count-two": "n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …",
|
||||||
|
"pluralRule-count-few": "n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …",
|
||||||
|
"pluralRule-count-many": "n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …"
|
||||||
|
},
|
||||||
|
"brx": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"bs": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ca": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ce": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ceb": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …"
|
||||||
|
},
|
||||||
|
"cgg": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"chr": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ckb": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"cs": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-few": "i = 2..4 and v = 0 @integer 2~4",
|
||||||
|
"pluralRule-count-many": "v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
|
||||||
|
},
|
||||||
|
"cy": {
|
||||||
|
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-few": "n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000",
|
||||||
|
"pluralRule-count-many": "n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000",
|
||||||
|
"pluralRule-count-other": " @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"da": {
|
||||||
|
"pluralRule-count-one": "n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"dsb": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-two": "v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"dv": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"dz": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ee": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"el": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"eo": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"et": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"eu": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"fa": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ff": {
|
||||||
|
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"fi": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"fil": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …"
|
||||||
|
},
|
||||||
|
"fo": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"fur": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"fy": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ga": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-few": "n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000",
|
||||||
|
"pluralRule-count-many": "n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"gd": {
|
||||||
|
"pluralRule-count-one": "n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000",
|
||||||
|
"pluralRule-count-few": "n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00",
|
||||||
|
"pluralRule-count-other": " @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"gl": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"gsw": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"gu": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"guw": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"gv": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …",
|
||||||
|
"pluralRule-count-two": "v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …",
|
||||||
|
"pluralRule-count-many": "v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 3~10, 13~19, 23, 103, 1003, …"
|
||||||
|
},
|
||||||
|
"ha": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"haw": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"he": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-two": "i = 2 and v = 0 @integer 2",
|
||||||
|
"pluralRule-count-many": "v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"hi": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"hr": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"hsb": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-two": "v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"hu": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"hy": {
|
||||||
|
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ia": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ig": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ii": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"in": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"io": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"is": {
|
||||||
|
"pluralRule-count-one": "t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1~1.6, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"it": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"iu": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"iw": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-two": "i = 2 and v = 0 @integer 2",
|
||||||
|
"pluralRule-count-many": "v = 0 and n != 0..10 and n % 10 = 0 @integer 20, 30, 40, 50, 60, 70, 80, 90, 100, 1000, 10000, 100000, 1000000, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"jbo": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"jgo": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ji": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"jmc": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"jv": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"jw": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ka": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kab": {
|
||||||
|
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kaj": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kcg": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kde": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kea": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kkj": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kl": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"km": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kn": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ko": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ks": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ksb": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ksh": {
|
||||||
|
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ku": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"kw": {
|
||||||
|
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000 @integer 2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, … @decimal 2.0, 22.0, 42.0, 62.0, 82.0, 102.0, 122.0, 142.0, 1000.0, 10000.0, 100000.0, …",
|
||||||
|
"pluralRule-count-few": "n % 100 = 3,23,43,63,83 @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, … @decimal 3.0, 23.0, 43.0, 63.0, 83.0, 103.0, 123.0, 143.0, 1003.0, …",
|
||||||
|
"pluralRule-count-many": "n != 1 and n % 100 = 1,21,41,61,81 @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, … @decimal 21.0, 41.0, 61.0, 81.0, 101.0, 121.0, 141.0, 161.0, 1001.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 4~19, 100, 1004, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.1, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ky": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"lag": {
|
||||||
|
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
|
||||||
|
"pluralRule-count-one": "i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"lb": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"lg": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"lkt": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ln": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"lo": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …",
|
||||||
|
"pluralRule-count-few": "n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, …",
|
||||||
|
"pluralRule-count-many": "f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"lv": {
|
||||||
|
"pluralRule-count-zero": "n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-other": " @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …"
|
||||||
|
},
|
||||||
|
"mas": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"mg": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"mgo": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"mk": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ml": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"mn": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"mo": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-few": "v != 0 or n = 0 or n % 100 = 2..19 @integer 0, 2~16, 102, 1002, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 20~35, 100, 1000, 10000, 100000, 1000000, …"
|
||||||
|
},
|
||||||
|
"mr": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"mt": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-few": "n = 0 or n % 100 = 2..10 @integer 0, 2~10, 102~107, 1002, … @decimal 0.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 102.0, 1002.0, …",
|
||||||
|
"pluralRule-count-many": "n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"my": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nah": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"naq": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nb": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nd": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ne": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nn": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nnh": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"no": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nqo": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nr": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nso": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ny": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"nyn": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"om": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"or": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"os": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"osa": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"pa": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"pap": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"pcm": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"pl": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
|
||||||
|
"pluralRule-count-many": "v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
|
||||||
|
"pluralRule-count-other": " @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"prg": {
|
||||||
|
"pluralRule-count-zero": "n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-other": " @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …"
|
||||||
|
},
|
||||||
|
"ps": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"pt": {
|
||||||
|
"pluralRule-count-one": "i = 0..1 @integer 0, 1 @decimal 0.0~1.5",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"pt-PT": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"rm": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ro": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-few": "v != 0 or n = 0 or n % 100 = 2..19 @integer 0, 2~16, 102, 1002, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 20~35, 100, 1000, 10000, 100000, 1000000, …"
|
||||||
|
},
|
||||||
|
"rof": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ru": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
|
||||||
|
"pluralRule-count-many": "v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
|
||||||
|
"pluralRule-count-other": " @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"rwk": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sah": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"saq": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sat": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sc": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"scn": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sd": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sdh": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"se": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"seh": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ses": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sg": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sh": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"shi": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-few": "n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00",
|
||||||
|
"pluralRule-count-other": " @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"si": {
|
||||||
|
"pluralRule-count-one": "n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sk": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-few": "i = 2..4 and v = 0 @integer 2~4",
|
||||||
|
"pluralRule-count-many": "v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
|
||||||
|
},
|
||||||
|
"sl": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …",
|
||||||
|
"pluralRule-count-two": "v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
|
||||||
|
},
|
||||||
|
"sma": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"smi": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"smj": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"smn": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sms": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sn": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"so": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sq": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sr": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
|
||||||
|
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ss": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ssy": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"st": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"su": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sv": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"sw": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"syr": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ta": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"te": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"teo": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"th": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ti": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"tig": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"tk": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"tl": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
|
||||||
|
"pluralRule-count-other": " @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …"
|
||||||
|
},
|
||||||
|
"tn": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"tr": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ts": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"tzm": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0",
|
||||||
|
"pluralRule-count-other": " @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ug": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"uk": {
|
||||||
|
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
|
||||||
|
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
|
||||||
|
"pluralRule-count-many": "v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
|
||||||
|
"pluralRule-count-other": " @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ur": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"uz": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"ve": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"vi": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"vo": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"vun": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"wa": {
|
||||||
|
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"wae": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"wo": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"xh": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"xog": {
|
||||||
|
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"yi": {
|
||||||
|
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
|
||||||
|
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"yo": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"yue": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"zh": {
|
||||||
|
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
},
|
||||||
|
"zu": {
|
||||||
|
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
|
||||||
|
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1237
testfiles/date/dictionary.json
Normal file
1237
testfiles/date/dictionary.json
Normal file
File diff suppressed because it is too large
Load diff
3239
testfiles/date/timeago_samples.json
Normal file
3239
testfiles/date/timeago_samples.json
Normal file
File diff suppressed because it is too large
Load diff
3956
testfiles/date/timeago_table.json
Normal file
3956
testfiles/date/timeago_table.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue