Files
sdgb-utils-rs/sdgb-cli/src/main.rs

468 lines
16 KiB
Rust

use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::SystemTime,
};
use nyquest_preset::nyquest::ClientBuilder;
use palc::Parser;
use spdlog::{Level, LevelFilter::MoreSevereEqual, sink::StdStreamSink, terminal_style::StyleMode};
use sdgb_api::{
all_net::{GetResponse, QRCode},
auth_lite::{SDGB, SDHJ, delivery_raw},
title::{
MaiVersionExt, Sdgb1_53,
helper::get_user_all_music,
methods::APIMethod,
model::{
DataVersion, DxCalculatedEntries, DxMusicRecord, DxRatingNet, GetUserDataApi,
GetUserDataApiResp, GetUserPreviewApi, GetUserPreviewApiResp, GetUserRatingApi,
GetUserRatingApiResp, Ping, PingResp, UserLoginApiResp, UserLogoutApi,
UserLogoutApiResp,
},
},
};
use spdlog::{error, info, warn};
use crate::{
commands::{Cli, Commands, RatingFormat},
utils::{human_readable_display, json_display, login_action},
};
#[cfg(feature = "fetchall")]
mod cache;
mod commands;
mod utils;
static EARLY_QUIT: AtomicBool = AtomicBool::new(false);
#[cfg_attr(feature = "compio", compio::main)]
#[cfg_attr(feature = "tokio", tokio::main)]
async fn main() -> Result<(), Box<dyn snafu::Error>> {
nyquest_preset::register();
let logger = spdlog::default_logger().fork_with(|log| {
log.set_level_filter(MoreSevereEqual(if cfg!(debug_assertions) {
Level::Debug
} else {
Level::Info
}));
let sink = StdStreamSink::builder()
.stderr()
.style_mode(StyleMode::Always)
.build()?;
*log.sinks_mut() = vec![Arc::new(sink)];
Ok(())
})?;
_ = spdlog::swap_default_logger(logger);
ctrlc::set_handler(|| {
if EARLY_QUIT.load(Ordering::Relaxed) {
error!("force-quit triggered!");
std::process::exit(1);
} else {
warn!("received early-quit request! will abort soon");
EARLY_QUIT.store(true, Ordering::Relaxed);
}
})?;
let Cli {
command,
machine_readable,
} = <Cli as Parser>::parse();
let human_readable = !machine_readable;
let client = ClientBuilder::default().build_async().await?;
// TODO: refactor via enum_dispatch
match command {
Commands::MusicDetail { user_id, format } => {
let music_detail = get_user_all_music(&client, user_id).await?;
let details = music_detail
.user_music_list
.iter()
.map(|m| &m.user_music_detail_list)
.flatten();
match (human_readable, format) {
(true, _) => {
let mut count = 0;
for detail in details {
println!("{detail}");
println!("----------");
count += 1;
}
println!("共查询到 {count} 条记录!");
}
(false, RatingFormat::Origin) => json_display(music_detail)?,
(false, RatingFormat::DxRatingNet) => {
let dx_export = Vec::from_iter(
details
.map(|music| {
DxMusicRecord::try_from(music).inspect_err(|e| {
warn!("failed to process {}: {e}", music.music_id)
})
})
.flatten(),
);
json_display(dx_export)?;
}
(_, format) => {
error!("{format:?} was not supported yet");
json_display(())?;
}
}
}
Commands::Rating { user_id, format } => {
let rating: GetUserRatingApiResp = Sdgb1_53::request(
&client,
APIMethod::GetUserRatingApi,
user_id,
GetUserRatingApi { user_id },
)
.await?;
match (human_readable, format) {
(true, _) => println!("{rating}"),
(false, RatingFormat::Origin) => json_display(rating)?,
(false, RatingFormat::DxRatingNet) => {
let mut data = DxCalculatedEntries::from_user_rating_lossy(&rating.user_rating);
let mut records = data.b35;
records.append(&mut data.b15);
json_display(records)?;
}
(false, RatingFormat::DxRatingPayload) => {
let data = DxCalculatedEntries::from_user_rating_lossy(&rating.user_rating);
let payload = DxRatingNet {
calculated_entries: data,
version: DataVersion::Prism,
region: "_generic",
};
json_display(payload)?;
}
}
}
Commands::Logout { user_id, timestamp } => {
let logout: UserLogoutApiResp = Sdgb1_53::request(
&client,
APIMethod::UserLogoutApi,
user_id,
UserLogoutApi {
user_id,
login_date_time: timestamp,
..Default::default()
},
)
.await?;
if human_readable {
println!("啥都木有");
} else {
json_display(logout)?;
}
}
Commands::Preview { user_id, token } => {
let preview: GetUserPreviewApiResp = Sdgb1_53::request(
&client,
APIMethod::GetUserPreviewApi,
user_id,
GetUserPreviewApi {
user_id,
client_id: "A63E01C2805",
token,
},
)
.await?;
human_readable_display(preview, human_readable)?;
}
Commands::Ping => {
for _ in 0..10 {
let time = SystemTime::now();
match Sdgb1_53::request::<_, PingResp>(&client, APIMethod::Ping, "", Ping {}).await
{
Ok(decoded) => {
info!(
"sdgb 1.53 resp: {decoded}, {}ms",
time.elapsed().unwrap_or_default().as_millis()
);
}
Err(e) => {
error!("sdgb 1.53 error: {e}");
}
}
}
}
Commands::QRLogin { ref qrcode_content } => {
let qrcode = QRCode { qrcode_content };
let resp = qrcode.login(&client).await;
match &resp {
Ok(GetResponse { user_id, token, .. }) => {
info!("login succeed: {user_id}, {token:?}")
}
Err(e) => error!("login failed: {e}"),
}
if !human_readable {
json_display(resp)?;
}
}
Commands::AuthLite { title_ver, variant } => {
let resp = match variant {
commands::AuthLiteVariant::SDGB => delivery_raw::<SDGB>(&client, title_ver).await?,
commands::AuthLiteVariant::SDHJ => delivery_raw::<SDHJ>(&client, title_ver).await?,
};
println!("{}", String::from_utf8_lossy(&resp));
}
#[cfg(feature = "fetchall")]
Commands::ListAllUser { concurrency } => {
use crate::{cache::PLAYERS, utils::helpers::cached_concurrent_fetch};
use sdgb_api::title::methods::GetUserPreviewApiExt;
use std::io::BufRead as _;
let mut user_ids = Vec::new();
{
let mut stdin = std::io::stdin().lock();
let mut buf = String::new();
while stdin.read_line(&mut buf).is_ok_and(|size| size != 0) {
if buf.is_empty() {
continue;
}
let user_id: u32 = buf.trim().parse()?;
buf.clear();
user_ids.push(user_id);
}
}
cached_concurrent_fetch::<GetUserPreviewApiExt>(
user_ids,
&client,
concurrency,
PLAYERS,
)
.await?;
}
#[cfg(feature = "fetchall")]
Commands::ScrapeAllRecord {
concurrency,
min_rating,
max_rating,
} => {
use crate::{
cache::{PLAYERS, RECORDS},
utils::helpers::{cached_concurrent_fetch_userfn, read_cache},
};
let players: Vec<GetUserPreviewApiResp> = read_cache(PLAYERS)?;
cached_concurrent_fetch_userfn(
players
.iter()
.filter(|p| p.player_rating >= min_rating && p.player_rating <= max_rating)
.map(|p| p.user_id)
.collect::<Vec<u32>>(),
&client,
concurrency,
RECORDS,
get_user_all_music,
)
.await?;
}
#[cfg(feature = "fetchall")]
Commands::ScrapeAllB50 {
concurrency,
min_rating,
max_rating,
} => {
use sdgb_api::title::methods::GetUserRatingApiExt;
use crate::{
cache::{B50, PLAYERS},
utils::helpers::{cached_concurrent_fetch, read_cache},
};
let players: Vec<GetUserPreviewApiResp> = read_cache(PLAYERS)?;
cached_concurrent_fetch::<GetUserRatingApiExt>(
players
.iter()
.filter(|p| p.player_rating >= min_rating && p.player_rating <= max_rating)
.map(|p| p.user_id)
.collect::<Vec<u32>>(),
&client,
concurrency,
B50,
)
.await?;
}
#[cfg(feature = "fetchall")]
Commands::ScrapeAllRegion {
concurrency,
min_rating,
max_rating,
} => {
use sdgb_api::title::methods::GetUserRegionApiExt;
use crate::{
cache::{PLAYERS, REGIONS},
utils::helpers::{cached_concurrent_fetch, read_cache},
};
let players: Vec<GetUserPreviewApiResp> = read_cache(PLAYERS)?;
cached_concurrent_fetch::<GetUserRegionApiExt>(
players
.iter()
.filter(|p| p.player_rating >= min_rating && p.player_rating <= max_rating)
.map(|p| p.user_id)
.collect::<Vec<u32>>(),
&client,
concurrency,
REGIONS,
)
.await?;
}
#[cfg(feature = "fetchall")]
Commands::ListAllUserDump {} => {
use crate::{
cache::PLAYERS,
utils::helpers::{dump_parquet, read_cache},
};
let players: Vec<GetUserPreviewApiResp> = read_cache(PLAYERS)?;
dump_parquet(players, "players.parquet")?;
}
#[cfg(feature = "fetchall")]
Commands::ScrapeAllRegionDump {} => {
use crate::{
cache::REGIONS,
utils::helpers::{dump_parquet, read_cache},
};
use sdgb_api::title::model::{GetUserRegionApiResp, UserRegionFlatten};
let regions: Vec<GetUserRegionApiResp> = read_cache(REGIONS)?;
let regions_flat = regions
.into_iter()
.map(Vec::<UserRegionFlatten>::from)
.flatten()
.collect::<Vec<_>>();
dump_parquet(regions_flat, "regions.parquet")?;
}
#[cfg(feature = "fetchall")]
Commands::ScrapeAllRecordDump {} => {
use crate::{
cache::RECORDS,
utils::helpers::{dump_parquet, read_cache},
};
use sdgb_api::title::model::GetUserMusicApiResp;
use sdgb_api::title::model::UserMusicDetailFlatten;
let records: Vec<GetUserMusicApiResp> = read_cache(RECORDS)?;
dump_parquet(
records
.into_iter()
.map(|resp| {
resp.user_music_list
.into_iter()
.map(|music| music.user_music_detail_list)
.flatten()
.map(move |detail| UserMusicDetailFlatten::new(resp.user_id, detail))
})
.flatten()
.collect::<Vec<UserMusicDetailFlatten>>(),
"records.parquet",
)?;
}
#[cfg(feature = "fetchall")]
Commands::ScrapeAllB50Dump {} => {
use sdgb_api::title::model::{MusicRating, MusicRatingFlatten};
use crate::{
cache::B50,
utils::helpers::{dump_parquet, read_cache},
};
let records: Vec<GetUserRatingApiResp> = read_cache(B50)?;
dump_parquet::<MusicRatingFlatten>(
records
.into_iter()
.map(
|GetUserRatingApiResp {
user_id,
user_rating,
}| {
user_rating
.rating_list
.into_iter()
.chain(user_rating.next_rating_list)
.filter_map(
move |MusicRating {
music_id,
level,
rom_version,
achievement,
}| {
let (_rank, dx_rating) =
music_db::query_music_level(music_id, level)?
.dx_rating(achievement);
Some(MusicRatingFlatten {
user_id,
music_id,
level,
rom_version,
achievement,
dx_rating,
})
},
)
},
)
.flatten()
.collect::<Vec<_>>(),
"b50.parquet",
)?;
}
Commands::Userdata {
user_id,
skip_login,
token,
} => {
let action = async |_| match Sdgb1_53::request::<_, GetUserDataApiResp>(
&client,
APIMethod::GetUserDataApi,
user_id,
GetUserDataApi { user_id },
)
.await
{
Ok(udata) => {
info!("{udata:#?}");
}
Err(e) => {
error!("failed to get userdata: {e}");
}
};
// userdata does not require loginResult
if skip_login {
action(UserLoginApiResp::default()).await;
} else {
login_action(&client, user_id, token, action).await?;
}
}
}
Ok(())
}
#[cfg(all(feature = "compio", feature = "tokio"))]
compile_error!("you must not enable both `compio` and `tokio`");