From 9b046036c9e12e482833b5336886c18c0780c3ad Mon Sep 17 00:00:00 2001 From: mokurin000 <1348292515a@gmail.com> Date: Fri, 1 Aug 2025 01:15:13 +0800 Subject: [PATCH] feat: initial support for GetUserRating --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 5 ++ sdgb-api/Cargo.toml | 5 +- sdgb-api/src/helper/mod.rs | 15 +++++ sdgb-api/src/lib.rs | 2 + .../title/model/get_user_rating_api/mod.rs | 55 +++++++++++++++++-- .../src/title/model/user_login_api/mod.rs | 4 +- sdgb-cli/Cargo.toml | 13 ++++- sdgb-cli/src/commands.rs | 7 +++ sdgb-cli/src/main.rs | 29 ++++++++-- sdgb-cli/src/utils/mod.rs | 17 ++++++ 12 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 README.md create mode 100644 sdgb-api/src/helper/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 2c32ed1..ea72f99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,6 +1478,7 @@ dependencies = [ "palc", "redb", "sdgb-api", + "serde", "serde_json", "snafu", "spdlog-rs", diff --git a/Cargo.toml b/Cargo.toml index 72344e1..e7987ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ spdlog-rs = { version = "0.4.3", default-features = false, features = [ ] } snafu = { version = "0.8.6", features = ["backtrace", "rust_1_81"] } +serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.141" strum = { version = "0.27.2", features = ["derive"] } tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e023b6 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# sdgb-utils-rs + +- SBGA 舞萌DX API 文档参考 +- “裸” cli 工具,没多少人性化功能 +- 暂时不完整开放,留在私仓 diff --git a/sdgb-api/Cargo.toml b/sdgb-api/Cargo.toml index 3a76a81..6eeef08 100644 --- a/sdgb-api/Cargo.toml +++ b/sdgb-api/Cargo.toml @@ -19,6 +19,9 @@ tokio = { workspace = true, optional = true } compio = { workspace = true, optional = true } spdlog-rs = { workspace = true } +# (de)serialization +serde = { workspace = true } + # hashing digest = "0.10.7" hmac-sha256 = { version = "1.1.12", features = ["digest010", "traits010"] } @@ -30,8 +33,6 @@ chrono = "0.4.41" # network request nyquest = { version = "0.2.0", features = ["async", "json"] } -# (de)serialization -serde = { version = "1.0.219", features = ["derive"] } # compression / encryption flate2 = "1.1.2" diff --git a/sdgb-api/src/helper/mod.rs b/sdgb-api/src/helper/mod.rs new file mode 100644 index 0000000..c3414c4 --- /dev/null +++ b/sdgb-api/src/helper/mod.rs @@ -0,0 +1,15 @@ +pub fn level_name(level: u32) -> &'static str { + match level { + 0 => "BASIC", + 1 => "ADVANCED", + 2 => "EXPERT", + 3 => "MASTER", + 4 => "RE: MASTER", + 5 => "UTAGE", + _ => "Unknown", + } +} + +// TODO: MusicDB lazy load +// struct MusicDB; +// static MUSIC_DB: LazyLock = LazyLock::new(|| unimplemented!()); diff --git a/sdgb-api/src/lib.rs b/sdgb-api/src/lib.rs index d674f57..36d0422 100644 --- a/sdgb-api/src/lib.rs +++ b/sdgb-api/src/lib.rs @@ -2,6 +2,8 @@ pub mod all_net; pub mod auth_lite; pub mod title; +pub mod helper; + mod error; pub use error::ApiError; diff --git a/sdgb-api/src/title/model/get_user_rating_api/mod.rs b/sdgb-api/src/title/model/get_user_rating_api/mod.rs index 1bd0a80..15aa33c 100644 --- a/sdgb-api/src/title/model/get_user_rating_api/mod.rs +++ b/sdgb-api/src/title/model/get_user_rating_api/mod.rs @@ -1,6 +1,10 @@ +use std::fmt::Display; + use serde::Deserialize; use serde::Serialize; +use crate::helper::level_name; + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetUserRatingApi { @@ -17,21 +21,22 @@ pub struct GetUserRatingApiResp { #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UserRating { + /// total rating, now it's 0 pub rating: i64, /// b35 - pub rating_list: Vec, + pub rating_list: Vec, /// b15 - pub new_rating_list: Vec, + pub new_rating_list: Vec, /// 候补 b35 - pub next_rating_list: Vec, + pub next_rating_list: Vec, /// 候补 b15 - pub next_new_rating_list: Vec, + pub next_new_rating_list: Vec, pub udemae: Udemae, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RatingList { +pub struct MusicRating { /// Maimai music id pub music_id: u32, /// difficulty @@ -83,3 +88,43 @@ pub struct Udemae { #[serde(rename = "NpcLoseNum")] pub npc_lose_num2: i64, } + +impl Display for GetUserRatingApiResp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("用户ID: {}\n", self.user_id))?; + + f.write_str("\n---------- B35 ----------\n")?; + for b35 in &self.user_rating.rating_list { + f.write_fmt(format_args!("{b35}\n---\n"))?; + } + + f.write_str("\n---------- B15 ----------\n")?; + for b15 in &self.user_rating.new_rating_list { + f.write_fmt(format_args!("{b15}\n---\n"))?; + } + Ok(()) + } +} + +impl Display for MusicRating { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("歌曲ID: \t{}\n", self.music_id))?; + f.write_fmt(format_args!( + "谱面版本: \t{}\n", + match (self.music_id / 10000) % 10 { + 0 => "SD", + 1 => "DX", + _ => "宴", + } + ))?; + + f.write_fmt(format_args!("游玩难度: \t{}\n", level_name(self.level)))?; + f.write_fmt(format_args!( + "达成率: \t{}.{}%", + self.achievement / 10000, + self.achievement % 10000 + ))?; + + Ok(()) + } +} diff --git a/sdgb-api/src/title/model/user_login_api/mod.rs b/sdgb-api/src/title/model/user_login_api/mod.rs index e401a96..e135957 100644 --- a/sdgb-api/src/title/model/user_login_api/mod.rs +++ b/sdgb-api/src/title/model/user_login_api/mod.rs @@ -11,7 +11,9 @@ pub struct UserLoginApi { pub acsess_code: String, pub place_id: String, pub client_id: String, - /// set to `false` is fine + /// false 的情况,二维码扫描后半小时可登录。 + /// + /// true 的情况,可延长至二维码扫描后的两小时可登录。 pub is_continue: bool, /// fixed to 0 pub generic_flag: u8, diff --git a/sdgb-cli/Cargo.toml b/sdgb-cli/Cargo.toml index 46ec120..662717d 100644 --- a/sdgb-cli/Cargo.toml +++ b/sdgb-cli/Cargo.toml @@ -16,11 +16,20 @@ fetchall = ["dep:redb", "dep:futures-util"] [dependencies] sdgb-api = { workspace = true, features = ["bincode"] } -spdlog-rs = { workspace = true } -snafu = { workspace = true } +# (de)serialization +serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true } + +# logging / errors +spdlog-rs = { workspace = true } +snafu = { workspace = true } + + +# kv database redb = { workspace = true, optional = true } + +# async runtime tokio = { workspace = true, features = ["macros"], optional = true } compio = { workspace = true, features = ["macros"], optional = true } diff --git a/sdgb-cli/src/commands.rs b/sdgb-cli/src/commands.rs index 4122fb2..6cf3f19 100644 --- a/sdgb-cli/src/commands.rs +++ b/sdgb-cli/src/commands.rs @@ -5,6 +5,9 @@ use strum::EnumString; #[derive(Parser)] #[command(about = "SDGB api tool", long_about = env!("CARGO_PKG_DESCRIPTION"))] pub struct Cli { + /// try to generate human readable output. + #[arg(short = 'M', long)] + pub machine_readable: bool, #[command(subcommand)] pub command: Commands, } @@ -37,6 +40,10 @@ pub enum Commands { #[arg(short, long)] user_id: u32, }, + Rating { + #[arg(short, long)] + user_id: u32, + }, // below requires login Userdata { diff --git a/sdgb-cli/src/main.rs b/sdgb-cli/src/main.rs index f0f93dc..3843d04 100644 --- a/sdgb-cli/src/main.rs +++ b/sdgb-cli/src/main.rs @@ -11,14 +11,18 @@ use sdgb_api::{ MaiVersionExt, Sdgb1_40, Sdgb1_50, methods::APIMethod, model::{ - GetUserDataApi, GetUserDataApiResp, GetUserPreviewApi, GetUserPreviewApiResp, Ping, - PingResp, UserLogoutApi, UserLogoutApiResp, + GetUserDataApi, GetUserDataApiResp, GetUserPreviewApi, GetUserPreviewApiResp, + GetUserRatingApi, GetUserRatingApiResp, Ping, PingResp, UserLogoutApi, + UserLogoutApiResp, }, }, }; use spdlog::{error, info, warn}; -use crate::{commands::Cli, utils::login_action}; +use crate::{ + commands::Cli, + utils::{human_readable_display, login_action}, +}; #[cfg(feature = "fetchall")] mod cache; @@ -48,12 +52,27 @@ async fn main() -> Result<(), Box> { } })?; - let Cli { command } = ::parse(); + let Cli { + command, + machine_readable, + } = ::parse(); + let human_readable = !machine_readable; let client = ClientBuilder::default().build_async().await?; // TODO: refactor via enum_dispatch match command { + commands::Commands::Rating { user_id } => { + let rating: GetUserRatingApiResp = Sdgb1_50::request( + &client, + APIMethod::GetUserRatingApi, + user_id, + GetUserRatingApi { user_id }, + ) + .await?; + + human_readable_display(rating, human_readable)?; + } commands::Commands::Logout { user_id, timestamp } => { let logout: UserLogoutApiResp = Sdgb1_50::request( &client, @@ -77,7 +96,7 @@ async fn main() -> Result<(), Box> { ) .await?; - println!("{preview}"); + human_readable_display(preview, human_readable)?; } commands::Commands::Ping => { let decoded: PingResp = Sdgb1_40::request( diff --git a/sdgb-cli/src/utils/mod.rs b/sdgb-cli/src/utils/mod.rs index d10e9f1..01171b7 100644 --- a/sdgb-cli/src/utils/mod.rs +++ b/sdgb-cli/src/utils/mod.rs @@ -1,3 +1,5 @@ +use std::{fmt::Display, io::stdout}; + use nyquest_preset::nyquest::AsyncClient; use sdgb_api::{ ApiError, @@ -7,6 +9,7 @@ use sdgb_api::{ model::{UserLoginApi, UserLoginApiResp, UserLogoutApi, UserLogoutApiResp}, }, }; +use serde::Serialize; use spdlog::info; pub async fn login_action( @@ -44,3 +47,17 @@ pub async fn login_action( info!("logout: {logout_resp:?}"); Ok(return_data) } + +pub fn human_readable_display( + value: impl Display + Serialize, + human_readable: bool, +) -> Result<(), Box> { + if human_readable { + println!("{value}"); + } else { + let lock = stdout().lock(); + serde_json::to_writer_pretty(lock, &value)?; + } + + Ok(()) +}