feat: support dxratingnet format

This commit is contained in:
mokurin000
2025-08-01 18:16:16 +08:00
parent 953feee4c4
commit 4e07eaf2e0
6 changed files with 201 additions and 6 deletions

View File

@@ -0,0 +1,67 @@
use crate::{
helper::MUSIC_DB,
title::model::get_user_rating_api::{
MusicRating, UserRating,
dxrating::{DxCalculatedEntries, DxLevelName, DxMusicRecord, DxSheetId},
},
};
impl DxCalculatedEntries {
pub fn from_user_rating_lossy(rating: &UserRating) -> DxCalculatedEntries {
let b35 = rating
.rating_list
.iter()
.map(DxMusicRecord::try_from)
.flatten()
.collect();
let b15 = rating
.new_rating_list
.iter()
.map(DxMusicRecord::try_from)
.flatten()
.collect();
DxCalculatedEntries { b35, b15 }
}
}
impl TryFrom<u32> for DxLevelName {
type Error = ConversionError;
fn try_from(level: u32) -> Result<Self, Self::Error> {
Self::from_repr(level).ok_or(ConversionError::UnknownDifficulty { level })
}
}
impl TryFrom<&MusicRating> for DxMusicRecord {
type Error = ConversionError;
fn try_from(value: &MusicRating) -> Result<Self, Self::Error> {
let music_title = MUSIC_DB
.as_ref()
.and_then(|db| db.get(&value.music_id))
.map(|info| info.name.clone())
.ok_or(ConversionError::MusicNotInDB)?;
Ok(Self {
sheet_id: DxSheetId {
music_title,
level: DxLevelName::try_from(value.level)?,
dx_version: value.music_id >= 1000,
},
achievement_rate: (value.achievement as f64) / 10000.0,
})
}
}
#[derive(Debug, snafu::Snafu)]
pub enum ConversionError {
#[snafu(display("Music was not found in database"))]
MusicNotInDB,
#[snafu(display("Utage difficulty was disallowed"))]
UtageDifficulty,
#[snafu(display("Unknown difficulty: {level}"))]
UnknownDifficulty { level: u32 },
}

View File

@@ -0,0 +1,95 @@
use serde::Serialize;
/// Full payload for image generate api
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DxRatingNet {
pub calculated_entries: DxCalculatedEntries,
pub version: DataVersion,
/// use `_generic`
pub region: &'static str,
}
/// Export/Import format
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DxCalculatedEntries {
pub b35: Vec<DxMusicRecord>,
pub b15: Vec<DxMusicRecord>,
}
/// full music record
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DxMusicRecord {
pub sheet_id: DxSheetId,
pub achievement_rate: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DxSheetId {
pub music_title: String,
pub dx_version: bool,
pub level: DxLevelName,
}
#[derive(Debug, Clone, Copy, PartialEq, strum::IntoStaticStr, strum::FromRepr)]
#[strum(serialize_all = "lowercase")]
#[repr(u32)]
pub enum DxLevelName {
Basic,
Advanced,
Expert,
Master,
ReMaster,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DataVersion {
Buddies,
BuddiesPlus,
Prism,
PrismPlus,
}
impl Serialize for DataVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(match self {
DataVersion::Buddies => "BUDDiES",
DataVersion::BuddiesPlus => "BUDDiES PLUS",
DataVersion::Prism => "PRiSM",
DataVersion::PrismPlus => "PRiSM PLUS",
})
}
}
impl Serialize for DxSheetId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl ToString for DxSheetId {
fn to_string(&self) -> String {
let mut output = self.music_title.clone();
if self.dx_version {
output += "__dxrt__dx__dxrt__"
} else {
output += "__dxrt__std__dxrt__"
}
output += self.level.into();
output
}
}
mod conversion;

View File

@@ -168,3 +168,5 @@ impl MusicRating {
)
}
}
pub mod dxrating;

View File

@@ -14,4 +14,11 @@ mod get_user_data_api;
pub use get_user_data_api::{GetUserDataApi, GetUserDataApiResp};
mod get_user_rating_api;
pub use get_user_rating_api::dxrating::{
DxCalculatedEntries, // entries
DxLevelName, // level name
DxMusicRecord,
DxRatingNet,
DxSheetId,
};
pub use get_user_rating_api::{GetUserRatingApi, GetUserRatingApiResp};

View File

@@ -43,6 +43,13 @@ pub enum Commands {
Rating {
#[arg(short, long)]
user_id: u32,
/// JSON format.
///
/// - `origin`: official json response
/// - `dx_rating_net`: DxRatingNet Format
#[arg(short, long, default_value_t = RatingFormat::default())]
format: RatingFormat,
},
// below requires login
@@ -71,3 +78,14 @@ pub enum Commands {
timestamp: u64,
},
}
#[derive(Default, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum RatingFormat {
#[default]
/// Official API response
Origin,
/// dxrating.net format
DxRatingNet,
}

View File

@@ -15,16 +15,16 @@ use sdgb_api::{
MaiVersionExt, Sdgb1_40, Sdgb1_50,
methods::APIMethod,
model::{
GetUserDataApi, GetUserDataApiResp, GetUserPreviewApi, GetUserPreviewApiResp,
GetUserRatingApi, GetUserRatingApiResp, Ping, PingResp, UserLogoutApi,
UserLogoutApiResp,
DxCalculatedEntries, GetUserDataApi, GetUserDataApiResp, GetUserPreviewApi,
GetUserPreviewApiResp, GetUserRatingApi, GetUserRatingApiResp, Ping, PingResp,
UserLogoutApi, UserLogoutApiResp,
},
},
};
use spdlog::{error, info, warn};
use crate::{
commands::Cli,
commands::{Cli, RatingFormat},
utils::{human_readable_display, json_display, login_action},
};
@@ -77,7 +77,7 @@ async fn main() -> Result<(), Box<dyn snafu::Error>> {
// TODO: refactor via enum_dispatch
match command {
commands::Commands::Rating { user_id } => {
commands::Commands::Rating { user_id, format } => {
let rating: GetUserRatingApiResp = Sdgb1_50::request(
&client,
APIMethod::GetUserRatingApi,
@@ -86,7 +86,13 @@ async fn main() -> Result<(), Box<dyn snafu::Error>> {
)
.await?;
human_readable_display(rating, human_readable)?;
match (human_readable, format) {
(true, _) => println!("{rating}"),
(false, RatingFormat::Origin) => json_display(rating)?,
(false, RatingFormat::DxRatingNet) => json_display(
DxCalculatedEntries::from_user_rating_lossy(&rating.user_rating),
)?,
}
}
commands::Commands::Logout { user_id, timestamp } => {
let logout: UserLogoutApiResp = Sdgb1_50::request(