diff --git a/sdgb-api/src/title/model/get_user_rating_api/dxrating/conversion/mod.rs b/sdgb-api/src/title/model/get_user_rating_api/dxrating/conversion/mod.rs new file mode 100644 index 0000000..9e6b225 --- /dev/null +++ b/sdgb-api/src/title/model/get_user_rating_api/dxrating/conversion/mod.rs @@ -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 for DxLevelName { + type Error = ConversionError; + + fn try_from(level: u32) -> Result { + Self::from_repr(level).ok_or(ConversionError::UnknownDifficulty { level }) + } +} + +impl TryFrom<&MusicRating> for DxMusicRecord { + type Error = ConversionError; + + fn try_from(value: &MusicRating) -> Result { + 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 }, +} diff --git a/sdgb-api/src/title/model/get_user_rating_api/dxrating/mod.rs b/sdgb-api/src/title/model/get_user_rating_api/dxrating/mod.rs new file mode 100644 index 0000000..1b1e78b --- /dev/null +++ b/sdgb-api/src/title/model/get_user_rating_api/dxrating/mod.rs @@ -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, + pub b15: Vec, +} + +/// 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(&self, serializer: S) -> Result + 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(&self, serializer: S) -> Result + 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; 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 dc08583..fdf4744 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 @@ -168,3 +168,5 @@ impl MusicRating { ) } } + +pub mod dxrating; diff --git a/sdgb-api/src/title/model/mod.rs b/sdgb-api/src/title/model/mod.rs index fa5266f..4e5b079 100644 --- a/sdgb-api/src/title/model/mod.rs +++ b/sdgb-api/src/title/model/mod.rs @@ -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}; diff --git a/sdgb-cli/src/commands.rs b/sdgb-cli/src/commands.rs index 6cf3f19..896e810 100644 --- a/sdgb-cli/src/commands.rs +++ b/sdgb-cli/src/commands.rs @@ -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, +} diff --git a/sdgb-cli/src/main.rs b/sdgb-cli/src/main.rs index 7816155..fff73bc 100644 --- a/sdgb-cli/src/main.rs +++ b/sdgb-cli/src/main.rs @@ -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> { // 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> { ) .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(