feat: initial support for GetUserRating

This commit is contained in:
mokurin000
2025-08-01 01:15:13 +08:00
parent 0b8de2b4bc
commit 9b046036c9
12 changed files with 139 additions and 15 deletions

1
Cargo.lock generated
View File

@@ -1478,6 +1478,7 @@ dependencies = [
"palc", "palc",
"redb", "redb",
"sdgb-api", "sdgb-api",
"serde",
"serde_json", "serde_json",
"snafu", "snafu",
"spdlog-rs", "spdlog-rs",

View File

@@ -12,6 +12,7 @@ spdlog-rs = { version = "0.4.3", default-features = false, features = [
] } ] }
snafu = { version = "0.8.6", features = ["backtrace", "rust_1_81"] } snafu = { version = "0.8.6", features = ["backtrace", "rust_1_81"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141" serde_json = "1.0.141"
strum = { version = "0.27.2", features = ["derive"] } strum = { version = "0.27.2", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# sdgb-utils-rs
- SBGA 舞萌DX API 文档参考
- “裸” cli 工具,没多少人性化功能
- 暂时不完整开放,留在私仓

View File

@@ -19,6 +19,9 @@ tokio = { workspace = true, optional = true }
compio = { workspace = true, optional = true } compio = { workspace = true, optional = true }
spdlog-rs = { workspace = true } spdlog-rs = { workspace = true }
# (de)serialization
serde = { workspace = true }
# hashing # hashing
digest = "0.10.7" digest = "0.10.7"
hmac-sha256 = { version = "1.1.12", features = ["digest010", "traits010"] } hmac-sha256 = { version = "1.1.12", features = ["digest010", "traits010"] }
@@ -30,8 +33,6 @@ chrono = "0.4.41"
# network request # network request
nyquest = { version = "0.2.0", features = ["async", "json"] } nyquest = { version = "0.2.0", features = ["async", "json"] }
# (de)serialization
serde = { version = "1.0.219", features = ["derive"] }
# compression / encryption # compression / encryption
flate2 = "1.1.2" flate2 = "1.1.2"

View File

@@ -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<MusicDB> = LazyLock::new(|| unimplemented!());

View File

@@ -2,6 +2,8 @@ pub mod all_net;
pub mod auth_lite; pub mod auth_lite;
pub mod title; pub mod title;
pub mod helper;
mod error; mod error;
pub use error::ApiError; pub use error::ApiError;

View File

@@ -1,6 +1,10 @@
use std::fmt::Display;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use crate::helper::level_name;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GetUserRatingApi { pub struct GetUserRatingApi {
@@ -17,21 +21,22 @@ pub struct GetUserRatingApiResp {
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UserRating { pub struct UserRating {
/// total rating, now it's 0
pub rating: i64, pub rating: i64,
/// b35 /// b35
pub rating_list: Vec<RatingList>, pub rating_list: Vec<MusicRating>,
/// b15 /// b15
pub new_rating_list: Vec<RatingList>, pub new_rating_list: Vec<MusicRating>,
/// 候补 b35 /// 候补 b35
pub next_rating_list: Vec<RatingList>, pub next_rating_list: Vec<MusicRating>,
/// 候补 b15 /// 候补 b15
pub next_new_rating_list: Vec<RatingList>, pub next_new_rating_list: Vec<MusicRating>,
pub udemae: Udemae, pub udemae: Udemae,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RatingList { pub struct MusicRating {
/// Maimai music id /// Maimai music id
pub music_id: u32, pub music_id: u32,
/// difficulty /// difficulty
@@ -83,3 +88,43 @@ pub struct Udemae {
#[serde(rename = "NpcLoseNum")] #[serde(rename = "NpcLoseNum")]
pub npc_lose_num2: i64, 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(())
}
}

View File

@@ -11,7 +11,9 @@ pub struct UserLoginApi {
pub acsess_code: String, pub acsess_code: String,
pub place_id: String, pub place_id: String,
pub client_id: String, pub client_id: String,
/// set to `false` is fine /// false 的情况,二维码扫描后半小时可登录。
///
/// true 的情况,可延长至二维码扫描后的两小时可登录。
pub is_continue: bool, pub is_continue: bool,
/// fixed to 0 /// fixed to 0
pub generic_flag: u8, pub generic_flag: u8,

View File

@@ -16,11 +16,20 @@ fetchall = ["dep:redb", "dep:futures-util"]
[dependencies] [dependencies]
sdgb-api = { workspace = true, features = ["bincode"] } sdgb-api = { workspace = true, features = ["bincode"] }
spdlog-rs = { workspace = true } # (de)serialization
snafu = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
# logging / errors
spdlog-rs = { workspace = true }
snafu = { workspace = true }
# kv database
redb = { workspace = true, optional = true } redb = { workspace = true, optional = true }
# async runtime
tokio = { workspace = true, features = ["macros"], optional = true } tokio = { workspace = true, features = ["macros"], optional = true }
compio = { workspace = true, features = ["macros"], optional = true } compio = { workspace = true, features = ["macros"], optional = true }

View File

@@ -5,6 +5,9 @@ use strum::EnumString;
#[derive(Parser)] #[derive(Parser)]
#[command(about = "SDGB api tool", long_about = env!("CARGO_PKG_DESCRIPTION"))] #[command(about = "SDGB api tool", long_about = env!("CARGO_PKG_DESCRIPTION"))]
pub struct Cli { pub struct Cli {
/// try to generate human readable output.
#[arg(short = 'M', long)]
pub machine_readable: bool,
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, pub command: Commands,
} }
@@ -37,6 +40,10 @@ pub enum Commands {
#[arg(short, long)] #[arg(short, long)]
user_id: u32, user_id: u32,
}, },
Rating {
#[arg(short, long)]
user_id: u32,
},
// below requires login // below requires login
Userdata { Userdata {

View File

@@ -11,14 +11,18 @@ use sdgb_api::{
MaiVersionExt, Sdgb1_40, Sdgb1_50, MaiVersionExt, Sdgb1_40, Sdgb1_50,
methods::APIMethod, methods::APIMethod,
model::{ model::{
GetUserDataApi, GetUserDataApiResp, GetUserPreviewApi, GetUserPreviewApiResp, Ping, GetUserDataApi, GetUserDataApiResp, GetUserPreviewApi, GetUserPreviewApiResp,
PingResp, UserLogoutApi, UserLogoutApiResp, GetUserRatingApi, GetUserRatingApiResp, Ping, PingResp, UserLogoutApi,
UserLogoutApiResp,
}, },
}, },
}; };
use spdlog::{error, info, warn}; 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")] #[cfg(feature = "fetchall")]
mod cache; mod cache;
@@ -48,12 +52,27 @@ async fn main() -> Result<(), Box<dyn snafu::Error>> {
} }
})?; })?;
let Cli { command } = <Cli as Parser>::parse(); let Cli {
command,
machine_readable,
} = <Cli as Parser>::parse();
let human_readable = !machine_readable;
let client = ClientBuilder::default().build_async().await?; let client = ClientBuilder::default().build_async().await?;
// TODO: refactor via enum_dispatch // TODO: refactor via enum_dispatch
match command { 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 } => { commands::Commands::Logout { user_id, timestamp } => {
let logout: UserLogoutApiResp = Sdgb1_50::request( let logout: UserLogoutApiResp = Sdgb1_50::request(
&client, &client,
@@ -77,7 +96,7 @@ async fn main() -> Result<(), Box<dyn snafu::Error>> {
) )
.await?; .await?;
println!("{preview}"); human_readable_display(preview, human_readable)?;
} }
commands::Commands::Ping => { commands::Commands::Ping => {
let decoded: PingResp = Sdgb1_40::request( let decoded: PingResp = Sdgb1_40::request(

View File

@@ -1,3 +1,5 @@
use std::{fmt::Display, io::stdout};
use nyquest_preset::nyquest::AsyncClient; use nyquest_preset::nyquest::AsyncClient;
use sdgb_api::{ use sdgb_api::{
ApiError, ApiError,
@@ -7,6 +9,7 @@ use sdgb_api::{
model::{UserLoginApi, UserLoginApiResp, UserLogoutApi, UserLogoutApiResp}, model::{UserLoginApi, UserLoginApiResp, UserLogoutApi, UserLogoutApiResp},
}, },
}; };
use serde::Serialize;
use spdlog::info; use spdlog::info;
pub async fn login_action<R>( pub async fn login_action<R>(
@@ -44,3 +47,17 @@ pub async fn login_action<R>(
info!("logout: {logout_resp:?}"); info!("logout: {logout_resp:?}");
Ok(return_data) Ok(return_data)
} }
pub fn human_readable_display(
value: impl Display + Serialize,
human_readable: bool,
) -> Result<(), Box<dyn snafu::Error>> {
if human_readable {
println!("{value}");
} else {
let lock = stdout().lock();
serde_json::to_writer_pretty(lock, &value)?;
}
Ok(())
}