Compare commits
2 commits
86a348f210
...
5d19259a14
Author | SHA1 | Date | |
---|---|---|---|
5d19259a14 | |||
67ae1eb21d |
33 changed files with 44801 additions and 129 deletions
|
@ -10,7 +10,7 @@ inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
- [X] **Player** (video/audio streams, subtitles)
|
- [X] **Player** (video/audio streams, subtitles)
|
||||||
- TODO: Livestream support
|
- TODO: Livestream support
|
||||||
- [X] **Playlist**
|
- [X] **Playlist**
|
||||||
- [ ] **VideoDetails** (metadata, comments, recommended videos)
|
- [X] **VideoDetails** (metadata, comments, recommended videos)
|
||||||
- [ ] **Channel**
|
- [ ] **Channel**
|
||||||
- [ ] **ChannelRSS**
|
- [ ] **ChannelRSS**
|
||||||
- [ ] **Search**
|
- [ ] **Search**
|
||||||
|
|
358
codegen/src/collect_large_numbers.rs
Normal file
358
codegen/src/collect_large_numbers.rs
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use fancy_regex::Regex;
|
||||||
|
use futures::{stream, StreamExt};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use reqwest::{header, Client};
|
||||||
|
use rustypipe::model::{locale::LANGUAGES, Language};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_with::serde_as;
|
||||||
|
use serde_with::VecSkipError;
|
||||||
|
|
||||||
|
use crate::util::{self, Text};
|
||||||
|
|
||||||
|
type CollectedNumbers = BTreeMap<Language, BTreeMap<u8, (String, u64)>>;
|
||||||
|
|
||||||
|
/// Collect video view count texts in every supported language
|
||||||
|
/// and write them to `testfiles/dict/large_number_samples.json`.
|
||||||
|
///
|
||||||
|
/// YouTube's API outputs the subscriber count of a channel only in a
|
||||||
|
/// approximated format (e.g *880K subscribers*), which varies
|
||||||
|
/// by language.
|
||||||
|
///
|
||||||
|
/// To parse these numbers correctly we need to collect textual numbers
|
||||||
|
/// of different orders of magnitude in every language. This script extracts
|
||||||
|
/// the view count texts from the most popular videos of different channels.
|
||||||
|
///
|
||||||
|
/// We extract these instead of subscriber counts because the YouTube API
|
||||||
|
/// outputs view counts both in approximated and exact format, so we can use
|
||||||
|
/// the exact counts to figure out the tokens.
|
||||||
|
pub async fn collect_large_numbers(project_root: &Path, concurrency: usize) {
|
||||||
|
let mut json_path = project_root.to_path_buf();
|
||||||
|
json_path.push("testfiles/dict/large_number_samples.json");
|
||||||
|
|
||||||
|
let channels = [
|
||||||
|
"UCq-Fj5jknLsUf-MWSy4_brA", // 10e8 (225M)
|
||||||
|
"UCcdwLMPsaU2ezNSJU1nFoBQ", // 10e7 (60M)
|
||||||
|
"UC6mIxFTvXkWQVEHPsEdflzQ", // 10e6 (1.7M)
|
||||||
|
"UCD0y51PJfvkZNe3y3FR5riw", // 10e5 (125K)
|
||||||
|
"UCNcN0dW43zE0Om3278fjY8A", // 10e4 (27K)
|
||||||
|
"UC0QEucPrn0-Ddi3JBTcs5Kw", // 10e3 (5K)
|
||||||
|
"UCGiJh0NZ52wRhYKYnuZI08Q", // 10e1 (37)
|
||||||
|
];
|
||||||
|
|
||||||
|
let collected_numbers: CollectedNumbers = stream::iter(LANGUAGES)
|
||||||
|
.map(|lang| async move {
|
||||||
|
let mut entry = BTreeMap::new();
|
||||||
|
|
||||||
|
for (n, ch_id) in channels.iter().enumerate() {
|
||||||
|
let channel = get_channel(ch_id, lang)
|
||||||
|
.await
|
||||||
|
.context(format!("{}-{}", lang, n))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
channel.view_counts.iter().for_each(|(num, txt)| {
|
||||||
|
entry.insert(get_mag(*num), (txt.to_owned(), *num));
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("collected {}-{}", lang, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
(lang, entry)
|
||||||
|
})
|
||||||
|
.buffer_unordered(concurrency)
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let file = File::create(json_path).unwrap();
|
||||||
|
serde_json::to_writer_pretty(file, &collected_numbers).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to parse the numbers collected by `collect-large-numbers`
|
||||||
|
/// and write the results to `dictionary.json`.
|
||||||
|
pub fn write_samples_to_dict(project_root: &Path) {
|
||||||
|
let mut json_path = project_root.to_path_buf();
|
||||||
|
json_path.push("testfiles/dict/large_number_samples.json");
|
||||||
|
|
||||||
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
let collected_nums: CollectedNumbers =
|
||||||
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
|
let mut dict = util::read_dict(project_root);
|
||||||
|
let langs = dict.keys().map(|k| k.to_owned()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
static POINT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\d(\.|,)\d{1,3}(?:\D|$)").unwrap());
|
||||||
|
|
||||||
|
for lang in langs {
|
||||||
|
let dict_entry = dict.entry(lang).or_default();
|
||||||
|
|
||||||
|
let mut e_langs = dict_entry.equivalent.clone();
|
||||||
|
e_langs.push(lang);
|
||||||
|
|
||||||
|
let comma_decimal = collected_nums
|
||||||
|
.get(&lang)
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find_map(|(mag, (txt, _))| {
|
||||||
|
let point = POINT_REGEX
|
||||||
|
.captures(txt)
|
||||||
|
.unwrap()
|
||||||
|
.map(|c| c.get(1).unwrap().as_str());
|
||||||
|
|
||||||
|
if let Some(point) = point {
|
||||||
|
let num_all = util::parse_numeric::<u64>(txt).unwrap();
|
||||||
|
// If the number parsed from all digits has the same order of
|
||||||
|
// magnitude as the actual number, it must be a separator.
|
||||||
|
// Otherwise it is a decimal point
|
||||||
|
return Some((get_mag(num_all) == *mag) ^ (point == ","));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let decimal_point = match comma_decimal {
|
||||||
|
true => ",",
|
||||||
|
false => ".",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search for tokens
|
||||||
|
|
||||||
|
// This map holds all the tokens we encounter while parsing the language
|
||||||
|
// If a new token is found, it is stored in this map with the derived order of
|
||||||
|
// magnitude.
|
||||||
|
// If the token is found again with a different derived order of magnitude,
|
||||||
|
// its value in the map is set to None.
|
||||||
|
let mut found_tokens: HashMap<String, Option<u8>> = HashMap::new();
|
||||||
|
|
||||||
|
let mut insert_token = |token: String, mag: u8| {
|
||||||
|
let found_token = found_tokens.entry(token).or_insert(match mag {
|
||||||
|
0 => None,
|
||||||
|
x => Some(x),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(f) = found_token {
|
||||||
|
if *f != mag {
|
||||||
|
*found_token = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for lang in e_langs {
|
||||||
|
let entry = collected_nums.get(&lang).unwrap();
|
||||||
|
|
||||||
|
entry.iter().for_each(|(mag, (txt, _))| {
|
||||||
|
let filtered = util::filter_largenumstr(txt);
|
||||||
|
|
||||||
|
let tokens: Vec<String> = match dict_entry.by_char {
|
||||||
|
true => filtered.chars().map(|c| c.to_string()).collect(),
|
||||||
|
false => filtered.split_whitespace().map(|c| c.to_string()).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let num_before_point =
|
||||||
|
util::parse_numeric::<u64>(txt.split(decimal_point).next().unwrap()).unwrap();
|
||||||
|
let mag_before_point = get_mag(num_before_point);
|
||||||
|
let mut mag_remaining = mag - mag_before_point;
|
||||||
|
|
||||||
|
tokens.iter().for_each(|t| {
|
||||||
|
// These tokens are correct in all languages
|
||||||
|
// and are used to parse combined prefixes like `1.1K crore` (en-IN)
|
||||||
|
let known_tmag: u8 = if t.len() == 1 {
|
||||||
|
match t.as_str() {
|
||||||
|
"K" | "k" => 3,
|
||||||
|
"M" => 6,
|
||||||
|
// 'm' means 10^3 in Catalan, 'B' means 10^3 in Turkish
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// K/M/B
|
||||||
|
if known_tmag > 0 {
|
||||||
|
mag_remaining = mag_remaining
|
||||||
|
.checked_sub(known_tmag)
|
||||||
|
.expect("known magnitude incorrect");
|
||||||
|
} else {
|
||||||
|
insert_token(t.to_owned(), mag_remaining);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert collected data into dictionary
|
||||||
|
dict_entry.number_tokens = found_tokens
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(k, v)| v.map(|v| (k, v)))
|
||||||
|
.collect();
|
||||||
|
dict_entry.comma_decimal = comma_decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
util::write_dict(project_root, &dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mag(n: u64) -> u8 {
|
||||||
|
(n as f64).log10().floor() as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
YouTube channel videos response
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Channel {
|
||||||
|
contents: Contents,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Contents {
|
||||||
|
two_column_browse_results_renderer: TabsRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct TabsRenderer {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
tabs: Vec<TabRendererWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct TabRendererWrap {
|
||||||
|
tab_renderer: TabRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct TabRenderer {
|
||||||
|
content: SectionListRendererWrap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SectionListRendererWrap {
|
||||||
|
section_list_renderer: SectionListRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct SectionListRenderer {
|
||||||
|
contents: Vec<ItemSectionRendererWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct ItemSectionRendererWrap {
|
||||||
|
item_section_renderer: ItemSectionRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct ItemSectionRenderer {
|
||||||
|
contents: Vec<GridRendererWrap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GridRendererWrap {
|
||||||
|
grid_renderer: GridRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GridRenderer {
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
items: Vec<VideoListItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct VideoListItem {
|
||||||
|
grid_video_renderer: GridVideoRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct GridVideoRenderer {
|
||||||
|
/// `24,194 views`
|
||||||
|
view_count_text: Text,
|
||||||
|
/// `19K views`
|
||||||
|
short_view_count_text: Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ChannelData {
|
||||||
|
view_counts: Vec<(u64, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_channel(channel_id: &str, lang: Language) -> Result<ChannelData> {
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let body = format!(
|
||||||
|
"{}{}{}{}{}",
|
||||||
|
r##"{"context":{"client":{"clientName":"WEB","clientVersion":"2.20220914.06.00","platform":"DESKTOP","originalUrl":"https://www.youtube.com/","hl":""##,
|
||||||
|
lang,
|
||||||
|
r##"","gl":"US"},"request":{"internalExperimentFlags":[],"useSsl":true},"user":{"lockedSafetyMode":false}},"params":"EgZ2aWRlb3MYASAAMAE%3D","browseId":""##,
|
||||||
|
channel_id,
|
||||||
|
"\"}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post("https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false")
|
||||||
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
|
.body(body)
|
||||||
|
.send().await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
let channel = resp.json::<Channel>().await?;
|
||||||
|
|
||||||
|
Ok(ChannelData {
|
||||||
|
view_counts: channel
|
||||||
|
.contents
|
||||||
|
.two_column_browse_results_renderer
|
||||||
|
.tabs
|
||||||
|
.get(0)
|
||||||
|
.map(|tab| {
|
||||||
|
tab.tab_renderer.content.section_list_renderer.contents[0]
|
||||||
|
.item_section_renderer
|
||||||
|
.contents[0]
|
||||||
|
.grid_renderer
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|itm| {
|
||||||
|
(
|
||||||
|
util::parse_numeric(
|
||||||
|
&itm.grid_video_renderer.view_count_text.simple_text,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
itm.grid_video_renderer
|
||||||
|
.short_view_count_text
|
||||||
|
.simple_text
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test() {
|
||||||
|
let channel = get_channel("UCcdwLMPsaU2ezNSJU1nFoBQ", Language::Az)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
dbg!(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test2() {
|
||||||
|
write_samples_to_dict(Path::new(
|
||||||
|
"/home/thetadev/Documents/Programmieren/Rust/rustypipe",
|
||||||
|
));
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ enum DateCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect 'Playlist updated' dates in every supported language
|
/// Collect 'Playlist updated' dates in every supported language
|
||||||
/// and write them to `testfiles/date/playlist_samples.json`.
|
/// and write them to `testfiles/dict/playlist_samples.json`.
|
||||||
///
|
///
|
||||||
/// YouTube's API outputs the update date of playlists only in a
|
/// YouTube's API outputs the update date of playlists only in a
|
||||||
/// textual format (e.g. *Last updated on Jan 3, 2020*), which varies
|
/// textual format (e.g. *Last updated on Jan 3, 2020*), which varies
|
||||||
|
@ -55,13 +55,15 @@ enum DateCase {
|
||||||
/// - one playlist updated yesterday
|
/// - one playlist updated yesterday
|
||||||
/// - one playlist updated 2-7 days ago
|
/// - one playlist updated 2-7 days ago
|
||||||
/// - one playlist from every month. Note that there should not
|
/// - one playlist from every month. Note that there should not
|
||||||
/// be any dates which include the same number twice (e.g. 01.01.2020).
|
/// be any dates which include the same number twice (e.g. 01.01.2020).
|
||||||
|
///
|
||||||
|
/// **IMPORTANT:**
|
||||||
///
|
///
|
||||||
/// Because the relative dates change with time, the first three playlists
|
/// Because the relative dates change with time, the first three playlists
|
||||||
/// should be checked and eventually changed before running the program.
|
/// have to checked and eventually changed before running the program.
|
||||||
pub async fn collect_dates(project_root: &Path, concurrency: usize) {
|
pub async fn collect_dates(project_root: &Path, concurrency: usize) {
|
||||||
let mut json_path = project_root.to_path_buf();
|
let mut json_path = project_root.to_path_buf();
|
||||||
json_path.push("testfiles/date/playlist_samples.json");
|
json_path.push("testfiles/dict/playlist_samples.json");
|
||||||
|
|
||||||
// These are the sample playlists
|
// These are the sample playlists
|
||||||
let cases = [
|
let cases = [
|
||||||
|
@ -115,7 +117,7 @@ pub async fn collect_dates(project_root: &Path, concurrency: usize) {
|
||||||
/// parsed automatically and require manual work.
|
/// parsed automatically and require manual work.
|
||||||
pub fn write_samples_to_dict(project_root: &Path) {
|
pub fn write_samples_to_dict(project_root: &Path) {
|
||||||
let mut json_path = project_root.to_path_buf();
|
let mut json_path = project_root.to_path_buf();
|
||||||
json_path.push("testfiles/date/playlist_samples.json");
|
json_path.push("testfiles/dict/playlist_samples.json");
|
||||||
|
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
let collected_dates: CollectedDates =
|
let collected_dates: CollectedDates =
|
||||||
|
|
|
@ -18,6 +18,7 @@ pub async fn download_testfiles(project_root: &Path) {
|
||||||
playlist(&testfiles),
|
playlist(&testfiles),
|
||||||
video_details(&testfiles),
|
video_details(&testfiles),
|
||||||
comments_top(&testfiles),
|
comments_top(&testfiles),
|
||||||
|
channel_videos(&testfiles),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ async fn video_details(testfiles: &Path) {
|
||||||
async fn comments_top(testfiles: &Path) {
|
async fn comments_top(testfiles: &Path) {
|
||||||
let mut json_path = testfiles.to_path_buf();
|
let mut json_path = testfiles.to_path_buf();
|
||||||
json_path.push("video_details");
|
json_path.push("video_details");
|
||||||
json_path.push(format!("comments_top.json"));
|
json_path.push("comments_top.json");
|
||||||
if json_path.exists() {
|
if json_path.exists() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -159,3 +160,22 @@ async fn comments_top(testfiles: &Path) {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn channel_videos(testfiles: &Path) {
|
||||||
|
for (name, id) in [
|
||||||
|
("base", "UC2DjFE7Xf11URZqWBigcVOQ"),
|
||||||
|
("music", "UC_vmjW5e1xEHhYjY2a0kK1A"), // YouTube Music channels have no videos
|
||||||
|
("shorts", "UCh8gHdtzO2tXd593_bjErWg"), // shorts and livestreams are rendered differently
|
||||||
|
("live", "UChs0pSaEoNLV4mevBFGaoKA"),
|
||||||
|
] {
|
||||||
|
let mut json_path = testfiles.to_path_buf();
|
||||||
|
json_path.push("channel");
|
||||||
|
json_path.push(format!("channel_videos_{}.json", name));
|
||||||
|
if json_path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rp = rp_testfile(&json_path);
|
||||||
|
rp.query().channel_videos(id).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,17 +34,47 @@ pub fn generate_dictionary(project_root: &Path) {
|
||||||
let dict = util::read_dict(project_root);
|
let dict = util::read_dict(project_root);
|
||||||
|
|
||||||
let code_head = r#"// This file is automatically generated. DO NOT EDIT.
|
let code_head = r#"// This file is automatically generated. DO NOT EDIT.
|
||||||
|
// See codegen/gen_dictionary.rs for the generation code.
|
||||||
use crate::{
|
use crate::{
|
||||||
model::Language,
|
model::Language,
|
||||||
timeago::{DateCmp, TaToken, TimeUnit},
|
timeago::{DateCmp, TaToken, TimeUnit},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The dictionary contains the information required to parse dates and numbers
|
||||||
|
/// in all supported languages.
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
|
/// Should the language be parsed by character instead of by word?
|
||||||
|
/// (e.g. Chinese/Japanese)
|
||||||
pub by_char: bool,
|
pub by_char: bool,
|
||||||
|
/// Tokens for parsing timeago strings.
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> \[Quantity\] Identifier
|
||||||
|
///
|
||||||
|
/// Identifiers: `Y`(ear), `M`(month), `W`(eek), `D`(ay),
|
||||||
|
/// `h`(our), `m`(inute), `s`(econd)
|
||||||
pub timeago_tokens: phf::Map<&'static str, TaToken>,
|
pub timeago_tokens: phf::Map<&'static str, TaToken>,
|
||||||
|
/// Order in which to parse numeric date components. Formatted as
|
||||||
|
/// a string of date identifiers (Y, M, D).
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// - 03.01.2020 => `"DMY"`
|
||||||
|
/// - Jan 3, 2020 => `"DY"`
|
||||||
pub date_order: &'static [DateCmp],
|
pub date_order: &'static [DateCmp],
|
||||||
|
/// Tokens for parsing month names.
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> Month number (starting from 1)
|
||||||
pub months: phf::Map<&'static str, u8>,
|
pub months: phf::Map<&'static str, u8>,
|
||||||
|
/// Tokens for parsing date strings with no digits (e.g. Today, Tomorrow)
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> \[Quantity\] Identifier
|
||||||
pub timeago_nd_tokens: phf::Map<&'static str, TaToken>,
|
pub timeago_nd_tokens: phf::Map<&'static str, TaToken>,
|
||||||
|
/// Are commas (instead of points) used as decimal separators?
|
||||||
|
pub comma_decimal: bool,
|
||||||
|
/// Tokens for parsing decimal prefixes (K, M, B, ...)
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> decimal power
|
||||||
|
pub number_tokens: phf::Map<&'static str, u8>,
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
@ -100,12 +130,19 @@ pub fn entry(lang: Language) -> Entry {
|
||||||
});
|
});
|
||||||
date_order = date_order.trim_end_matches([' ', ',']).to_owned() + "]";
|
date_order = date_order.trim_end_matches([' ', ',']).to_owned() + "]";
|
||||||
|
|
||||||
|
// Number tokens
|
||||||
|
let mut number_tokens = phf_codegen::Map::<&str>::new();
|
||||||
|
entry.number_tokens.iter().for_each(|(txt, mag)| {
|
||||||
|
number_tokens.entry(txt, &mag.to_string());
|
||||||
|
});
|
||||||
|
|
||||||
let code_ta_tokens = &ta_tokens.build().to_string().replace('\n', "\n ");
|
let code_ta_tokens = &ta_tokens.build().to_string().replace('\n', "\n ");
|
||||||
let code_ta_nd_tokens = &ta_nd_tokens.build().to_string().replace('\n', "\n ");
|
let code_ta_nd_tokens = &ta_nd_tokens.build().to_string().replace('\n', "\n ");
|
||||||
let code_months = &months.build().to_string().replace('\n', "\n ");
|
let code_months = &months.build().to_string().replace('\n', "\n ");
|
||||||
|
let code_number_tokens = &number_tokens.build().to_string().replace('\n', "\n ");
|
||||||
|
|
||||||
let _ = write!(code_timeago_tokens, "{} => Entry {{\n by_char: {:?},\n timeago_tokens: {},\n date_order: {},\n months: {},\n timeago_nd_tokens: {},\n }},\n ",
|
let _ = write!(code_timeago_tokens, "{} => Entry {{\n by_char: {:?},\n timeago_tokens: {},\n date_order: {},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n }},\n ",
|
||||||
selector, entry.by_char, code_ta_tokens, date_order, code_months, code_ta_nd_tokens);
|
selector, entry.by_char, code_ta_tokens, date_order, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n";
|
code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n";
|
||||||
|
|
|
@ -8,6 +8,8 @@ use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::VecSkipError;
|
use serde_with::VecSkipError;
|
||||||
|
|
||||||
|
use crate::util::Text;
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -135,12 +137,6 @@ struct LanguageCountryCommand {
|
||||||
hl: String,
|
hl: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct Text {
|
|
||||||
simple_text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_locales(project_root: &Path) {
|
pub async fn generate_locales(project_root: &Path) {
|
||||||
let (languages, countries) = get_locales().await;
|
let (languages, countries) = get_locales().await;
|
||||||
|
|
||||||
|
@ -284,7 +280,7 @@ pub enum Country {
|
||||||
async fn get_locales() -> (BTreeMap<String, String>, BTreeMap<String, String>) {
|
async fn get_locales() -> (BTreeMap<String, String>, BTreeMap<String, String>) {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let resp = client
|
let resp = client
|
||||||
.post("https://www.youtube.com/youtubei/v1/account/account_menu?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8")
|
.post("https://www.youtube.com/youtubei/v1/account/account_menu?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false")
|
||||||
.header(header::CONTENT_TYPE, "application/json")
|
.header(header::CONTENT_TYPE, "application/json")
|
||||||
.body(
|
.body(
|
||||||
r##"{"context":{"client":{"clientName":"WEB","clientVersion":"2.20220914.06.00","platform":"DESKTOP","originalUrl":"https://www.youtube.com/","hl":"en","gl":"US"},"request":{"internalExperimentFlags":[],"useSsl":true},"user":{"lockedSafetyMode":false}}}"##
|
r##"{"context":{"client":{"clientName":"WEB","clientVersion":"2.20220914.06.00","platform":"DESKTOP","originalUrl":"https://www.youtube.com/","hl":"en","gl":"US"},"request":{"internalExperimentFlags":[],"useSsl":true},"user":{"lockedSafetyMode":false}}}"##
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod collect_large_numbers;
|
||||||
mod collect_playlist_dates;
|
mod collect_playlist_dates;
|
||||||
mod download_testfiles;
|
mod download_testfiles;
|
||||||
mod gen_dictionary;
|
mod gen_dictionary;
|
||||||
|
@ -21,7 +22,9 @@ struct Cli {
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
CollectPlaylistDates,
|
CollectPlaylistDates,
|
||||||
WritePlaylistDates,
|
CollectLargeNumbers,
|
||||||
|
ParsePlaylistDates,
|
||||||
|
ParseLargeNumbers,
|
||||||
GenLocales,
|
GenLocales,
|
||||||
GenDict,
|
GenDict,
|
||||||
DownloadTestfiles,
|
DownloadTestfiles,
|
||||||
|
@ -36,8 +39,14 @@ async fn main() {
|
||||||
Commands::CollectPlaylistDates => {
|
Commands::CollectPlaylistDates => {
|
||||||
collect_playlist_dates::collect_dates(&cli.project_root, cli.concurrency).await;
|
collect_playlist_dates::collect_dates(&cli.project_root, cli.concurrency).await;
|
||||||
}
|
}
|
||||||
Commands::WritePlaylistDates => {
|
Commands::CollectLargeNumbers => {
|
||||||
collect_playlist_dates::write_samples_to_dict(&cli.project_root);
|
collect_large_numbers::collect_large_numbers(&cli.project_root, cli.concurrency).await;
|
||||||
|
}
|
||||||
|
Commands::ParsePlaylistDates => {
|
||||||
|
collect_playlist_dates::write_samples_to_dict(&cli.project_root)
|
||||||
|
}
|
||||||
|
Commands::ParseLargeNumbers => {
|
||||||
|
collect_large_numbers::write_samples_to_dict(&cli.project_root)
|
||||||
}
|
}
|
||||||
Commands::GenLocales => {
|
Commands::GenLocales => {
|
||||||
gen_locales::generate_locales(&cli.project_root).await;
|
gen_locales::generate_locales(&cli.project_root).await;
|
||||||
|
|
|
@ -3,19 +3,53 @@ use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path, str::FromS
|
||||||
use rustypipe::model::Language;
|
use rustypipe::model::Language;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const DICT_PATH: &str = "testfiles/date/dictionary.json";
|
const DICT_PATH: &str = "testfiles/dict/dictionary.json";
|
||||||
|
|
||||||
type Dictionary = BTreeMap<Language, DictEntry>;
|
type Dictionary = BTreeMap<Language, DictEntry>;
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct DictEntry {
|
pub struct DictEntry {
|
||||||
|
/// List of languages that should be treated equally (e.g. EnUs/EnGb/EnIn)
|
||||||
pub equivalent: Vec<Language>,
|
pub equivalent: Vec<Language>,
|
||||||
|
/// Should the language be parsed by character instead of by word?
|
||||||
|
/// (e.g. Chinese/Japanese)
|
||||||
pub by_char: bool,
|
pub by_char: bool,
|
||||||
|
/// Tokens for parsing timeago strings.
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> \[Quantity\] Identifier
|
||||||
|
///
|
||||||
|
/// Identifiers: `Y`(ear), `M`(month), `W`(eek), `D`(ay),
|
||||||
|
/// `h`(our), `m`(inute), `s`(econd)
|
||||||
pub timeago_tokens: BTreeMap<String, String>,
|
pub timeago_tokens: BTreeMap<String, String>,
|
||||||
|
/// Order in which to parse numeric date components. Formatted as
|
||||||
|
/// a string of date identifiers (Y, M, D).
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// - 03.01.2020 => `"DMY"`
|
||||||
|
/// - Jan 3, 2020 => `"DY"`
|
||||||
pub date_order: String,
|
pub date_order: String,
|
||||||
|
/// Tokens for parsing month names.
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> Month number (starting from 1)
|
||||||
pub months: BTreeMap<String, u8>,
|
pub months: BTreeMap<String, u8>,
|
||||||
|
/// Tokens for parsing date strings with no digits (e.g. Today, Tomorrow)
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> \[Quantity\] Identifier
|
||||||
pub timeago_nd_tokens: BTreeMap<String, String>,
|
pub timeago_nd_tokens: BTreeMap<String, String>,
|
||||||
|
/// Are commas (instead of points) used as decimal separators?
|
||||||
|
pub comma_decimal: bool,
|
||||||
|
/// Tokens for parsing decimal prefixes (K, M, B, ...)
|
||||||
|
///
|
||||||
|
/// Format: Parsed token -> decimal power
|
||||||
|
pub number_tokens: BTreeMap<String, u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Text {
|
||||||
|
pub simple_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_dict(project_root: &Path) -> Dictionary {
|
pub fn read_dict(project_root: &Path) -> Dictionary {
|
||||||
|
@ -48,6 +82,27 @@ pub fn filter_datestr(string: &str) -> String {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn filter_largenumstr(string: &str) -> String {
|
||||||
|
string
|
||||||
|
.chars()
|
||||||
|
.filter(|c| !matches!(c, '\u{200b}' | '.' | ',') && !c.is_ascii_digit())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a string after removing all non-numeric characters
|
||||||
|
pub fn parse_numeric<F>(string: &str) -> Result<F, F::Err>
|
||||||
|
where
|
||||||
|
F: FromStr,
|
||||||
|
{
|
||||||
|
let mut buf = String::new();
|
||||||
|
for c in string.chars() {
|
||||||
|
if c.is_ascii_digit() {
|
||||||
|
buf.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.parse()
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse all numbers occurring in a string and reurn them as a vec
|
/// Parse all numbers occurring in a string and reurn them as a vec
|
||||||
pub fn parse_numeric_vec<F>(string: &str) -> Vec<F>
|
pub fn parse_numeric_vec<F>(string: &str) -> Vec<F>
|
||||||
where
|
where
|
||||||
|
|
|
@ -52,3 +52,13 @@ Sep PL1J-6JOckZtHVs0JhBW_qfsW-dtXuM0mQ 16.09.2018
|
||||||
Oct PL1J-6JOckZtE4g-XgZkL_N0kkoKui5Eys 31.10.2014
|
Oct PL1J-6JOckZtE4g-XgZkL_N0kkoKui5Eys 31.10.2014
|
||||||
Nov PL1J-6JOckZtEzjMUEyPyPpG836pjeIapw 03.11.2016
|
Nov PL1J-6JOckZtEzjMUEyPyPpG836pjeIapw 03.11.2016
|
||||||
Dec PL1J-6JOckZtHo91uApeb10Qlf2XhkfM-9 24.12.2021
|
Dec PL1J-6JOckZtHo91uApeb10Qlf2XhkfM-9 24.12.2021
|
||||||
|
|
||||||
|
# Channels
|
||||||
|
10e8: 225M UCq-Fj5jknLsUf-MWSy4_brA
|
||||||
|
10e7: 52M UC0C-w0YjGpqDXGB8IHb662A
|
||||||
|
10e6: 1.7M UC6mIxFTvXkWQVEHPsEdflzQ
|
||||||
|
10e5: 125K UCD0y51PJfvkZNe3y3FR5riw
|
||||||
|
10e4: 27K UCNcN0dW43zE0Om3278fjY8A
|
||||||
|
10e3: 5K UC0QEucPrn0-Ddi3JBTcs5Kw
|
||||||
|
10e2: 388 UCllyEQfcoiPN68zHv6mGHDQ
|
||||||
|
10e1: 37 UCGiJh0NZ52wRhYKYnuZI08Q
|
||||||
|
|
82
src/client/channel.rs
Normal file
82
src/client/channel.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{model::ChannelVideos, serializer::MapResult};
|
||||||
|
|
||||||
|
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct QChannel {
|
||||||
|
context: YTContext,
|
||||||
|
browse_id: String,
|
||||||
|
params: Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
enum Params {
|
||||||
|
#[serde(rename = "EgZ2aWRlb3PyBgQKAjoA")]
|
||||||
|
VideosLatest,
|
||||||
|
#[serde(rename = "EgZ2aWRlb3MYAiAAMAE%3D")]
|
||||||
|
VideosOldest,
|
||||||
|
#[serde(rename = "EgZ2aWRlb3MYASAAMAE%3D")]
|
||||||
|
VideosPopular,
|
||||||
|
#[serde(rename = "EglwbGF5bGlzdHMgAQ%3D%3D")]
|
||||||
|
Playlists,
|
||||||
|
#[serde(rename = "EgVhYm91dPIGBAoCEgA%3D")]
|
||||||
|
Info,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustyPipeQuery {
|
||||||
|
pub async fn channel_videos(&self, channel_id: &str) -> Result<ChannelVideos> {
|
||||||
|
let context = self.get_context(ClientType::Desktop, true).await;
|
||||||
|
let request_body = QChannel {
|
||||||
|
context,
|
||||||
|
browse_id: channel_id.to_owned(),
|
||||||
|
params: Params::VideosLatest,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.execute_request::<response::Channel, _, _>(
|
||||||
|
ClientType::Desktop,
|
||||||
|
"channel_videos",
|
||||||
|
channel_id,
|
||||||
|
Method::POST,
|
||||||
|
"browse",
|
||||||
|
&request_body,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MapResponse<ChannelVideos> for response::Channel {
|
||||||
|
fn map_response(
|
||||||
|
self,
|
||||||
|
id: &str,
|
||||||
|
_lang: crate::model::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::Deobfuscator>,
|
||||||
|
) -> Result<MapResult<ChannelVideos>> {
|
||||||
|
let warnings = Vec::new();
|
||||||
|
// dbg!(&self);
|
||||||
|
let header = self.header.c4_tabbed_header_renderer;
|
||||||
|
|
||||||
|
if header.channel_id != id {
|
||||||
|
bail!(
|
||||||
|
"got wrong channel id {}, expected {}",
|
||||||
|
header.channel_id,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(MapResult {
|
||||||
|
c: ChannelVideos {
|
||||||
|
id: header.channel_id,
|
||||||
|
name: header.title,
|
||||||
|
},
|
||||||
|
warnings,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {}
|
|
@ -1,9 +1,9 @@
|
||||||
pub mod pagination;
|
mod channel;
|
||||||
pub mod player;
|
mod pagination;
|
||||||
pub mod playlist;
|
mod player;
|
||||||
pub mod video_details;
|
mod playlist;
|
||||||
|
|
||||||
mod response;
|
mod response;
|
||||||
|
mod video_details;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
@ -24,7 +24,7 @@ use super::{
|
||||||
ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QPlayer {
|
struct QPlayer {
|
||||||
context: YTContext,
|
context: YTContext,
|
||||||
|
@ -42,13 +42,13 @@ struct QPlayer {
|
||||||
racy_check_ok: bool,
|
racy_check_ok: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QPlaybackContext {
|
struct QPlaybackContext {
|
||||||
content_playback_context: QContentPlaybackContext,
|
content_playback_context: QContentPlaybackContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QContentPlaybackContext {
|
struct QContentPlaybackContext {
|
||||||
/// Signature timestamp extracted from player.js
|
/// Signature timestamp extracted from player.js
|
||||||
|
|
|
@ -13,14 +13,14 @@ use crate::{
|
||||||
|
|
||||||
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QPlaylist {
|
struct QPlaylist {
|
||||||
context: YTContext,
|
context: YTContext,
|
||||||
browse_id: String,
|
browse_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct QPlaylistCont {
|
struct QPlaylistCont {
|
||||||
context: YTContext,
|
context: YTContext,
|
||||||
|
@ -206,14 +206,12 @@ impl MapResponse<Paginator<PlaylistVideo>> for response::PlaylistCont {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_playlist_items(
|
fn map_playlist_items(items: Vec<response::VideoListItem>) -> (Vec<PlaylistVideo>, Option<String>) {
|
||||||
items: Vec<response::VideoListItem<response::playlist::PlaylistVideo>>,
|
|
||||||
) -> (Vec<PlaylistVideo>, Option<String>) {
|
|
||||||
let mut ctoken: Option<String> = None;
|
let mut ctoken: Option<String> = None;
|
||||||
let videos = items
|
let videos = items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|it| match it {
|
.filter_map(|it| match it {
|
||||||
response::VideoListItem::GridVideoRenderer { video } => {
|
response::VideoListItem::PlaylistVideoRenderer(video) => {
|
||||||
match ChannelId::try_from(video.channel) {
|
match ChannelId::try_from(video.channel) {
|
||||||
Ok(channel) => Some(PlaylistVideo {
|
Ok(channel) => Some(PlaylistVideo {
|
||||||
id: video.video_id,
|
id: video.video_id,
|
||||||
|
@ -231,7 +229,7 @@ fn map_playlist_items(
|
||||||
ctoken = Some(continuation_endpoint.continuation_command.token);
|
ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
response::VideoListItem::None => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
(videos, ctoken)
|
(videos, ctoken)
|
||||||
|
|
|
@ -2,13 +2,15 @@ use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::VecSkipError;
|
use serde_with::VecSkipError;
|
||||||
|
|
||||||
use super::TimeOverlay;
|
use super::ChannelBadge;
|
||||||
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
|
use super::Thumbnails;
|
||||||
use crate::serializer::text::Text;
|
use super::{ContentRenderer, ContentsRenderer, VideoListItem};
|
||||||
|
use crate::serializer::{text::Text, MapResult, VecLogError};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
|
pub header: Header,
|
||||||
pub contents: Contents,
|
pub contents: Contents,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +20,8 @@ pub struct Contents {
|
||||||
pub two_column_browse_results_renderer: TabsRenderer,
|
pub two_column_browse_results_renderer: TabsRenderer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// YouTube channel tab view. Contains multiple tabs
|
||||||
|
/// (Home, Videos, Playlists, About...). We can ignore unknown tabs.
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -35,7 +39,14 @@ pub struct TabRendererWrap {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SectionListRendererWrap {
|
pub struct SectionListRendererWrap {
|
||||||
pub section_list_renderer: ContentsRenderer<ItemSectionRendererWrap>,
|
pub section_list_renderer: SectionListRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SectionListRenderer {
|
||||||
|
pub contents: Vec<ItemSectionRendererWrap>,
|
||||||
|
pub target_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
@ -54,22 +65,33 @@ pub struct GridRendererWrap {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct GridRenderer {
|
pub struct GridRenderer {
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecLogError<_>")]
|
||||||
pub items: Vec<VideoListItem<ChannelVideo>>,
|
pub items: MapResult<Vec<VideoListItem>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Header {
|
||||||
|
pub c4_tabbed_header_renderer: HeaderRenderer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ChannelVideo {
|
pub struct HeaderRenderer {
|
||||||
pub video_id: String,
|
pub channel_id: String,
|
||||||
pub thumbnail: Thumbnails,
|
/// Channel name
|
||||||
#[serde_as(as = "Text")]
|
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
/// Approximate subscriber count (e.g. `880K subscribers`), depends on language.
|
||||||
|
///
|
||||||
|
/// `None` if the subscriber count is hidden.
|
||||||
#[serde_as(as = "Option<Text>")]
|
#[serde_as(as = "Option<Text>")]
|
||||||
pub published_time_text: Option<String>,
|
pub subscriber_count_text: Option<String>,
|
||||||
#[serde_as(as = "Text")]
|
pub avatar: Thumbnails,
|
||||||
pub view_count_text: String,
|
#[serde_as(as = "Option<VecSkipError<_>>")]
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
pub badges: Option<Vec<ChannelBadge>>,
|
||||||
pub thumbnail_overlays: Vec<TimeOverlay>,
|
pub banner: Thumbnails,
|
||||||
|
pub mobile_banner: Thumbnails,
|
||||||
|
/// Fullscreen (16:9) channel banner
|
||||||
|
pub tv_banner: Thumbnails,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub use video_details::VideoDetails;
|
||||||
pub use video_details::VideoRecommendations;
|
pub use video_details::VideoRecommendations;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
use serde_with::{json::JsonString, serde_as, DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use crate::serializer::{
|
use crate::serializer::{
|
||||||
ignore_any,
|
ignore_any,
|
||||||
|
@ -40,6 +40,8 @@ pub struct ThumbnailsWrap {
|
||||||
pub thumbnail: Thumbnails,
|
pub thumbnail: Thumbnails,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List of images in different resolutions.
|
||||||
|
/// Not only used for thumbnails, but also for avatars and banners.
|
||||||
#[derive(Default, Clone, Debug, Deserialize)]
|
#[derive(Default, Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Thumbnails {
|
pub struct Thumbnails {
|
||||||
|
@ -54,27 +56,108 @@ pub struct Thumbnail {
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum VideoListItem<T> {
|
pub enum VideoListItem {
|
||||||
#[serde(alias = "playlistVideoRenderer", alias = "compactVideoRenderer")]
|
GridVideoRenderer(GridVideoRenderer),
|
||||||
GridVideoRenderer {
|
CompactVideoRenderer(CompactVideoRenderer),
|
||||||
#[serde(flatten)]
|
PlaylistVideoRenderer(PlaylistVideoRenderer),
|
||||||
video: T,
|
|
||||||
},
|
GridPlaylistRenderer(GridPlaylistRenderer),
|
||||||
|
|
||||||
|
/// Continauation items are located at the end of a list
|
||||||
|
/// and contain the continuation token for progressive loading
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ContinuationItemRenderer {
|
ContinuationItemRenderer {
|
||||||
continuation_endpoint: ContinuationEndpoint,
|
continuation_endpoint: ContinuationEndpoint,
|
||||||
},
|
},
|
||||||
/// No video list item (e.g. ad)
|
/// No video list item (e.g. ad) or unimplemented item
|
||||||
///
|
///
|
||||||
/// Note that there are sometimes playlists among the recommended
|
/// Unimplemented:
|
||||||
/// videos. They are currently ignored.
|
/// - compactPlaylistRenderer (recommended playlists)
|
||||||
|
/// - compactRadioRenderer (recommended mix)
|
||||||
#[serde(other, deserialize_with = "ignore_any")]
|
#[serde(other, deserialize_with = "ignore_any")]
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Video displayed on a channel page
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GridVideoRenderer {
|
||||||
|
pub video_id: String,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde_as(as = "Option<Text>")]
|
||||||
|
pub published_time_text: Option<String>,
|
||||||
|
#[serde_as(as = "Option<Text>")]
|
||||||
|
pub view_count_text: Option<String>,
|
||||||
|
/// Contains video length
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub thumbnail_overlays: Vec<TimeOverlay>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Video displayed in recommendations
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CompactVideoRenderer {
|
||||||
|
pub video_id: String,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(rename = "shortBylineText")]
|
||||||
|
pub channel: TextComponent,
|
||||||
|
pub channel_thumbnail: Thumbnails,
|
||||||
|
/// Channel verification badge
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub owner_badges: Vec<ChannelBadge>,
|
||||||
|
#[serde_as(as = "Option<Text>")]
|
||||||
|
pub length_text: Option<String>,
|
||||||
|
/// (e.g. `11 months ago`)
|
||||||
|
#[serde_as(as = "Option<Text>")]
|
||||||
|
pub published_time_text: Option<String>,
|
||||||
|
#[serde_as(as = "Option<Text>")]
|
||||||
|
pub view_count_text: Option<String>,
|
||||||
|
/// Badges are displayed on the video thumbnail and
|
||||||
|
/// show certain video properties (e.g. active livestream)
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
pub badges: Vec<VideoBadge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Video displayed in a playlist
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PlaylistVideoRenderer {
|
||||||
|
pub video_id: String,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(rename = "shortBylineText")]
|
||||||
|
pub channel: TextComponent,
|
||||||
|
#[serde_as(as = "JsonString")]
|
||||||
|
pub length_seconds: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Playlist displayed on a channel page
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GridPlaylistRenderer {
|
||||||
|
pub playlist_id: String,
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub title: String,
|
||||||
|
pub thumbnail: Thumbnails,
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub published_time_text: String,
|
||||||
|
#[serde_as(as = "Text")]
|
||||||
|
pub video_count_short_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ContinuationItemRenderer {
|
pub struct ContinuationItemRenderer {
|
||||||
|
@ -315,6 +398,10 @@ pub trait IsLive {
|
||||||
fn is_live(&self) -> bool;
|
fn is_live(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait IsShort {
|
||||||
|
fn is_short(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
impl IsLive for Vec<VideoBadge> {
|
impl IsLive for Vec<VideoBadge> {
|
||||||
fn is_live(&self) -> bool {
|
fn is_live(&self) -> bool {
|
||||||
self.iter().any(|badge| {
|
self.iter().any(|badge| {
|
||||||
|
@ -322,3 +409,19 @@ impl IsLive for Vec<VideoBadge> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IsLive for Vec<TimeOverlay> {
|
||||||
|
fn is_live(&self) -> bool {
|
||||||
|
self.iter().any(|overlay| {
|
||||||
|
overlay.thumbnail_overlay_time_status_renderer.style == TimeOverlayStyle::Live
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsShort for Vec<TimeOverlay> {
|
||||||
|
fn is_short(&self) -> bool {
|
||||||
|
self.iter().any(|overlay| {
|
||||||
|
overlay.thumbnail_overlay_time_status_renderer.style == TimeOverlayStyle::Shorts
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
use serde_with::{DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use crate::serializer::text::{Text, TextComponent};
|
use crate::serializer::text::{Text, TextComponent};
|
||||||
use crate::serializer::{MapResult, VecLogError};
|
use crate::serializer::{MapResult, VecLogError};
|
||||||
|
|
||||||
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
|
use super::{ContentRenderer, ContentsRenderer, ThumbnailsWrap, VideoListItem};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -58,21 +58,7 @@ pub struct PlaylistVideoListRenderer {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PlaylistVideoList {
|
pub struct PlaylistVideoList {
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecLogError<_>")]
|
||||||
pub contents: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
|
pub contents: MapResult<Vec<VideoListItem>>,
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PlaylistVideo {
|
|
||||||
pub video_id: String,
|
|
||||||
pub thumbnail: Thumbnails,
|
|
||||||
#[serde_as(as = "Text")]
|
|
||||||
pub title: String,
|
|
||||||
#[serde(rename = "shortBylineText")]
|
|
||||||
pub channel: TextComponent,
|
|
||||||
#[serde_as(as = "JsonString")]
|
|
||||||
pub length_seconds: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
@ -172,5 +158,5 @@ pub struct OnResponseReceivedAction {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppendAction {
|
pub struct AppendAction {
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecLogError<_>")]
|
||||||
pub continuation_items: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
|
pub continuation_items: MapResult<Vec<VideoListItem>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,12 @@ use crate::serializer::text::TextComponents;
|
||||||
use crate::serializer::MapResult;
|
use crate::serializer::MapResult;
|
||||||
use crate::serializer::{
|
use crate::serializer::{
|
||||||
ignore_any,
|
ignore_any,
|
||||||
text::{AccessibilityText, Text, TextComponent},
|
text::{AccessibilityText, Text},
|
||||||
VecLogError,
|
VecLogError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ChannelBadge, ContinuationEndpoint, ContinuationItemRenderer, Icon, Thumbnails, VideoBadge,
|
ContinuationEndpoint, ContinuationItemRenderer, Icon, Thumbnails, VideoListItem, VideoOwner,
|
||||||
VideoListItem, VideoOwner,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -283,37 +282,7 @@ pub struct RecommendationResultsWrap {
|
||||||
pub struct RecommendationResults {
|
pub struct RecommendationResults {
|
||||||
/// Can be `None` for age-restricted videos
|
/// Can be `None` for age-restricted videos
|
||||||
#[serde_as(as = "Option<VecLogError<_>>")]
|
#[serde_as(as = "Option<VecLogError<_>>")]
|
||||||
pub results: Option<MapResult<Vec<VideoListItem<RecommendedVideo>>>>,
|
pub results: Option<MapResult<Vec<VideoListItem>>>,
|
||||||
}
|
|
||||||
|
|
||||||
/// Video recommendation item
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct RecommendedVideo {
|
|
||||||
pub video_id: String,
|
|
||||||
pub thumbnail: Thumbnails,
|
|
||||||
#[serde_as(as = "Text")]
|
|
||||||
pub title: String,
|
|
||||||
#[serde(rename = "shortBylineText")]
|
|
||||||
pub channel: TextComponent,
|
|
||||||
pub channel_thumbnail: Thumbnails,
|
|
||||||
/// Channel verification badge
|
|
||||||
#[serde(default)]
|
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
|
||||||
pub owner_badges: Vec<ChannelBadge>,
|
|
||||||
#[serde_as(as = "Option<Text>")]
|
|
||||||
pub length_text: Option<String>,
|
|
||||||
/// (e.g. `11 months ago`)
|
|
||||||
#[serde_as(as = "Option<Text>")]
|
|
||||||
pub published_time_text: Option<String>,
|
|
||||||
#[serde_as(as = "Option<Text>")]
|
|
||||||
pub view_count_text: Option<String>,
|
|
||||||
/// Badges are displayed on the video thumbnail and
|
|
||||||
/// show certain video properties (e.g. active livestream)
|
|
||||||
#[serde(default)]
|
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
|
||||||
pub badges: Vec<VideoBadge>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The engagement panels are displayed below the video and contain chapter markers
|
/// The engagement panels are displayed below the video and contain chapter markers
|
||||||
|
@ -468,7 +437,7 @@ pub struct RecommendationsContItem {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppendRecommendations {
|
pub struct AppendRecommendations {
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecLogError<_>")]
|
||||||
pub continuation_items: MapResult<Vec<VideoListItem<RecommendedVideo>>>,
|
pub continuation_items: MapResult<Vec<VideoListItem>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -18,7 +18,7 @@ use super::{
|
||||||
ClientType, MapResponse, RustyPipeQuery, YTContext,
|
ClientType, MapResponse, RustyPipeQuery, YTContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct QVideo {
|
struct QVideo {
|
||||||
context: YTContext,
|
context: YTContext,
|
||||||
/// YouTube video ID
|
/// YouTube video ID
|
||||||
|
@ -29,7 +29,7 @@ struct QVideo {
|
||||||
racy_check_ok: bool,
|
racy_check_ok: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct QVideoCont {
|
struct QVideoCont {
|
||||||
context: YTContext,
|
context: YTContext,
|
||||||
continuation: String,
|
continuation: String,
|
||||||
|
@ -407,7 +407,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_recommendations(
|
fn map_recommendations(
|
||||||
r: MapResult<Vec<response::VideoListItem<response::video_details::RecommendedVideo>>>,
|
r: MapResult<Vec<response::VideoListItem>>,
|
||||||
lang: Language,
|
lang: Language,
|
||||||
) -> MapResult<Paginator<RecommendedVideo>> {
|
) -> MapResult<Paginator<RecommendedVideo>> {
|
||||||
let mut warnings = r.warnings;
|
let mut warnings = r.warnings;
|
||||||
|
@ -416,7 +416,7 @@ fn map_recommendations(
|
||||||
let items =
|
let items =
|
||||||
r.c.into_iter()
|
r.c.into_iter()
|
||||||
.filter_map(|item| match item {
|
.filter_map(|item| match item {
|
||||||
response::VideoListItem::GridVideoRenderer { video } => {
|
response::VideoListItem::CompactVideoRenderer(video) => {
|
||||||
match ChannelId::try_from(video.channel) {
|
match ChannelId::try_from(video.channel) {
|
||||||
Ok(channel) => Some(RecommendedVideo {
|
Ok(channel) => Some(RecommendedVideo {
|
||||||
id: video.video_id,
|
id: video.video_id,
|
||||||
|
@ -454,7 +454,7 @@ fn map_recommendations(
|
||||||
ctoken = Some(continuation_endpoint.continuation_command.token);
|
ctoken = Some(continuation_endpoint.continuation_command.token);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
response::VideoListItem::None => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
1021
src/dictionary.rs
1021
src/dictionary.rs
File diff suppressed because it is too large
Load diff
|
@ -23,6 +23,7 @@ pub trait FileFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct VideoPlayer {
|
pub struct VideoPlayer {
|
||||||
pub details: VideoPlayerDetails,
|
pub details: VideoPlayerDetails,
|
||||||
pub video_streams: Vec<VideoStream>,
|
pub video_streams: Vec<VideoStream>,
|
||||||
|
@ -33,6 +34,7 @@ pub struct VideoPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct VideoPlayerDetails {
|
pub struct VideoPlayerDetails {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
@ -49,6 +51,7 @@ pub struct VideoPlayerDetails {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct VideoStream {
|
pub struct VideoStream {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub itag: u32,
|
pub itag: u32,
|
||||||
|
@ -69,6 +72,7 @@ pub struct VideoStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct AudioStream {
|
pub struct AudioStream {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub itag: u32,
|
pub itag: u32,
|
||||||
|
@ -128,6 +132,7 @@ pub enum VideoFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct AudioTrack {
|
pub struct AudioTrack {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub lang: Option<String>,
|
pub lang: Option<String>,
|
||||||
|
@ -163,6 +168,7 @@ impl FileFormat for AudioFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Thumbnail {
|
pub struct Thumbnail {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
|
@ -170,6 +176,7 @@ pub struct Thumbnail {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Subtitle {
|
pub struct Subtitle {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
@ -182,6 +189,7 @@ pub struct Subtitle {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -195,6 +203,7 @@ pub struct Playlist {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct PlaylistVideo {
|
pub struct PlaylistVideo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
@ -204,6 +213,7 @@ pub struct PlaylistVideo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct ChannelId {
|
pub struct ChannelId {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -214,6 +224,7 @@ pub struct ChannelId {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct VideoDetails {
|
pub struct VideoDetails {
|
||||||
/// Unique YouTube video ID
|
/// Unique YouTube video ID
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -260,6 +271,7 @@ pub struct VideoDetails {
|
||||||
/// Videos can consist of different chapters, which YouTube shows
|
/// Videos can consist of different chapters, which YouTube shows
|
||||||
/// on the seek bar and below the description text.
|
/// on the seek bar and below the description text.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Chapter {
|
pub struct Chapter {
|
||||||
/// Chapter title
|
/// Chapter title
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
@ -274,6 +286,7 @@ pub struct Chapter {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct RecommendedVideo {
|
pub struct RecommendedVideo {
|
||||||
/// Unique YouTube video ID
|
/// Unique YouTube video ID
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -297,13 +310,14 @@ pub struct RecommendedVideo {
|
||||||
pub publish_date_txt: Option<String>,
|
pub publish_date_txt: Option<String>,
|
||||||
/// View count
|
/// View count
|
||||||
///
|
///
|
||||||
/// Is `None` if it could not be parsed
|
/// `None` if it could not be extracted.
|
||||||
pub view_count: Option<u64>,
|
pub view_count: Option<u64>,
|
||||||
/// Is the video an active livestream?
|
/// Is the video an active livestream?
|
||||||
pub is_live: bool,
|
pub is_live: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
/// Unique YouTube channel ID
|
/// Unique YouTube channel ID
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -327,6 +341,7 @@ pub struct Channel {
|
||||||
|
|
||||||
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum Verification {
|
pub enum Verification {
|
||||||
#[default]
|
#[default]
|
||||||
/// Unverified channel (default)
|
/// Unverified channel (default)
|
||||||
|
@ -343,8 +358,8 @@ impl Verification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: impl popularity comparison
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
/// Unique YouTube Comment-ID (e.g. `UgynScMrsqGSL8qvePl4AaABAg`)
|
/// Unique YouTube Comment-ID (e.g. `UgynScMrsqGSL8qvePl4AaABAg`)
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -373,3 +388,55 @@ pub struct Comment {
|
||||||
/// Has the channel owner marked the comment with a ❤️ heart ?
|
/// Has the channel owner marked the comment with a ❤️ heart ?
|
||||||
pub hearted: bool,
|
pub hearted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#CHANNEL
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct ChannelVideos {
|
||||||
|
/// Unique YouTube Channel-ID (e.g. `UC-lHJZR3Gqxm24_Vd_AJ5Yw`)
|
||||||
|
pub id: String,
|
||||||
|
/// Channel name
|
||||||
|
pub name: String,
|
||||||
|
/*
|
||||||
|
/// Channel subscriber count
|
||||||
|
///
|
||||||
|
/// `None` if the subscriber count was hidden by the owner
|
||||||
|
/// or could not be parsed.
|
||||||
|
pub subscriber_count: Option<u64>,
|
||||||
|
pub videos: Paginator<ChannelVideo>,
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct ChannelVideo {
|
||||||
|
/// Unique YouTube video ID
|
||||||
|
pub id: String,
|
||||||
|
/// Video title
|
||||||
|
pub title: String,
|
||||||
|
/// Video length in seconds.
|
||||||
|
///
|
||||||
|
/// Is `None` for livestreams.
|
||||||
|
pub length: Option<u32>,
|
||||||
|
/// Video thumbnail
|
||||||
|
pub thumbnail: Vec<Thumbnail>,
|
||||||
|
/// Video publishing date.
|
||||||
|
///
|
||||||
|
/// `None` if the date could not be parsed.
|
||||||
|
pub publish_date: Option<DateTime<Local>>,
|
||||||
|
/// Textual video publish date (e.g. `11 months ago`, depends on language)
|
||||||
|
///
|
||||||
|
/// Is `None` for livestreams.
|
||||||
|
pub publish_date_txt: Option<String>,
|
||||||
|
/// View count
|
||||||
|
///
|
||||||
|
/// `None` if it could not be extracted.
|
||||||
|
pub view_count: Option<u64>,
|
||||||
|
/// Is the video an active livestream?
|
||||||
|
pub is_live: bool,
|
||||||
|
/// Is the video a YouTube Short video (vertical and <60s)?
|
||||||
|
pub is_short: bool,
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||||
/// in pages from the YouTube API (e.g. playlist items,
|
/// in pages from the YouTube API (e.g. playlist items,
|
||||||
/// video recommendations or comments).
|
/// video recommendations or comments).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct Paginator<T> {
|
pub struct Paginator<T> {
|
||||||
/// Total number of items if finite and known.
|
/// Total number of items if finite and known.
|
||||||
///
|
///
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub struct RichText(pub Vec<TextComponent>);
|
pub struct RichText(pub Vec<TextComponent>);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum TextComponent {
|
pub enum TextComponent {
|
||||||
/// Plain text
|
/// Plain text
|
||||||
Text(String),
|
Text(String),
|
||||||
|
@ -51,7 +53,7 @@ pub trait ToHtml {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextComponent {
|
impl TextComponent {
|
||||||
pub fn get_text<'a>(&'a self) -> &'a str {
|
pub fn get_text(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
TextComponent::Text(text) => text,
|
TextComponent::Text(text) => text,
|
||||||
TextComponent::Web { text, .. } => text,
|
TextComponent::Web { text, .. } => text,
|
||||||
|
|
|
@ -247,7 +247,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_testfile() {
|
fn t_testfile() {
|
||||||
let json_path = Path::new("testfiles/date/timeago_samples.json");
|
let json_path = Path::new("testfiles/dict/timeago_samples.json");
|
||||||
|
|
||||||
let expect = [
|
let expect = [
|
||||||
TimeAgo {
|
TimeAgo {
|
||||||
|
@ -430,7 +430,7 @@ mod tests {
|
||||||
cases: BTreeMap<String, u8>,
|
cases: BTreeMap<String, u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let json_path = Path::new("testfiles/date/timeago_table.json");
|
let json_path = Path::new("testfiles/dict/timeago_table.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
let timeago_table: TimeagoTable =
|
let timeago_table: TimeagoTable =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
|
@ -477,7 +477,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn t_parse_date_samples() {
|
fn t_parse_date_samples() {
|
||||||
let json_path = Path::new("testfiles/date/playlist_samples.json");
|
let json_path = Path::new("testfiles/dict/playlist_samples.json");
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
let date_samples: BTreeMap<Language, BTreeMap<String, String>> =
|
let date_samples: BTreeMap<Language, BTreeMap<String, String>> =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
|
|
12030
testfiles/channel/channel_videos_base.json
Normal file
12030
testfiles/channel/channel_videos_base.json
Normal file
File diff suppressed because it is too large
Load diff
8570
testfiles/channel/channel_videos_live.json
Normal file
8570
testfiles/channel/channel_videos_live.json
Normal file
File diff suppressed because it is too large
Load diff
4802
testfiles/channel/channel_videos_music.json
Normal file
4802
testfiles/channel/channel_videos_music.json
Normal file
File diff suppressed because it is too large
Load diff
13888
testfiles/channel/channel_videos_shorts.json
Normal file
13888
testfiles/channel/channel_videos_shorts.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -35,6 +35,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"gister": "1D",
|
"gister": "1D",
|
||||||
"vandag": "0D"
|
"vandag": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"m": 6,
|
||||||
|
"mjd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"am": {
|
"am": {
|
||||||
|
@ -74,6 +79,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ትላንት": "1D",
|
"ትላንት": "1D",
|
||||||
"ዛሬ": "0D"
|
"ዛሬ": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ሚ": 6,
|
||||||
|
"ሺ": 3,
|
||||||
|
"ቢ": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ar": {
|
"ar": {
|
||||||
|
@ -110,6 +121,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"اليوم": "0D",
|
"اليوم": "0D",
|
||||||
"بالأمس": "1D"
|
"بالأمس": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ألف": 3,
|
||||||
|
"مليار": 9,
|
||||||
|
"مليون": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"as": {
|
"as": {
|
||||||
|
@ -129,6 +146,15 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"আজি": "0D",
|
"আজি": "0D",
|
||||||
"কালি": "1D"
|
"কালি": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"কোঃটা": 9,
|
||||||
|
"নিঃটা": 6,
|
||||||
|
"নিযুতটা": 6,
|
||||||
|
"লাখটা": 5,
|
||||||
|
"শঃ": 9,
|
||||||
|
"হাজাৰটা": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"az": {
|
"az": {
|
||||||
|
@ -161,6 +187,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"bugün": "0D",
|
"bugün": "0D",
|
||||||
"dünən": "1D"
|
"dünən": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mln": 6,
|
||||||
|
"mlrd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"be": {
|
"be": {
|
||||||
|
@ -210,6 +241,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"сёння": "0D",
|
"сёння": "0D",
|
||||||
"ўчора": "1D"
|
"ўчора": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"млн": 6,
|
||||||
|
"млрд": 9,
|
||||||
|
"тыс": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bg": {
|
"bg": {
|
||||||
|
@ -236,6 +273,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"вчера": "1D",
|
"вчера": "1D",
|
||||||
"днес": "0D"
|
"днес": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"млн": 6,
|
||||||
|
"млрд": 9,
|
||||||
|
"хил": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bn": {
|
"bn": {
|
||||||
|
@ -268,6 +311,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"আজ": "0D",
|
"আজ": "0D",
|
||||||
"গতকাল": "1D"
|
"গতকাল": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"লাটি": 5,
|
||||||
|
"শত": 9,
|
||||||
|
"হাটি": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bs": {
|
"bs": {
|
||||||
|
@ -312,6 +361,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"danas": "0D",
|
"danas": "0D",
|
||||||
"jučer": "1D"
|
"jučer": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"hilj": 3,
|
||||||
|
"mil": 6,
|
||||||
|
"mlr": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ca": {
|
"ca": {
|
||||||
|
@ -351,6 +406,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ahir": "1D",
|
"ahir": "1D",
|
||||||
"avui": "0D"
|
"avui": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"m": 3,
|
||||||
|
"mM": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cs": {
|
"cs": {
|
||||||
|
@ -378,6 +438,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"dnes": "0D",
|
"dnes": "0D",
|
||||||
"včera": "1D"
|
"včera": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mil": 6,
|
||||||
|
"mld": 9,
|
||||||
|
"tis": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"da": {
|
"da": {
|
||||||
|
@ -416,6 +482,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"dag": "0D",
|
"dag": "0D",
|
||||||
"går": "1D"
|
"går": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mia": 9,
|
||||||
|
"mio": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"de": {
|
"de": {
|
||||||
|
@ -442,6 +513,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"gestern": "1D",
|
"gestern": "1D",
|
||||||
"heute": "0D"
|
"heute": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"Mio": 6,
|
||||||
|
"Mrd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"el": {
|
"el": {
|
||||||
|
@ -481,6 +557,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"σήμερα": "0D",
|
"σήμερα": "0D",
|
||||||
"χτες": "1D"
|
"χτες": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"δισ": 9,
|
||||||
|
"εκ": 6,
|
||||||
|
"χιλ": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
|
@ -524,11 +606,59 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"today": "0D",
|
"today": "0D",
|
||||||
"yesterday": "1D"
|
"yesterday": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 9,
|
||||||
|
"crore": 7,
|
||||||
|
"lakh": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
|
"equivalent": [],
|
||||||
|
"by_char": false,
|
||||||
|
"timeago_tokens": {
|
||||||
|
"año": "Y",
|
||||||
|
"años": "Y",
|
||||||
|
"día": "D",
|
||||||
|
"días": "D",
|
||||||
|
"hora": "h",
|
||||||
|
"horas": "h",
|
||||||
|
"mes": "M",
|
||||||
|
"meses": "M",
|
||||||
|
"minuto": "m",
|
||||||
|
"minutos": "m",
|
||||||
|
"segundo": "s",
|
||||||
|
"segundos": "s",
|
||||||
|
"semana": "W",
|
||||||
|
"semanas": "W"
|
||||||
|
},
|
||||||
|
"date_order": "DY",
|
||||||
|
"months": {
|
||||||
|
"abr": 4,
|
||||||
|
"ago": 8,
|
||||||
|
"dic": 12,
|
||||||
|
"ene": 1,
|
||||||
|
"feb": 2,
|
||||||
|
"jul": 7,
|
||||||
|
"jun": 6,
|
||||||
|
"mar": 3,
|
||||||
|
"may": 5,
|
||||||
|
"nov": 11,
|
||||||
|
"oct": 10,
|
||||||
|
"sept": 9
|
||||||
|
},
|
||||||
|
"timeago_nd_tokens": {
|
||||||
|
"ayer": "1D",
|
||||||
|
"hoy": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mil": 9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-US": {
|
||||||
"equivalent": [
|
"equivalent": [
|
||||||
"es-US",
|
|
||||||
"es-419"
|
"es-419"
|
||||||
],
|
],
|
||||||
"by_char": false,
|
"by_char": false,
|
||||||
|
@ -566,6 +696,10 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ayer": "1D",
|
"ayer": "1D",
|
||||||
"hoy": "0D"
|
"hoy": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"mil": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"et": {
|
"et": {
|
||||||
|
@ -607,6 +741,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"eile": "1D",
|
"eile": "1D",
|
||||||
"täna": "0D"
|
"täna": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mld": 9,
|
||||||
|
"mln": 6,
|
||||||
|
"tuh": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eu": {
|
"eu": {
|
||||||
|
@ -642,7 +782,9 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"atzo": "1D",
|
"atzo": "1D",
|
||||||
"gaur": "0D"
|
"gaur": "0D"
|
||||||
}
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {}
|
||||||
},
|
},
|
||||||
"fa": {
|
"fa": {
|
||||||
"equivalent": [],
|
"equivalent": [],
|
||||||
|
@ -674,6 +816,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"امروز": "0D",
|
"امروز": "0D",
|
||||||
"دیروز": "1D"
|
"دیروز": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"میلیارد": 9,
|
||||||
|
"میلیون": 6,
|
||||||
|
"هزار": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fi": {
|
"fi": {
|
||||||
|
@ -700,6 +848,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"eilen": "1D",
|
"eilen": "1D",
|
||||||
"tänään": "0D"
|
"tänään": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"milj": 6,
|
||||||
|
"mrd": 9,
|
||||||
|
"t": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fil": {
|
"fil": {
|
||||||
|
@ -732,6 +886,10 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"kahapon": "1D",
|
"kahapon": "1D",
|
||||||
"ngayong": "0D"
|
"ngayong": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
|
@ -773,6 +931,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"aujourd'hui": "0D",
|
"aujourd'hui": "0D",
|
||||||
"hier": "1D"
|
"hier": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"G": 9,
|
||||||
|
"Md": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gl": {
|
"gl": {
|
||||||
|
@ -812,7 +975,9 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"hoxe": "0D",
|
"hoxe": "0D",
|
||||||
"onte": "1D"
|
"onte": "1D"
|
||||||
}
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {}
|
||||||
},
|
},
|
||||||
"gu": {
|
"gu": {
|
||||||
"equivalent": [],
|
"equivalent": [],
|
||||||
|
@ -844,6 +1009,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"આજે": "0D",
|
"આજે": "0D",
|
||||||
"ગઈ": "1D"
|
"ગઈ": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"અબજ": 9,
|
||||||
|
"કરોડ": 7,
|
||||||
|
"લાખ": 5,
|
||||||
|
"હજાર": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hi": {
|
"hi": {
|
||||||
|
@ -876,6 +1048,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"आज": "0D",
|
"आज": "0D",
|
||||||
"कल": "1D"
|
"कल": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"अ॰": 9,
|
||||||
|
"क॰": 7,
|
||||||
|
"लाख": 5,
|
||||||
|
"हज़ार": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hr": {
|
"hr": {
|
||||||
|
@ -920,6 +1099,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"danas": "0D",
|
"danas": "0D",
|
||||||
"jučer": "1D"
|
"jučer": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mil": 6,
|
||||||
|
"mlr": 9,
|
||||||
|
"tis": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hu": {
|
"hu": {
|
||||||
|
@ -959,6 +1144,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ma": "0D",
|
"ma": "0D",
|
||||||
"tegnap": "1D"
|
"tegnap": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"E": 3,
|
||||||
|
"Mrd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hy": {
|
"hy": {
|
||||||
|
@ -991,6 +1181,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"այսօր": "0D",
|
"այսօր": "0D",
|
||||||
"երեկ": "1D"
|
"երեկ": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"հզր": 3,
|
||||||
|
"մլն": 6,
|
||||||
|
"մլրդ": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
|
@ -1023,6 +1219,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ini": "0D",
|
"ini": "0D",
|
||||||
"kemarin": "1D"
|
"kemarin": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"jt": 6,
|
||||||
|
"rb": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is": {
|
"is": {
|
||||||
|
@ -1062,6 +1263,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"dag": "0D",
|
"dag": "0D",
|
||||||
"gær": "1D"
|
"gær": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"m": 6,
|
||||||
|
"ma": 9,
|
||||||
|
"þ": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it": {
|
"it": {
|
||||||
|
@ -1101,6 +1308,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ieri": "1D",
|
"ieri": "1D",
|
||||||
"oggi": "0D"
|
"oggi": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"Mln": 6,
|
||||||
|
"Mrd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"iw": {
|
"iw": {
|
||||||
|
@ -1146,6 +1358,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"אתמול": "1D",
|
"אתמול": "1D",
|
||||||
"היום": "0D"
|
"היום": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 9,
|
||||||
|
"K": 3,
|
||||||
|
"M": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
|
@ -1165,6 +1383,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"日": "1D",
|
"日": "1D",
|
||||||
"本": "0D"
|
"本": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"万": 4,
|
||||||
|
"億": 8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ka": {
|
"ka": {
|
||||||
|
@ -1197,6 +1420,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"გუშინ": "1D",
|
"გუშინ": "1D",
|
||||||
"დღეს": "0D"
|
"დღეს": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"ათ": 3,
|
||||||
|
"მლნ": 6,
|
||||||
|
"მლრდ": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"kk": {
|
"kk": {
|
||||||
|
@ -1229,6 +1458,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"бүгін": "0D",
|
"бүгін": "0D",
|
||||||
"кеше": "1D"
|
"кеше": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"м": 3,
|
||||||
|
"млн": 6,
|
||||||
|
"млрд": 9,
|
||||||
|
"мың": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"km": {
|
"km": {
|
||||||
|
@ -1261,6 +1497,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"បានធ្វើបច្ចុប្បន្នភាពថ្ងៃនេះ": "0D",
|
"បានធ្វើបច្ចុប្បន្នភាពថ្ងៃនេះ": "0D",
|
||||||
"បានធ្វើបច្ចុប្បន្នភាពម្សិលមិញ": "1D"
|
"បានធ្វើបច្ចុប្បន្នភាពម្សិលមិញ": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"ប៊ីលាន": 9,
|
||||||
|
"ពាន់": 3,
|
||||||
|
"លាន": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"kn": {
|
"kn": {
|
||||||
|
@ -1300,6 +1542,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ಇಂದು": "0D",
|
"ಇಂದು": "0D",
|
||||||
"ನಿನ್ನೆ": "1D"
|
"ನಿನ್ನೆ": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ಕೋಟಿ": 7,
|
||||||
|
"ಲಕ್ಷ": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko": {
|
"ko": {
|
||||||
|
@ -1318,6 +1565,12 @@
|
||||||
"months": {},
|
"months": {},
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"오늘": "0D"
|
"오늘": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"만회": 4,
|
||||||
|
"억회": 8,
|
||||||
|
"천회": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ky": {
|
"ky": {
|
||||||
|
@ -1350,6 +1603,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"бүгүн": "0D",
|
"бүгүн": "0D",
|
||||||
"кечээ": "1D"
|
"кечээ": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"миң": 3,
|
||||||
|
"млд": 9,
|
||||||
|
"млн": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lo": {
|
"lo": {
|
||||||
|
@ -1382,6 +1641,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ອັບເດດມື້ນີ້": "0D",
|
"ອັບເດດມື້ນີ້": "0D",
|
||||||
"ອັບເດດມື້ວານນີ້": "1D"
|
"ອັບເດດມື້ວານນີ້": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"ກີບ": 3,
|
||||||
|
"ຕື້": 9,
|
||||||
|
"ພັນ": 3,
|
||||||
|
"ລ້ານ": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lt": {
|
"lt": {
|
||||||
|
@ -1415,6 +1681,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"vakar": "1D",
|
"vakar": "1D",
|
||||||
"šiandien": "0D"
|
"šiandien": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mln": 6,
|
||||||
|
"mlrd": 9,
|
||||||
|
"tūkst": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lv": {
|
"lv": {
|
||||||
|
@ -1454,6 +1726,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"vakar": "1D",
|
"vakar": "1D",
|
||||||
"šodien": "0D"
|
"šodien": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"milj": 6,
|
||||||
|
"mljrd": 9,
|
||||||
|
"tūkst": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mk": {
|
"mk": {
|
||||||
|
@ -1480,6 +1758,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"вчера": "1D",
|
"вчера": "1D",
|
||||||
"денес": "0D"
|
"денес": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"М": 6,
|
||||||
|
"илј": 3,
|
||||||
|
"мил": 6,
|
||||||
|
"милј": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ml": {
|
"ml": {
|
||||||
|
@ -1512,6 +1797,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ഇന്നലെ": "1D",
|
"ഇന്നലെ": "1D",
|
||||||
"ഇന്ന്": "0D"
|
"ഇന്ന്": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"കോടി": 7,
|
||||||
|
"ലക്ഷം": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mn": {
|
"mn": {
|
||||||
|
@ -1531,6 +1821,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"өнөөдөр": "0D",
|
"өнөөдөр": "0D",
|
||||||
"өчигдөр": "1D"
|
"өчигдөр": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"мянга": 3,
|
||||||
|
"сая": 6,
|
||||||
|
"тэрбум": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mr": {
|
"mr": {
|
||||||
|
@ -1570,6 +1866,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"आज": "0D",
|
"आज": "0D",
|
||||||
"काल": "1D"
|
"काल": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"अब्ज": 9,
|
||||||
|
"कोटी": 7,
|
||||||
|
"लाख": 5,
|
||||||
|
"ह": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
|
@ -1602,6 +1905,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ini": "0D",
|
"ini": "0D",
|
||||||
"semalam": "1D"
|
"semalam": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 9,
|
||||||
|
"J": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"my": {
|
"my": {
|
||||||
|
@ -1635,6 +1943,15 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"မနေ့က": "1D",
|
"မနေ့က": "1D",
|
||||||
"ယနေ့": "0D"
|
"ယနေ့": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ကုဋေ": 7,
|
||||||
|
"ကုဋေထ": 10,
|
||||||
|
"ထောင်": 3,
|
||||||
|
"သန်း": 6,
|
||||||
|
"သိန်း": 5,
|
||||||
|
"သောင်း": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ne": {
|
"ne": {
|
||||||
|
@ -1667,6 +1984,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"आज": "0D",
|
"आज": "0D",
|
||||||
"हिजो": "1D"
|
"हिजो": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"अरब": 9,
|
||||||
|
"करोड": 7,
|
||||||
|
"लाख": 5,
|
||||||
|
"हजार": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nl": {
|
"nl": {
|
||||||
|
@ -1704,6 +2028,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"gisteren": "1D",
|
"gisteren": "1D",
|
||||||
"vandaag": "0D"
|
"vandaag": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mld": 9,
|
||||||
|
"mln": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"no": {
|
"no": {
|
||||||
|
@ -1743,6 +2072,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"dag": "0D",
|
"dag": "0D",
|
||||||
"går": "1D"
|
"går": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mill": 6,
|
||||||
|
"mrd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"or": {
|
"or": {
|
||||||
|
@ -1775,6 +2109,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ଆଜି": "0D",
|
"ଆଜି": "0D",
|
||||||
"ଗତକାଲି": "1D"
|
"ଗତକାଲି": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ନିଟି": 6,
|
||||||
|
"ବିଟି": 9,
|
||||||
|
"ହଟି": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pa": {
|
"pa": {
|
||||||
|
@ -1810,6 +2150,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ਅੱਜ": "0D",
|
"ਅੱਜ": "0D",
|
||||||
"ਬੀੇਤੇ": "1D"
|
"ਬੀੇਤੇ": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ਅਰਬ": 9,
|
||||||
|
"ਕਰੋੜ": 7,
|
||||||
|
"ਲੱਖ": 5,
|
||||||
|
"ਹਜ਼ਾਰ": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl": {
|
"pl": {
|
||||||
|
@ -1854,6 +2201,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"dzisiaj": "0D",
|
"dzisiaj": "0D",
|
||||||
"wczoraj": "1D"
|
"wczoraj": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mld": 9,
|
||||||
|
"mln": 6,
|
||||||
|
"tys": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt": {
|
"pt": {
|
||||||
|
@ -1893,6 +2246,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"hoje": "0D",
|
"hoje": "0D",
|
||||||
"ontem": "1D"
|
"ontem": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"bi": 9,
|
||||||
|
"mi": 6,
|
||||||
|
"mil": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt-PT": {
|
"pt-PT": {
|
||||||
|
@ -1919,6 +2278,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"hoje": "0D",
|
"hoje": "0D",
|
||||||
"ontem": "1D"
|
"ontem": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mM": 9,
|
||||||
|
"mil": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ro": {
|
"ro": {
|
||||||
|
@ -1958,6 +2322,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"astăzi": "0D",
|
"astăzi": "0D",
|
||||||
"ieri": "1D"
|
"ieri": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mil": 6,
|
||||||
|
"mld": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
|
@ -2003,6 +2372,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"вчера": "1D",
|
"вчера": "1D",
|
||||||
"сегодня": "0D"
|
"сегодня": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"млн": 6,
|
||||||
|
"млрд": 9,
|
||||||
|
"тыс": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"si": {
|
"si": {
|
||||||
|
@ -2036,6 +2411,12 @@
|
||||||
"අද": "0D",
|
"අද": "0D",
|
||||||
"ඊයෙ": "1D",
|
"ඊයෙ": "1D",
|
||||||
"ඊයේ": "1D"
|
"ඊයේ": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ද": 3,
|
||||||
|
"බි": 9,
|
||||||
|
"මි": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sk": {
|
"sk": {
|
||||||
|
@ -2062,6 +2443,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"dnes": "0D",
|
"dnes": "0D",
|
||||||
"včera": "1D"
|
"včera": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mil": 6,
|
||||||
|
"mld": 9,
|
||||||
|
"tis": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sl": {
|
"sl": {
|
||||||
|
@ -2109,6 +2496,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"danes": "0D",
|
"danes": "0D",
|
||||||
"včeraj": "1D"
|
"včeraj": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mio": 6,
|
||||||
|
"mrd": 9,
|
||||||
|
"tis": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sq": {
|
"sq": {
|
||||||
|
@ -2144,6 +2537,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"dje": "1D",
|
"dje": "1D",
|
||||||
"sot": "0D"
|
"sot": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"mijë": 3,
|
||||||
|
"mld": 9,
|
||||||
|
"mln": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr": {
|
"sr": {
|
||||||
|
@ -2172,6 +2571,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"данас": "0D",
|
"данас": "0D",
|
||||||
"јуче": "1D"
|
"јуче": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"мил": 6,
|
||||||
|
"млрд": 9,
|
||||||
|
"хиљ": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sr-Latn": {
|
"sr-Latn": {
|
||||||
|
@ -2201,6 +2606,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"danas": "0D",
|
"danas": "0D",
|
||||||
"juče": "1D"
|
"juče": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"hilj": 3,
|
||||||
|
"mil": 6,
|
||||||
|
"mlrd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sv": {
|
"sv": {
|
||||||
|
@ -2239,6 +2650,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"idag": "0D",
|
"idag": "0D",
|
||||||
"igår": "1D"
|
"igår": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"md": 9,
|
||||||
|
"mn": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sw": {
|
"sw": {
|
||||||
|
@ -2273,6 +2689,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"jana": "1D",
|
"jana": "1D",
|
||||||
"leo": "0D"
|
"leo": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 9,
|
||||||
|
"elfu": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ta": {
|
"ta": {
|
||||||
|
@ -2311,6 +2732,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"இன்று": "0D",
|
"இன்று": "0D",
|
||||||
"நேற்று": "1D"
|
"நேற்று": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"கோடி": 7,
|
||||||
|
"லட்சம்": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"te": {
|
"te": {
|
||||||
|
@ -2350,6 +2776,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"ఈ": "0D",
|
"ఈ": "0D",
|
||||||
"నిన్న": "1D"
|
"నిన్న": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"కోట్లు": 7,
|
||||||
|
"లక్ష": 5,
|
||||||
|
"లక్షలు": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"th": {
|
"th": {
|
||||||
|
@ -2382,6 +2814,15 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"อัปเดตแล้ววันนี้": "0D",
|
"อัปเดตแล้ววันนี้": "0D",
|
||||||
"อัปเดตแล้วเมื่อวาน": "1D"
|
"อัปเดตแล้วเมื่อวาน": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"พัน": 3,
|
||||||
|
"พันล้าน": 9,
|
||||||
|
"ล้าน": 6,
|
||||||
|
"หมื่น": 4,
|
||||||
|
"หมื่นล้าน": 10,
|
||||||
|
"แสน": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tr": {
|
"tr": {
|
||||||
|
@ -2414,6 +2855,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"bugün": "0D",
|
"bugün": "0D",
|
||||||
"dün": "1D"
|
"dün": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 3,
|
||||||
|
"Mn": 6,
|
||||||
|
"Mr": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uk": {
|
"uk": {
|
||||||
|
@ -2459,6 +2906,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"вчора": "1D",
|
"вчора": "1D",
|
||||||
"сьогодні": "0D"
|
"сьогодні": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"млн": 6,
|
||||||
|
"млрд": 9,
|
||||||
|
"тис": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ur": {
|
"ur": {
|
||||||
|
@ -2497,6 +2950,13 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"آج": "0D",
|
"آج": "0D",
|
||||||
"کل": "1D"
|
"کل": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"ارب": 9,
|
||||||
|
"لاکھ": 5,
|
||||||
|
"کروڑ": 7,
|
||||||
|
"ہزار": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uz": {
|
"uz": {
|
||||||
|
@ -2529,6 +2989,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"bugun": "0D",
|
"bugun": "0D",
|
||||||
"kecha": "1D"
|
"kecha": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"ming": 3,
|
||||||
|
"mln": 6,
|
||||||
|
"mlrd": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vi": {
|
"vi": {
|
||||||
|
@ -2549,6 +3015,12 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"nay": "0D",
|
"nay": "0D",
|
||||||
"qua": "1D"
|
"qua": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": true,
|
||||||
|
"number_tokens": {
|
||||||
|
"N": 3,
|
||||||
|
"T": 9,
|
||||||
|
"Tr": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
|
@ -2568,6 +3040,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"今": "0D",
|
"今": "0D",
|
||||||
"日": "1D"
|
"日": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"万": 4,
|
||||||
|
"亿": 8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-HK": {
|
"zh-HK": {
|
||||||
|
@ -2588,6 +3065,10 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"今": "0D",
|
"今": "0D",
|
||||||
"天": "1D"
|
"天": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 9
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-TW": {
|
"zh-TW": {
|
||||||
|
@ -2607,6 +3088,11 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"今": "0D",
|
"今": "0D",
|
||||||
"天": "1D"
|
"天": "1D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"億": 8,
|
||||||
|
"萬": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zu": {
|
"zu": {
|
||||||
|
@ -2646,6 +3132,10 @@
|
||||||
"timeago_nd_tokens": {
|
"timeago_nd_tokens": {
|
||||||
"izolo": "1D",
|
"izolo": "1D",
|
||||||
"namuhla": "0D"
|
"namuhla": "0D"
|
||||||
|
},
|
||||||
|
"comma_decimal": false,
|
||||||
|
"number_tokens": {
|
||||||
|
"B": 9
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
3156
testfiles/dict/large_number_samples.json
Normal file
3156
testfiles/dict/large_number_samples.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue