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"
|
||||
filenamify = "0.1.0"
|
||||
ress = "0.11.4"
|
||||
phf = "0.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.9.0"
|
||||
|
@ -37,3 +38,6 @@ rstest = "0.15.0"
|
|||
temp_testdir = "0.2.3"
|
||||
insta = "1.17.1"
|
||||
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 player_data = rt
|
||||
.get_player(video_id.as_str(), ClientType::Desktop)
|
||||
.get_player(video_id.as_str(), ClientType::TvHtml5Embed)
|
||||
.await
|
||||
.context(format!(
|
||||
"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
|
||||
78 Videos: PL2_OBreMn7FpDFj9lWfoZ8OQJvZkQa3yG
|
||||
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 playlist;
|
||||
pub mod video;
|
||||
|
||||
mod response;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
@ -15,7 +17,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::{
|
||||
cache::{Cache, ClientData},
|
||||
model::Locale,
|
||||
model::{Country, Language},
|
||||
util,
|
||||
};
|
||||
|
||||
|
@ -68,10 +70,8 @@ struct ClientInfo {
|
|||
platform: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
original_url: Option<String>,
|
||||
/// Language (`en`, `de`)
|
||||
hl: String,
|
||||
/// Country (`US`, `DE`)
|
||||
gl: String,
|
||||
hl: Language,
|
||||
gl: Country,
|
||||
}
|
||||
|
||||
#[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_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(),
|
||||
|
@ -136,7 +136,7 @@ const CLIENT_VERSION_REGEXES: Lazy<[Regex; 3]> = Lazy::new(|| {
|
|||
});
|
||||
|
||||
pub struct RustyTube {
|
||||
pub locale: Arc<Locale>,
|
||||
localization: Arc<Localization>,
|
||||
cache: Cache,
|
||||
desktop_client: Arc<DesktopClient>,
|
||||
desktop_music_client: Arc<DesktopMusicClient>,
|
||||
|
@ -145,17 +145,30 @@ pub struct RustyTube {
|
|||
tvhtml5embed_client: Arc<TvHtml5EmbedClient>,
|
||||
}
|
||||
|
||||
struct Localization {
|
||||
language: Language,
|
||||
content_country: Country,
|
||||
}
|
||||
|
||||
impl RustyTube {
|
||||
#[must_use]
|
||||
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]
|
||||
pub fn new_with_ua(lang: &str, country: &str, cache_file: Option<String>) -> Self {
|
||||
let locale = Arc::new(Locale {
|
||||
lang: lang.to_owned(),
|
||||
country: country.to_owned(),
|
||||
pub fn new_with_ua(
|
||||
language: Language,
|
||||
content_country: Country,
|
||||
cache_file: Option<String>,
|
||||
) -> Self {
|
||||
let loc = Arc::new(Localization {
|
||||
language,
|
||||
content_country,
|
||||
});
|
||||
|
||||
let cache = match cache_file.as_ref() {
|
||||
|
@ -164,17 +177,17 @@ impl RustyTube {
|
|||
};
|
||||
|
||||
Self {
|
||||
locale: locale.clone(),
|
||||
localization: loc.clone(),
|
||||
cache: cache.clone(),
|
||||
desktop_client: Arc::new(DesktopClient::new(locale.clone(), cache.clone())),
|
||||
desktop_music_client: Arc::new(DesktopMusicClient::new(locale.clone(), cache)),
|
||||
android_client: Arc::new(AndroidClient::new(locale.clone())),
|
||||
ios_client: Arc::new(IosClient::new(locale.clone())),
|
||||
tvhtml5embed_client: Arc::new(TvHtml5EmbedClient::new(locale)),
|
||||
desktop_client: Arc::new(DesktopClient::new(loc.clone(), cache.clone())),
|
||||
desktop_music_client: Arc::new(DesktopMusicClient::new(loc.clone(), cache)),
|
||||
android_client: Arc::new(AndroidClient::new(loc.clone())),
|
||||
ios_client: Arc::new(IosClient::new(loc.clone())),
|
||||
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 {
|
||||
ClientType::Desktop => self.desktop_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 {
|
||||
locale: Arc<Locale>,
|
||||
localization: Arc<Localization>,
|
||||
http: Client,
|
||||
cache: Cache,
|
||||
consent_cookie: String,
|
||||
|
@ -220,12 +233,12 @@ impl YTClient for DesktopClient {
|
|||
platform: "DESKTOP".to_owned(),
|
||||
original_url: Some("https://www.youtube.com/".to_owned()),
|
||||
hl: match localized {
|
||||
true => self.locale.lang.to_owned(),
|
||||
false => "en".to_owned(),
|
||||
true => self.localization.language,
|
||||
false => Language::En,
|
||||
},
|
||||
gl: match localized {
|
||||
true => self.locale.country.to_owned(),
|
||||
false => "US".to_owned(),
|
||||
true => self.localization.content_country,
|
||||
false => Country::Us,
|
||||
},
|
||||
},
|
||||
request: Some(RequestYT::default()),
|
||||
|
@ -260,7 +273,7 @@ impl YTClient for 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 http = ClientBuilder::new()
|
||||
|
@ -271,7 +284,7 @@ impl DesktopClient {
|
|||
.expect("unable to build the HTTP client");
|
||||
|
||||
Self {
|
||||
locale,
|
||||
localization,
|
||||
http,
|
||||
cache,
|
||||
consent_cookie: format!(
|
||||
|
@ -329,7 +342,7 @@ impl DesktopClient {
|
|||
}
|
||||
|
||||
pub struct AndroidClient {
|
||||
locale: Arc<Locale>,
|
||||
localization: Arc<Localization>,
|
||||
http: Client,
|
||||
}
|
||||
|
||||
|
@ -345,12 +358,12 @@ impl YTClient for AndroidClient {
|
|||
platform: "MOBILE".to_owned(),
|
||||
original_url: None,
|
||||
hl: match localized {
|
||||
true => self.locale.lang.to_owned(),
|
||||
false => "en".to_owned(),
|
||||
true => self.localization.language,
|
||||
false => Language::En,
|
||||
},
|
||||
gl: match localized {
|
||||
true => self.locale.country.to_owned(),
|
||||
false => "US".to_owned(),
|
||||
true => self.localization.content_country,
|
||||
false => Country::Us,
|
||||
},
|
||||
},
|
||||
request: None,
|
||||
|
@ -384,22 +397,22 @@ impl YTClient for AndroidClient {
|
|||
}
|
||||
|
||||
impl AndroidClient {
|
||||
fn new(locale: Arc<Locale>) -> Self {
|
||||
fn new(localization: Arc<Localization>) -> Self {
|
||||
let http = ClientBuilder::new()
|
||||
.user_agent(format!(
|
||||
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
|
||||
MOBILE_CLIENT_VERSION, locale.country
|
||||
MOBILE_CLIENT_VERSION, localization.content_country
|
||||
))
|
||||
.gzip(true)
|
||||
.build()
|
||||
.expect("unable to build the HTTP client");
|
||||
|
||||
Self { locale, http }
|
||||
Self { localization, http }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IosClient {
|
||||
locale: Arc<Locale>,
|
||||
localization: Arc<Localization>,
|
||||
http: Client,
|
||||
}
|
||||
|
||||
|
@ -415,12 +428,12 @@ impl YTClient for IosClient {
|
|||
platform: "MOBILE".to_owned(),
|
||||
original_url: None,
|
||||
hl: match localized {
|
||||
true => self.locale.lang.to_owned(),
|
||||
false => "en".to_owned(),
|
||||
true => self.localization.language,
|
||||
false => Language::En,
|
||||
},
|
||||
gl: match localized {
|
||||
true => self.locale.country.to_owned(),
|
||||
false => "US".to_owned(),
|
||||
true => self.localization.content_country,
|
||||
false => Country::Us,
|
||||
},
|
||||
},
|
||||
request: None,
|
||||
|
@ -451,22 +464,22 @@ impl YTClient for IosClient {
|
|||
}
|
||||
|
||||
impl IosClient {
|
||||
fn new(locale: Arc<Locale>) -> Self {
|
||||
fn new(localization: Arc<Localization>) -> Self {
|
||||
let http = ClientBuilder::new()
|
||||
.user_agent(format!(
|
||||
"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)
|
||||
.build()
|
||||
.expect("unable to build the HTTP client");
|
||||
|
||||
Self { locale, http }
|
||||
Self { localization, http }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TvHtml5EmbedClient {
|
||||
locale: Arc<Locale>,
|
||||
localization: Arc<Localization>,
|
||||
http: Client,
|
||||
}
|
||||
|
||||
|
@ -482,12 +495,12 @@ impl YTClient for TvHtml5EmbedClient {
|
|||
platform: "TV".to_owned(),
|
||||
original_url: None,
|
||||
hl: match localized {
|
||||
true => self.locale.lang.to_owned(),
|
||||
false => "en".to_owned(),
|
||||
true => self.localization.language,
|
||||
false => Language::En,
|
||||
},
|
||||
gl: match localized {
|
||||
true => self.locale.country.to_owned(),
|
||||
false => "US".to_owned(),
|
||||
true => self.localization.content_country,
|
||||
false => Country::Us,
|
||||
},
|
||||
},
|
||||
request: Some(RequestYT::default()),
|
||||
|
@ -523,7 +536,7 @@ impl YTClient for TvHtml5EmbedClient {
|
|||
}
|
||||
|
||||
impl TvHtml5EmbedClient {
|
||||
fn new(locale: Arc<Locale>) -> Self {
|
||||
fn new(localization: Arc<Localization>) -> Self {
|
||||
let http = ClientBuilder::new()
|
||||
.user_agent(DEFAULT_UA)
|
||||
.gzip(true)
|
||||
|
@ -531,12 +544,12 @@ impl TvHtml5EmbedClient {
|
|||
.build()
|
||||
.expect("unable to build the HTTP client");
|
||||
|
||||
Self { locale, http }
|
||||
Self { localization, http }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DesktopMusicClient {
|
||||
locale: Arc<Locale>,
|
||||
localization: Arc<Localization>,
|
||||
http: Client,
|
||||
cache: Cache,
|
||||
consent_cookie: String,
|
||||
|
@ -554,12 +567,12 @@ impl YTClient for DesktopMusicClient {
|
|||
platform: "DESKTOP".to_owned(),
|
||||
original_url: Some("https://music.youtube.com/".to_owned()),
|
||||
hl: match localized {
|
||||
true => self.locale.lang.to_owned(),
|
||||
false => "en".to_owned(),
|
||||
true => self.localization.language,
|
||||
false => Language::En,
|
||||
},
|
||||
gl: match localized {
|
||||
true => self.locale.country.to_owned(),
|
||||
false => "US".to_owned(),
|
||||
true => self.localization.content_country,
|
||||
false => Country::Us,
|
||||
},
|
||||
},
|
||||
request: Some(RequestYT::default()),
|
||||
|
@ -597,7 +610,7 @@ impl YTClient for 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 http = ClientBuilder::new()
|
||||
|
@ -608,7 +621,7 @@ impl DesktopMusicClient {
|
|||
.expect("unable to build the HTTP client");
|
||||
|
||||
Self {
|
||||
locale,
|
||||
localization,
|
||||
http,
|
||||
cache,
|
||||
consent_cookie: format!(
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use chrono::{DateTime, NaiveDateTime, NaiveTime, Utc};
|
||||
use chrono::{NaiveDateTime, NaiveTime, TimeZone, Utc};
|
||||
use fancy_regex::Regex;
|
||||
use log::{error, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
|
@ -16,8 +16,6 @@ use url::Url;
|
|||
use super::{response, ClientType, ContextYT, RustyTube, YTClient};
|
||||
use crate::{client::response::player, deobfuscate::Deobfuscator, model::*, util};
|
||||
|
||||
// REQUEST
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QPlayer {
|
||||
|
@ -369,7 +367,7 @@ fn map_player_data(response: response::Player, deobf: &Deobfuscator) -> Result<V
|
|||
},
|
||||
publish_date: microformat.as_ref().map(|m| {
|
||||
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,
|
||||
keywords: video_details
|
||||
|
@ -532,9 +530,9 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Assert equality within 10% margin
|
||||
fn assert_approx(left: u32, right: u32) {
|
||||
fn assert_approx(left: f64, right: f64) {
|
||||
if left != right {
|
||||
let f = left as f64 / right as f64;
|
||||
let f = left / right;
|
||||
assert!(
|
||||
0.9 < f && f < 1.1,
|
||||
"{} not within 10% margin of {}",
|
||||
|
@ -595,7 +593,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
// 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.size, 43553412);
|
||||
assert_eq!(video.width, 1280);
|
||||
|
@ -607,7 +605,7 @@ mod tests {
|
|||
assert_eq!(video.format, VideoFormat::Webm);
|
||||
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.size, 4193863);
|
||||
assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\"");
|
||||
|
@ -625,9 +623,9 @@ mod tests {
|
|||
.find(|s| s.itag == 251)
|
||||
.unwrap();
|
||||
|
||||
assert_approx(video.bitrate, 1340829);
|
||||
assert_eq!(video.average_bitrate, 1233444);
|
||||
assert_eq!(video.size, 39936630);
|
||||
assert_approx(video.bitrate as f64, 1340829.0);
|
||||
assert_approx(video.average_bitrate as f64, 1233444.0);
|
||||
assert_approx(video.size as f64, 39936630.0);
|
||||
assert_eq!(video.width, 1280);
|
||||
assert_eq!(video.height, 720);
|
||||
assert_eq!(video.fps, 30);
|
||||
|
@ -638,9 +636,9 @@ mod tests {
|
|||
assert_eq!(video.codec, VideoCodec::Av01);
|
||||
assert_eq!(video.throttled, false);
|
||||
|
||||
assert_approx(audio.bitrate, 142718);
|
||||
assert_eq!(audio.average_bitrate, 130708);
|
||||
assert_eq!(audio.size, 4232344);
|
||||
assert_approx(audio.bitrate as f64, 142718.0);
|
||||
assert_approx(audio.average_bitrate as f64, 130708.0);
|
||||
assert_approx(audio.size as f64, 4232344.0);
|
||||
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
||||
assert_eq!(audio.format, AudioFormat::Webm);
|
||||
assert_eq!(audio.codec, AudioCodec::Opus);
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// REQUEST
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
@ -18,10 +16,11 @@ struct QPlaylist {
|
|||
browse_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TmpEntry {
|
||||
pub title: String,
|
||||
pub video_id: String,
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QPlaylistCont {
|
||||
context: ContextYT,
|
||||
continuation: String,
|
||||
}
|
||||
|
||||
impl RustyTube {
|
||||
|
@ -46,6 +45,51 @@ impl RustyTube {
|
|||
|
||||
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> {
|
||||
|
@ -74,49 +118,7 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
.playlist_video_list_renderer
|
||||
.contents;
|
||||
|
||||
let mut ctoken: Option<String> = None;
|
||||
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 (videos, ctoken) = map_playlist_items(video_items);
|
||||
|
||||
let thumbnail_renderer = some_or_bail!(
|
||||
response
|
||||
|
@ -151,7 +153,7 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
match &response.header.playlist_header_renderer.num_videos_text {
|
||||
Text::Multiple { runs } =>
|
||||
if runs.len() == 2 && runs[1] == " videos" {
|
||||
runs[0].parse().ok()
|
||||
runs[0].replace(",", "").replace(".", "").parse().ok()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
@ -175,6 +177,11 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let id = response
|
||||
.header
|
||||
.playlist_header_renderer
|
||||
.playlist_id
|
||||
.to_owned();
|
||||
let name = response.header.playlist_header_renderer.title.to_owned();
|
||||
let description = response
|
||||
.header
|
||||
|
@ -201,16 +208,65 @@ fn map_playlist(response: &response::Playlist) -> Result<Playlist> {
|
|||
};
|
||||
|
||||
Ok(Playlist {
|
||||
id,
|
||||
name,
|
||||
videos,
|
||||
n_videos,
|
||||
ctoken,
|
||||
name,
|
||||
thumbnails,
|
||||
description,
|
||||
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)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::BufReader, path::Path};
|
||||
|
@ -299,6 +355,7 @@ mod tests {
|
|||
let rt = RustyTube::new();
|
||||
let playlist = rt.get_playlist(id).await.unwrap();
|
||||
|
||||
assert_eq!(playlist.id, id);
|
||||
assert_eq!(playlist.name, name);
|
||||
assert!(!playlist.videos.is_empty());
|
||||
assert_eq!(playlist.ctoken.is_some(), is_long);
|
||||
|
@ -323,4 +380,19 @@ mod tests {
|
|||
let playlist_data = map_playlist(&playlist).unwrap();
|
||||
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 playlist;
|
||||
pub mod playlist_music;
|
||||
pub mod video;
|
||||
|
||||
pub use channel::Channel;
|
||||
pub use player::Player;
|
||||
pub use playlist::Playlist;
|
||||
pub use playlist_music::PlaylistMusic;
|
||||
pub use video::Video;
|
||||
pub use video::VideoComments;
|
||||
pub use video::VideoRecommendations;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||
|
@ -44,10 +50,19 @@ pub struct Thumbnail {
|
|||
pub height: u32,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContinuationItemRenderer {
|
||||
pub continuation_endpoint: ContinuationEndpoint,
|
||||
pub enum VideoListItem<T> {
|
||||
#[serde(alias = "playlistVideoRenderer", alias = "compactVideoRenderer")]
|
||||
GridVideoRenderer {
|
||||
#[serde(flatten)]
|
||||
video: T,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContinuationItemRenderer {
|
||||
continuation_endpoint: ContinuationEndpoint,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
@ -62,6 +77,77 @@ pub struct ContinuationCommand {
|
|||
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
|
||||
|
||||
#[serde_as]
|
||||
|
@ -72,10 +158,8 @@ pub struct MusicItem {
|
|||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
pub playlist_item_data: Option<PlaylistItemData>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub flex_columns: Vec<MusicColumn>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub fixed_columns: Vec<MusicColumn>,
|
||||
}
|
||||
|
|
|
@ -24,27 +24,15 @@ pub enum PlayabilityStatus {
|
|||
Ok { live_streamability: Option<Empty> },
|
||||
/// Video cant be played because of DRM / Geoblock
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Unplayable {
|
||||
reason: String,
|
||||
// error_screen: Option<ErrorScreen>,
|
||||
},
|
||||
Unplayable { reason: String },
|
||||
/// Age limit / Private video
|
||||
#[serde(rename_all = "camelCase")]
|
||||
LoginRequired {
|
||||
reason: String,
|
||||
// error_screen: Option<ErrorScreen>
|
||||
},
|
||||
LoginRequired { reason: String },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
LiveStreamOffline {
|
||||
reason: String,
|
||||
// error_screen: Option<ErrorScreen>
|
||||
},
|
||||
LiveStreamOffline { reason: String },
|
||||
/// Video was censored / deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Error {
|
||||
reason: String,
|
||||
// error_screen: Option<ErrorScreen>
|
||||
},
|
||||
Error { reason: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
|
|
@ -4,7 +4,9 @@ use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
|||
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -14,6 +16,14 @@ pub struct Playlist {
|
|||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Contents {
|
||||
|
@ -49,21 +59,7 @@ pub struct PlaylistVideoListRenderer {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistVideoList {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
pub contents: Vec<PlaylistVideoItem>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
pub contents: Vec<VideoListItem<PlaylistVideo>>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
@ -79,7 +75,6 @@ pub struct PlaylistVideo {
|
|||
pub channel: TextLink,
|
||||
#[serde_as(as = "JsonString")]
|
||||
pub length_seconds: u32,
|
||||
pub is_playable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
@ -128,7 +123,7 @@ pub enum SidebarRendererItem {
|
|||
// stats: Vec<Text>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PlaylistSidebarSecondaryInfoRenderer { video_owner: VideoOwnerWrap },
|
||||
PlaylistSidebarSecondaryInfoRenderer { video_owner: VideoOwner },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
@ -141,14 +136,15 @@ pub struct PlaylistThumbnailRenderer {
|
|||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoOwnerWrap {
|
||||
pub video_owner_renderer: VideoOwner,
|
||||
pub struct OnResponseReceivedAction {
|
||||
pub append_continuation_items_action: AppendAction,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoOwner {
|
||||
#[serde_as(as = "crate::serializer::text::TextLink")]
|
||||
pub title: TextLink,
|
||||
pub struct AppendAction {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
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
|
||||
expression: playlist_data
|
||||
---
|
||||
id: PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ
|
||||
name: Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022
|
||||
videos:
|
||||
- id: Bkj3IVIO2Os
|
||||
title: Stereoact feat. Kerstin Ott - Die Immer Lacht (Official Video HD)
|
||||
|
@ -1905,7 +1907,6 @@ videos:
|
|||
name: KMNGANG
|
||||
n_videos: 495
|
||||
ctoken: 4qmFsgJhEiRWTFBMNWREeDY4MVQ0YlI3WkYxSXVXek92MW9tbFJiRTdQaUoaFENBRjZCbEJVT2tOSFdRJTNEJTNEmgIiUEw1ZER4NjgxVDRiUjdaRjFJdVd6T3Yxb21sUmJFN1BpSg%3D%3D
|
||||
name: Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022
|
||||
thumbnails:
|
||||
- url: "https://i.ytimg.com/vi/Bkj3IVIO2Os/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLBShlXfy9oWvTy2ntoHxmuhBlZP3g"
|
||||
width: 168
|
||||
|
@ -1923,4 +1924,5 @@ description: ~
|
|||
channel:
|
||||
id: UCIekuFeMaV78xYfvpmoCnPg
|
||||
name: Best Music
|
||||
last_update: ~
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
source: src/client/playlist.rs
|
||||
expression: playlist_data
|
||||
---
|
||||
id: PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe
|
||||
name: Minecraft SHINE
|
||||
videos:
|
||||
- id: X82TrticM4A
|
||||
title: Minecraft SHINE (Trailer)
|
||||
|
@ -1259,7 +1261,6 @@ videos:
|
|||
name: Chaosflo44
|
||||
n_videos: 66
|
||||
ctoken: ~
|
||||
name: Minecraft SHINE
|
||||
thumbnails:
|
||||
- url: "https://i.ytimg.com/vi/X82TrticM4A/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLBeSNDZEaBoP0KX9_Ayn3VO3X8rkw"
|
||||
width: 168
|
||||
|
@ -1277,4 +1278,5 @@ description: "SHINE - Survival Hardcore in New Environment: Auf einem Server mac
|
|||
channel:
|
||||
id: UCQM0bS4_04-Y4JuYrgmnpZQ
|
||||
name: Chaosflo44
|
||||
last_update: ~
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
source: src/client/playlist.rs
|
||||
expression: playlist_data
|
||||
---
|
||||
id: RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk
|
||||
name: Easy Pop
|
||||
videos:
|
||||
- id: psuRGfAaju4
|
||||
title: Owl City - Fireflies (Official Music Video)
|
||||
|
@ -1848,7 +1850,6 @@ videos:
|
|||
name: Diaven - Topic
|
||||
n_videos: 97
|
||||
ctoken: ~
|
||||
name: Easy Pop
|
||||
thumbnails:
|
||||
- url: "https://i9.ytimg.com/s_p/RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk/mqdefault.jpg?sqp=CPDmr5gGir7X7AMGCLaHmpgG&rs=AOn4CLDg9xLeJDPeMtbWfp19VFd6vCQzqQ&v=1661371318"
|
||||
width: 180
|
||||
|
@ -1861,4 +1862,5 @@ thumbnails:
|
|||
height: 1200
|
||||
description: ~
|
||||
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()
|
||||
+ ";";
|
||||
|
||||
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!(
|
||||
helper_object_name_pattern
|
||||
HELPER_OBJECT_NAME_PATTERN
|
||||
.captures(&deobfuscate_function)
|
||||
.ok()
|
||||
.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> {
|
||||
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]\\)")
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
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"))
|
||||
);
|
||||
|
||||
|
@ -315,11 +317,12 @@ async fn get_player_js_url(http: &Client) -> Result<String> {
|
|||
.error_for_status()?;
|
||||
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})\\\/"#)
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
});
|
||||
let player_hash = some_or_bail!(
|
||||
player_hash_pattern.captures(&text)?,
|
||||
PLAYER_HASH_PATTERN.captures(&text)?,
|
||||
Err(anyhow!("could not find player hash"))
|
||||
)
|
||||
.get(1)
|
||||
|
@ -338,10 +341,11 @@ async fn get_response(http: &Client, url: &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!(
|
||||
sts_pattern.captures(&player_js)?,
|
||||
STS_PATTERN.captures(&player_js)?,
|
||||
Err(anyhow!("could not find sts"))
|
||||
)
|
||||
.get(1)
|
||||
|
@ -357,7 +361,7 @@ mod tests {
|
|||
use super::*;
|
||||
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");
|
||||
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::{
|
||||
model::{stream_filter::Filter, AudioCodec, FileFormat, VideoPlayer, VideoCodec},
|
||||
model::{stream_filter::Filter, AudioCodec, FileFormat, VideoCodec, VideoPlayer},
|
||||
util,
|
||||
};
|
||||
|
||||
|
|
|
@ -3,11 +3,16 @@
|
|||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
#[cfg(test)]
|
||||
mod codegen;
|
||||
|
||||
mod cache;
|
||||
mod deobfuscate;
|
||||
mod dictionary;
|
||||
mod serializer;
|
||||
mod util;
|
||||
|
||||
pub mod client;
|
||||
pub mod download;
|
||||
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;
|
||||
pub mod stream_filter;
|
||||
|
||||
pub use locale::{Country, Language};
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
@ -22,13 +25,15 @@ pub struct VideoPlayer {
|
|||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Playlist {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub videos: Vec<Video>,
|
||||
pub n_videos: u32,
|
||||
pub ctoken: Option<String>,
|
||||
pub name: String,
|
||||
pub thumbnails: Vec<Thumbnail>,
|
||||
pub description: Option<String>,
|
||||
pub channel: Option<Channel>,
|
||||
pub last_update: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
@ -176,12 +181,6 @@ pub struct Subtitle {
|
|||
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)]
|
||||
pub struct Video {
|
||||
pub id: String,
|
||||
|
|
|
@ -331,14 +331,14 @@ mod tests {
|
|||
use rstest::rstest;
|
||||
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_file = File::open(json_path).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_file = File::open(json_path).unwrap();
|
||||
|
||||
|
@ -356,8 +356,7 @@ mod tests {
|
|||
#[case::noformat(Filter::default().audio_formats(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>) {
|
||||
let player_data = PLAYER_ML;
|
||||
let selection = player_data.select_audio_stream(&filter);
|
||||
let selection = PLAYER_ML.select_audio_stream(&filter);
|
||||
|
||||
match 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::nocodec(Filter::default().video_codecs(hash_set!()).to_owned(), None)]
|
||||
fn t_select_video_only_stream(#[case] filter: Filter, #[case] expect_url: Option<&str>) {
|
||||
let player_data = PLAYER_HDR;
|
||||
let selection = player_data.select_video_only_stream(&filter);
|
||||
let selection = PLAYER_HDR.select_video_only_stream(&filter);
|
||||
|
||||
match 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_audio_url: Option<&str>,
|
||||
) {
|
||||
let player_data = PLAYER_HDR;
|
||||
let (video, audio) = player_data.select_video_audio_stream(&filter);
|
||||
let (video, audio) = PLAYER_HDR.select_video_audio_stream(&filter);
|
||||
|
||||
match expect_video_url {
|
||||
Some(expect_url) => assert_eq!(video.unwrap().url, expect_url),
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
pub mod range;
|
||||
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,
|
||||
browse_id: String,
|
||||
},
|
||||
Web {
|
||||
text: String,
|
||||
url: String,
|
||||
},
|
||||
None {
|
||||
text: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct TextLinks {}
|
||||
pub struct TextLinks;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TextLinkInternal {
|
||||
|
@ -97,6 +101,9 @@ struct NavigationEndpoint {
|
|||
browse_endpoint: Option<BrowseEndpoint>,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
url_endpoint: Option<UrlEndpoint>,
|
||||
#[serde(default)]
|
||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||
command_metadata: Option<CommandMetadata>,
|
||||
}
|
||||
|
||||
|
@ -113,6 +120,12 @@ struct BrowseEndpoint {
|
|||
browse_endpoint_context_supported_configs: Option<BrowseEndpointConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UrlEndpoint {
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct BrowseEndpointConfig {
|
||||
|
@ -173,8 +186,14 @@ fn map_text_linkrun(lr: &TextLinkRun) -> Option<TextLink> {
|
|||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
fn t_links_artists() {
|
||||
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 fancy_regex::Regex;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use url::Url;
|
||||
|
||||
|
@ -18,26 +19,28 @@ where
|
|||
.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 {
|
||||
let mut result = String::with_capacity(length);
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
unsafe {
|
||||
for _ in 0..length {
|
||||
result.push(char::from(
|
||||
*charset.get_unchecked(rng.gen_range(0..charset.len())),
|
||||
));
|
||||
}
|
||||
result.push(char::from(charset[rng.gen_range(0..charset.len())]));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Generate a 16 characters long random string used as a CPN (Content Playback Nonce)
|
||||
pub fn generate_content_playback_nonce() -> String {
|
||||
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>)> {
|
||||
let parsed_url = Url::parse(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))
|
||||
}
|
||||
|
||||
/// 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