Compare commits

...

7 commits

Author SHA1 Message Date
513bf1dc9c finish timeago parser, refactor codegen 2022-09-05 21:43:43 +02:00
500ea77788 fix: improved timeago performance 2022-09-05 14:33:04 +02:00
cc63477406 test: add timeago_table 2022-09-04 14:56:45 +02:00
2bba9064fc Completed timeago table 2022-09-04 00:04:44 +02:00
17094d121b fix: response models 2022-09-04 00:04:37 +02:00
9da166304a feat: add video response
- started timeago_table
2022-09-03 11:20:07 +02:00
346406c1c8 feat: add timeago parser, playlist_cont 2022-09-01 20:16:00 +02:00
41 changed files with 58339 additions and 9327 deletions

View file

@ -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"

View file

@ -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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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

View file

@ -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
View 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 {
}

View file

@ -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!(

View file

@ -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);

View file

@ -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);
}
}

View 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>,
}

View file

@ -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>,
}

View file

@ -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)]

View file

@ -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,
}

View 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,
}

View file

@ -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: ~

View file

@ -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: ~

View file

@ -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
View 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());
}
}

View 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
View 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
View file

@ -0,0 +1,3 @@
#![cfg(test)]
mod gen_dictionary;
mod gen_locales;

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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,
};

View file

@ -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
View 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,
/// Ozbek
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 => "Ozbek",
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))
}
}

View file

@ -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,

View file

@ -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),

View file

@ -1,3 +1,2 @@
pub mod range;
pub mod text;
// pub mod renderer;

View file

@ -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>, {
}
}
*/

View file

@ -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,7 +186,13 @@ fn map_text_linkrun(lr: &TextLinkRun) -> Option<TextLink> {
},
browse_id: b.browse_id.to_owned(),
},
None => TextLink::None { text },
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
View 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)
}
}

View file

@ -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())),
));
}
for _ in 0..length {
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);
}
}

View 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, …"
}
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff