fix: improve number parsing, add number_nd_tokens
add dictionary overrides
This commit is contained in:
parent
97492780c6
commit
19781eab36
13 changed files with 33097 additions and 35712 deletions
|
|
@ -72,7 +72,7 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
});
|
||||
}
|
||||
|
||||
util::write_dict(project_root, &dict);
|
||||
util::write_dict(project_root, dict);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures::{stream, StreamExt};
|
||||
|
|
@ -9,26 +13,12 @@ use path_macro::path;
|
|||
use regex::Regex;
|
||||
use rustypipe::client::{ClientType, RustyPipe, RustyPipeQuery};
|
||||
use rustypipe::param::{locale::LANGUAGES, Language};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||
|
||||
use crate::util::{self, QBrowse, QCont, Text};
|
||||
use crate::util::{self, QBrowse, QCont, Text, TextRuns};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(untagged)]
|
||||
enum NumKey {
|
||||
Mag(u8),
|
||||
S(NumKeyS),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum NumKeyS {
|
||||
Zero,
|
||||
One,
|
||||
}
|
||||
|
||||
type CollectedNumbers = BTreeMap<Language, BTreeMap<NumKey, (String, u64)>>;
|
||||
type CollectedNumbers = BTreeMap<Language, BTreeMap<String, u64>>;
|
||||
|
||||
/// Collect video view count texts in every supported language
|
||||
/// and write them to `testfiles/dict/large_number_samples.json`.
|
||||
|
|
@ -45,9 +35,7 @@ type CollectedNumbers = BTreeMap<Language, BTreeMap<NumKey, (String, u64)>>;
|
|||
/// 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 json_path = path!(project_root / "testfiles" / "dict" / "large_number_samples.json");
|
||||
let json_path_all =
|
||||
path!(project_root / "testfiles" / "dict" / "large_number_samples_all.json");
|
||||
let json_path = path!(project_root / "testfiles" / "dict" / "large_number_samples_all.json");
|
||||
let rp = RustyPipe::new();
|
||||
|
||||
let channels = [
|
||||
|
|
@ -64,6 +52,16 @@ pub async fn collect_large_numbers(project_root: &Path, concurrency: usize) {
|
|||
"UCQXYK94vDqOEkPbTCyL0OjA", // (1)
|
||||
];
|
||||
|
||||
// YTM outputs the subscriber count in a shortened format in some languages
|
||||
let music_channels = [
|
||||
"UC_1N84buVNgR_-3gDZ9Jtxg", // 10e8 (158M)
|
||||
"UCRw0x9_EfawqmgDI2IgQLLg", // 10e7 (29M)
|
||||
"UChWu2clmvJ5wN_0Ic5dnqmw", // 10e6 (1.9M)
|
||||
"UCOYiPDuimprrGHgFy4_Fw8Q", // 10e5 (149K)
|
||||
"UC8nZf9WyVIxNMly_hy2PTyQ", // 10e4 (17K)
|
||||
"UCaltNL5XvZ7dKvBsBPi-gqg", // 10e3 (8K)
|
||||
];
|
||||
|
||||
// Build a lookup table for the channel's subscriber counts
|
||||
let subscriber_counts: Arc<BTreeMap<String, u64>> = stream::iter(channels)
|
||||
.map(|c| {
|
||||
|
|
@ -80,10 +78,26 @@ pub async fn collect_large_numbers(project_root: &Path, concurrency: usize) {
|
|||
.await
|
||||
.into();
|
||||
|
||||
let collected_numbers_all: BTreeMap<Language, BTreeMap<String, u64>> = stream::iter(LANGUAGES)
|
||||
let music_subscriber_counts: Arc<BTreeMap<String, u64>> = stream::iter(music_channels)
|
||||
.map(|c| {
|
||||
let rp = rp.query();
|
||||
async move {
|
||||
let subscriber_count = music_channel_subscribers(&rp, c).await.unwrap();
|
||||
|
||||
let n = util::parse_largenum_en(&subscriber_count).unwrap();
|
||||
(c.to_owned(), n)
|
||||
}
|
||||
})
|
||||
.buffer_unordered(concurrency)
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
.await
|
||||
.into();
|
||||
|
||||
let collected_numbers: CollectedNumbers = stream::iter(LANGUAGES)
|
||||
.map(|lang| {
|
||||
let rp = rp.query().lang(lang);
|
||||
let subscriber_counts = subscriber_counts.clone();
|
||||
let music_subscriber_counts = music_subscriber_counts.clone();
|
||||
async move {
|
||||
let mut entry = BTreeMap::new();
|
||||
|
||||
|
|
@ -101,6 +115,15 @@ pub async fn collect_large_numbers(project_root: &Path, concurrency: usize) {
|
|||
println!("collected {lang}-{n}");
|
||||
}
|
||||
|
||||
for (n, ch_id) in music_channels.iter().enumerate() {
|
||||
let subscriber_count = music_channel_subscribers(&rp, ch_id)
|
||||
.await
|
||||
.context(format!("{lang}-music-{n}"))
|
||||
.unwrap();
|
||||
entry.insert(subscriber_count, music_subscriber_counts[*ch_id]);
|
||||
println!("collected {lang}-music-{n}");
|
||||
}
|
||||
|
||||
(lang, entry)
|
||||
}
|
||||
})
|
||||
|
|
@ -108,61 +131,13 @@ pub async fn collect_large_numbers(project_root: &Path, concurrency: usize) {
|
|||
.collect()
|
||||
.await;
|
||||
|
||||
let collected_numbers: CollectedNumbers = collected_numbers_all
|
||||
.iter()
|
||||
.map(|(lang, entry)| {
|
||||
let mut e2 = BTreeMap::new();
|
||||
entry.iter().for_each(|(txt, num)| {
|
||||
let key = if num == &0 {
|
||||
NumKey::S(NumKeyS::Zero)
|
||||
} else if num == &1 {
|
||||
NumKey::S(NumKeyS::One)
|
||||
} else {
|
||||
NumKey::Mag(get_mag(*num))
|
||||
};
|
||||
|
||||
e2.insert(key, (txt.to_owned(), *num));
|
||||
});
|
||||
(*lang, e2)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let file = File::create(json_path).unwrap();
|
||||
serde_json::to_writer_pretty(file, &collected_numbers).unwrap();
|
||||
|
||||
let file = File::create(json_path_all).unwrap();
|
||||
serde_json::to_writer_pretty(file, &collected_numbers_all).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) {
|
||||
/*
|
||||
Manual corrections:
|
||||
as
|
||||
"কোঃটা": 9,
|
||||
"নিঃটা": 6,
|
||||
"নিযুতটা": 6,
|
||||
"লাখটা": 5,
|
||||
"হাজাৰটা": 3
|
||||
|
||||
ar
|
||||
"ألف": 3,
|
||||
"آلاف": 3,
|
||||
"مليار": 9,
|
||||
"مليون": 6
|
||||
|
||||
bn
|
||||
"লাটি": 5,
|
||||
"শত": 2,
|
||||
"হাটি": 3,
|
||||
"কোটি": 7
|
||||
|
||||
es/es-US
|
||||
"mil": 3,
|
||||
"M": 6
|
||||
*/
|
||||
|
||||
let json_path = path!(project_root / "testfiles" / "dict" / "large_number_samples.json");
|
||||
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
|
|
@ -179,27 +154,21 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
let mut e_langs = dict_entry.equivalent.clone();
|
||||
e_langs.push(lang);
|
||||
|
||||
let comma_decimal = collected_nums
|
||||
.get(&lang)
|
||||
.unwrap()
|
||||
let comma_decimal = collected_nums[&lang]
|
||||
.iter()
|
||||
.find_map(|(key, (txt, _))| {
|
||||
match key {
|
||||
NumKey::Mag(mag) => {
|
||||
let point = POINT_REGEX
|
||||
.captures(txt)
|
||||
.map(|c| c.get(1).unwrap().as_str());
|
||||
.find_map(|(txt, val)| {
|
||||
let point = POINT_REGEX
|
||||
.captures(txt)
|
||||
.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 == ","));
|
||||
}
|
||||
}
|
||||
NumKey::S(_) => {}
|
||||
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) == get_mag(*val)) ^ (point == ","));
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.unwrap();
|
||||
|
|
@ -217,6 +186,7 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
// 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 found_nd_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 {
|
||||
|
|
@ -231,22 +201,30 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
}
|
||||
};
|
||||
|
||||
let mut insert_nd_token = |token: String, n: Option<u8>| {
|
||||
let found_token = found_nd_tokens.entry(token).or_insert(n);
|
||||
|
||||
if let Some(f) = found_token {
|
||||
if Some(*f) != n {
|
||||
*found_token = None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for lang in e_langs {
|
||||
let entry = collected_nums.get(&lang).unwrap();
|
||||
|
||||
entry.iter().for_each(|(key, (txt, _))| {
|
||||
match key {
|
||||
NumKey::Mag(mag) => {
|
||||
let filtered = util::filter_largenumstr(txt);
|
||||
entry.iter().for_each(|(txt, val)| {
|
||||
let filtered = util::filter_largenumstr(txt);
|
||||
let mag = get_mag(*val);
|
||||
|
||||
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 tokens: Vec<String> = match dict_entry.by_char || lang == Language::Ko {
|
||||
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();
|
||||
match util::parse_numeric::<u64>(txt.split(decimal_point).next().unwrap()) {
|
||||
Ok(num_before_point) => {
|
||||
let mag_before_point = get_mag(num_before_point);
|
||||
let mut mag_remaining = mag - mag_before_point;
|
||||
|
||||
|
|
@ -272,9 +250,22 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
} else {
|
||||
insert_token(t.to_owned(), mag_remaining);
|
||||
}
|
||||
insert_nd_token(t.to_owned(), None);
|
||||
});
|
||||
}
|
||||
NumKey::S(_) => {}
|
||||
Err(e) => {
|
||||
if matches!(e.kind(), std::num::IntErrorKind::Empty) {
|
||||
// Text does not contain any digits, search for nd_tokens
|
||||
tokens.iter().for_each(|t| {
|
||||
insert_nd_token(
|
||||
t.to_owned(),
|
||||
Some((*val).try_into().expect("nd_token value too large")),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("{e}, txt: {txt}")
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -284,6 +275,10 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
.into_iter()
|
||||
.filter_map(|(k, v)| v.map(|v| (k, v)))
|
||||
.collect();
|
||||
dict_entry.number_nd_tokens = found_nd_tokens
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| v.map(|v| (k, v)))
|
||||
.collect();
|
||||
dict_entry.comma_decimal = comma_decimal;
|
||||
|
||||
// Check for duplicates
|
||||
|
|
@ -291,9 +286,13 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
if !dict_entry.number_tokens.values().all(|x| uniq.insert(x)) {
|
||||
println!("Warning: collected duplicate tokens for {lang}");
|
||||
}
|
||||
let mut uniq = HashSet::new();
|
||||
if !dict_entry.number_nd_tokens.values().all(|x| uniq.insert(x)) {
|
||||
println!("Warning: collected duplicate nd_tokens for {lang}");
|
||||
}
|
||||
}
|
||||
|
||||
util::write_dict(project_root, &dict);
|
||||
util::write_dict(project_root, dict);
|
||||
}
|
||||
|
||||
fn get_mag(n: u64) -> u8 {
|
||||
|
|
@ -304,59 +303,59 @@ fn get_mag(n: u64) -> u8 {
|
|||
YouTube channel videos response
|
||||
*/
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Channel {
|
||||
contents: Contents,
|
||||
header: ChannelHeader,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChannelHeader {
|
||||
c4_tabbed_header_renderer: HeaderRenderer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HeaderRenderer {
|
||||
subscriber_count_text: Text,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Contents {
|
||||
two_column_browse_results_renderer: TabsRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TabsRenderer {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
tabs: Vec<TabRendererWrap>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TabRendererWrap {
|
||||
tab_renderer: TabRenderer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TabRenderer {
|
||||
content: RichGridRendererWrap,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RichGridRendererWrap {
|
||||
rich_grid_renderer: RichGridRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RichGridRenderer {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
|
|
@ -366,25 +365,25 @@ struct RichGridRenderer {
|
|||
header: Option<RichGridHeader>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RichItemRendererWrap {
|
||||
rich_item_renderer: RichItemRenderer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RichItemRenderer {
|
||||
content: VideoRendererWrap,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VideoRendererWrap {
|
||||
video_renderer: VideoRenderer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VideoRenderer {
|
||||
/// `24,194 views`
|
||||
|
|
@ -393,65 +392,100 @@ struct VideoRenderer {
|
|||
short_view_count_text: Text,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RichGridHeader {
|
||||
feed_filter_chip_bar_renderer: ChipBar,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChipBar {
|
||||
contents: Vec<Chip>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Chip {
|
||||
chip_cloud_chip_renderer: ChipRenderer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChipRenderer {
|
||||
navigation_endpoint: NavigationEndpoint,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NavigationEndpoint {
|
||||
continuation_command: ContinuationCommand,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ContinuationCommand {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ContinuationResponse {
|
||||
// #[serde_as(as = "VecSkipError<_>")]
|
||||
on_response_received_actions: Vec<ContinuationAction>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ContinuationAction {
|
||||
reload_continuation_items_command: ContinuationItemsWrap,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ContinuationItemsWrap {
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
continuation_items: Vec<RichItemRendererWrap>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/*
|
||||
YouTube Music channel data
|
||||
*/
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MusicChannel {
|
||||
header: MusicHeader,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MusicHeader {
|
||||
#[serde(alias = "musicVisualHeaderRenderer")]
|
||||
music_immersive_header_renderer: MusicHeaderRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MusicHeaderRenderer {
|
||||
subscription_button: SubscriptionButton,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SubscriptionButton {
|
||||
subscribe_button_renderer: SubscriptionButtonRenderer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SubscriptionButtonRenderer {
|
||||
subscriber_count_text: TextRuns,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ChannelData {
|
||||
view_counts: BTreeMap<u64, String>,
|
||||
subscriber_count: String,
|
||||
|
|
@ -460,7 +494,7 @@ struct ChannelData {
|
|||
async fn get_channel(query: &RustyPipeQuery, channel_id: &str) -> Result<ChannelData> {
|
||||
let resp = query
|
||||
.raw(
|
||||
ClientType::DesktopMusic,
|
||||
ClientType::Desktop,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: query.get_context(ClientType::Desktop, true, None).await,
|
||||
|
|
@ -540,18 +574,31 @@ async fn get_channel(query: &RustyPipeQuery, channel_id: &str) -> Result<Channel
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustypipe::client::RustyPipe;
|
||||
async fn music_channel_subscribers(query: &RustyPipeQuery, channel_id: &str) -> Result<String> {
|
||||
let resp = query
|
||||
.raw(
|
||||
ClientType::DesktopMusic,
|
||||
"browse",
|
||||
&QBrowse {
|
||||
context: query
|
||||
.get_context(ClientType::DesktopMusic, true, None)
|
||||
.await,
|
||||
browse_id: channel_id,
|
||||
params: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn t() {
|
||||
let rp = RustyPipe::new();
|
||||
let x = get_channel(&rp.query(), "UCQXYK94vDqOEkPbTCyL0OjA")
|
||||
.await
|
||||
.unwrap();
|
||||
dbg!(&x);
|
||||
}
|
||||
let channel = serde_json::from_str::<MusicChannel>(&resp)?;
|
||||
channel
|
||||
.header
|
||||
.music_immersive_header_renderer
|
||||
.subscription_button
|
||||
.subscribe_button_renderer
|
||||
.subscriber_count_text
|
||||
.runs
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|t| t.text)
|
||||
.ok_or_else(|| anyhow::anyhow!("no text"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,5 +291,5 @@ pub fn write_samples_to_dict(project_root: &Path) {
|
|||
dict_entry.date_order = num_order;
|
||||
}
|
||||
|
||||
util::write_dict(project_root, &dict);
|
||||
util::write_dict(project_root, dict);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ pub(crate) struct Entry {
|
|||
///
|
||||
/// Format: Parsed token -> decimal power
|
||||
pub number_tokens: phf::Map<&'static str, u8>,
|
||||
/// Tokens for parsing number strings with no digits (e.g. "No videos")
|
||||
///
|
||||
/// Format: Parsed token -> value
|
||||
pub number_nd_tokens: phf::Map<&'static str, u8>,
|
||||
/// Names of album types (Album, Single, ...)
|
||||
///
|
||||
/// Format: Parsed text -> Album type
|
||||
|
|
@ -138,6 +142,12 @@ pub(crate) fn entry(lang: Language) -> Entry {
|
|||
number_tokens.entry(txt, &mag.to_string());
|
||||
});
|
||||
|
||||
// Number nd tokens
|
||||
let mut number_nd_tokens = phf_codegen::Map::<&str>::new();
|
||||
entry.number_nd_tokens.iter().for_each(|(txt, mag)| {
|
||||
number_nd_tokens.entry(txt, &mag.to_string());
|
||||
});
|
||||
|
||||
// Album types
|
||||
let mut album_types = phf_codegen::Map::<&str>::new();
|
||||
entry.album_types.iter().for_each(|(txt, album_type)| {
|
||||
|
|
@ -148,10 +158,11 @@ pub(crate) fn entry(lang: Language) -> Entry {
|
|||
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_number_tokens = &number_tokens.build().to_string().replace('\n', "\n ");
|
||||
let code_number_nd_tokens = &number_nd_tokens.build().to_string().replace('\n', "\n ");
|
||||
let code_album_types = &album_types.build().to_string().replace('\n', "\n ");
|
||||
|
||||
let _ = write!(code_timeago_tokens, "{} => Entry {{\n timeago_tokens: {},\n date_order: {},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n album_types: {},\n }},\n ",
|
||||
selector, code_ta_tokens, date_order, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens, code_album_types);
|
||||
write!(code_timeago_tokens, "{} => Entry {{\n timeago_tokens: {},\n date_order: {},\n months: {},\n timeago_nd_tokens: {},\n comma_decimal: {:?},\n number_tokens: {},\n number_nd_tokens: {},\n album_types: {},\n }},\n ",
|
||||
selector, code_ta_tokens, date_order, code_months, code_ta_nd_tokens, entry.comma_decimal, code_number_tokens, code_number_nd_tokens, code_album_types).unwrap();
|
||||
});
|
||||
|
||||
code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n";
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ use rustypipe::{client::YTContext, model::AlbumType, param::Language};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
static DICT_PATH: Lazy<PathBuf> = Lazy::new(|| path!("testfiles" / "dict" / "dictionary.json"));
|
||||
static DICT_OVERRIDE_PATH: Lazy<PathBuf> =
|
||||
Lazy::new(|| path!("testfiles" / "dict" / "dictionary_override.json"));
|
||||
|
||||
type Dictionary = BTreeMap<Language, DictEntry>;
|
||||
type DictionaryOverride = BTreeMap<Language, DictOverrideEntry>;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
|
|
@ -62,6 +65,13 @@ pub struct DictEntry {
|
|||
pub album_types: BTreeMap<String, AlbumType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct DictOverrideEntry {
|
||||
pub number_tokens: BTreeMap<String, Option<u8>>,
|
||||
pub number_nd_tokens: BTreeMap<String, Option<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QBrowse<'a> {
|
||||
|
|
@ -95,10 +105,41 @@ pub fn read_dict(project_root: &Path) -> Dictionary {
|
|||
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||
}
|
||||
|
||||
pub fn write_dict(project_root: &Path, dict: &Dictionary) {
|
||||
pub fn read_dict_override(project_root: &Path) -> DictionaryOverride {
|
||||
let json_path = path!(project_root / *DICT_OVERRIDE_PATH);
|
||||
let json_file = File::open(json_path).unwrap();
|
||||
serde_json::from_reader(BufReader::new(json_file)).unwrap()
|
||||
}
|
||||
|
||||
pub fn write_dict(project_root: &Path, dict: Dictionary) {
|
||||
let dict_override = read_dict_override(project_root);
|
||||
|
||||
let json_path = path!(project_root / *DICT_PATH);
|
||||
let json_file = File::create(json_path).unwrap();
|
||||
serde_json::to_writer_pretty(json_file, dict).unwrap();
|
||||
|
||||
fn apply_map<K: Clone + Ord, V: Clone>(map: &mut BTreeMap<K, V>, or: &BTreeMap<K, Option<V>>) {
|
||||
or.iter().for_each(|(key, val)| match val {
|
||||
Some(val) => {
|
||||
map.insert(key.clone(), val.clone());
|
||||
}
|
||||
None => {
|
||||
map.remove(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let dict: Dictionary = dict
|
||||
.into_iter()
|
||||
.map(|(lang, mut entry)| {
|
||||
if let Some(or) = dict_override.get(&lang) {
|
||||
apply_map(&mut entry.number_tokens, &or.number_tokens);
|
||||
apply_map(&mut entry.number_nd_tokens, &or.number_nd_tokens);
|
||||
}
|
||||
(lang, entry)
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_writer_pretty(json_file, &dict).unwrap();
|
||||
}
|
||||
|
||||
pub fn filter_datestr(string: &str) -> String {
|
||||
|
|
@ -133,6 +174,7 @@ pub fn filter_largenumstr(string: &str) -> String {
|
|||
| ','
|
||||
) && !c.is_ascii_digit()
|
||||
})
|
||||
.flat_map(char::to_lowercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
Reference in a new issue