[feature-107] use size optimized song details cache

This commit is contained in:
MathieuG-P
2024-04-20 16:22:07 +02:00
parent 6836f18e89
commit 0cea1cd89f
24 changed files with 565 additions and 200 deletions
+51 -5
View File
@@ -1,4 +1,4 @@
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache.model";
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
export interface BsvMapDetail {
automapper: boolean;
@@ -142,11 +142,57 @@ export interface BsvMapParitySummary {
warns: number;
}
export type BsvMapCharacteristic = "Standard" | "OneSaber" | "NoArrows" | "90Degree" | "360Degree" | "Lightshow" | "Lawless";
export type BsvMapDifficultyType = "Easy" | "Normal" | "Hard" | "Expert" | "ExpertPlus";
export enum MapStyle {
Dance = "dance",
Swing = "swing",
Nightcore = "nightcore",
Folk = "folk",
Family = "family",
Ambient = "ambient",
Funk = "funk",
Jazz = "jazz",
Soul = "soul",
Speedcore = "speedcore",
Punk = "punk",
Rb = "rb",
Holiday = "holiday",
Vocaloid = "vocaloid",
JRock = "j-rock",
Trance = "trance",
DrumBass = "drumbass",
Comedy = "comedy",
Instrumental = "instrumental",
Hardcore = "hardcore",
KPop = "k-pop",
Indie = "indie",
Techno = "techno",
House = "house",
Game = "game",
Film = "film",
Alt = "alt",
Dubstep = "dubstep",
Metal = "metal",
Anime = "anime",
Hiphop = "hiphop",
JPop = "j-pop",
Rock = "rock",
Pop = "pop",
Electronic = "electronic",
ClassicalOrchestral = "classical-orchestral"
}
export type MapStyle = "dance" | "swing" | "nightcore" | "folk" | "family" | "ambient" | "funk" | "jazz" | "soul" | "speedcore" | "punk" | "rb" | "holiday" | "vocaloid" | "j-rock" | "trance" | "drumbass" | "comedy" | "instrumental" | "hardcore" | "k-pop" | "indie" | "techno" | "house" | "game" | "film" | "alt" | "dubstep" | "metal" | "anime" | "hiphop" | "j-pop" | "rock" | "pop" | "electronic" | "classical-orchestral";
export type MapType = "accuracy" | "balanced" | "challenge" | "dancestyle" | "fitness" | "speed" | "tech";
// MapType Enum
export enum MapType {
Accuracy = "accuracy",
Balanced = "balanced",
Challenge = "challenge",
Dancestyle = "dancestyle",
Fitness = "fitness",
Speed = "speed",
Tech = "tech"
}
export const MapTags = { ...MapStyle, ...MapType };
export type MapTag = MapStyle | MapType;
export type MapRequirement = "chroma" | "noodle" | "me" | "cinema";
@@ -1,5 +1,5 @@
import { RawMapInfoData } from "./raw-map.model";
import { SongDetails } from "./song-details-cache.model";
import { SongDetails } from "./song-details-cache/song-details-cache.model";
export interface BsmLocalMap {
hash: string;
+1 -1
View File
@@ -1,3 +1,3 @@
export { RawMapInfoData, RawMapDifficulty, RawDifficultySet } from "./raw-map.model";
export { BsvInstant, BsvMapDetail, BsvMapDetailMetadata, BsvMapDifficulty, BsvMapParitySummary, BsvMapStats, BsvMapTestplay, BsvMapVersion, BsvUserDetail } from "./beat-saver.model";
export { SongDetailsCache, SongDetails, Difficulty, MapDetailMetadata, Uploader, SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache.model";
export { SongDetails, SongDifficulty, SongUploader, SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
+1 -1
View File
@@ -1,4 +1,4 @@
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache.model";
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
export interface RawMapInfoData<T = unknown> {
_version: string;
@@ -1,82 +0,0 @@
export interface SongDetailsCache {
songs: SongDetails[];
lastUpdated: number; // int64 in protobuf is represented as number in TypeScript, but consider using BigInt for precise representation
total: number; // uint32
}
export interface SongDetails {
id: string;
hash: string;
name: string;
metadata: MapDetailMetadata;
uploader: Uploader;
uploadedAt: number; // int32
tags: string[];
bpm: number; // float
ranked: boolean;
qualified: boolean;
curated: boolean;
rankedBL: boolean;
nominatedBL: boolean;
qualifiedBL: boolean;
upVotes: number; // int32
downVotes: number; // int32
downloads: number; // int32
duration: number; // int32
automapper: boolean;
difficulties: Difficulty[];
}
export interface Difficulty {
difficulty: SongDiffName;
characteristic: SongDetailDiffCharactertistic;
label: string;
stars: number; // float
starsBL: number; // float
njs: number; // float
nps: number; // float
offset: number; // float
chroma: boolean;
cinema: boolean;
me: boolean;
ne: boolean;
bombs: number; // int32
notes: number; // int32
obstacles: number; // int32
}
export interface Uploader {
name: string;
id: number; // int32
verified: boolean;
}
export interface MapDetailMetadata {
bpm: number; // float
duration: number; // int32
levelAuthorName: string;
songAuthorName: string;
songName: string;
songSubName: string;
}
export enum SongDiffName {
Easy = "Easy",
Normal = "Normal",
Hard = "Hard",
Expert = "Expert",
ExpertPlus = "ExpertPlus",
}
export enum SongDetailDiffCharactertistic {
Standard = "Standard",
OneSaber = "OneSaber",
NoArrows = "NoArrows",
Lawless = "Lawless",
Lightshow = "Lightshow",
Legacy = "Legacy",
_90Degree = "90Degree",
_360Degree = "360Degree",
}
@@ -0,0 +1,125 @@
// SongDetailsCache Message
export interface RawSongDetailsCache {
songs: RawSongDetails[];
lastUpdated: number;
total: number;
}
// SongDetails Message
export interface RawSongDetails {
idInt: number;
hash: string;
duration: number;
uploader: RawUploader;
uploadedAt: number;
tags: RawMapTag[];
ranked: boolean;
qualified: boolean;
curated: boolean;
rankedBL: boolean;
nominatedBL: boolean;
qualifiedBL: boolean;
upVotes: number;
downVotes: number;
downloads: number;
automapper: boolean;
difficulties: RawDifficulty[];
}
// Difficulty Message
export interface RawDifficulty {
difficulty: RawDifficultyLabel;
characteristic: RawDifficultyCharacteristic;
label: string;
starsT100: number;
starsBlT100: number;
njsT100: number;
npsT100: number;
offsetT100: number;
chroma: boolean;
cinema: boolean;
me: boolean;
ne: boolean;
bombs: number;
notes: number;
obstacles: number;
}
// Uploader Message
export interface RawUploader {
name: string;
id: number;
verified: boolean;
}
// DifficultyLabel Enum
export enum RawDifficultyLabel {
UNKNOWN_LABEL,
EASY,
NORMAL,
HARD,
EXPERT,
EXPERT_PLUS
}
// DifficultyCharacteristic Enum
export enum RawDifficultyCharacteristic {
UNKNOWN_CHARACTERISTIC,
STANDARD,
ONE_SABER,
NO_ARROWS,
LAWLESS,
LIGHTSHOW,
LEGACY,
NINETY_DEGREE,
THREESIXTY_DEGREE
}
// MapTag Enum
export enum RawMapTag {
UNKNOWN_TAG,
DANCE,
SWING,
NIGHTCORE,
FOLK,
FAMILY,
AMBIENT,
FUNK,
JAZZ,
SOUL,
SPEEDCORE,
PUNK,
RB,
HOLIDAY,
VOCALOID,
J_ROCK,
TRANCE,
DRUMBASS,
COMEDY,
INSTRUMENTAL,
HARDCORE,
K_POP,
INDIE,
TECHNO,
HOUSE,
GAME,
FILM,
ALT,
DUBSTEP,
METAL,
ANIME,
HIPHOP,
J_POP,
ROCK,
POP,
ELECTRONIC,
CLASSICAL_ORCHESTRAL,
ACCURACY,
BALANCED,
CHALLENGE,
DANCESTYLE,
FITNESS,
SPEED,
TECH
}
@@ -0,0 +1,158 @@
import { MapStyle, MapTag, MapType } from "../beat-saver.model";
import { RawDifficulty, RawDifficultyCharacteristic, RawDifficultyLabel, RawMapTag, RawSongDetails, RawUploader } from "./raw-song-details-cache.model";
import { SongDetailDiffCharactertistic, SongDetails, SongDiffName, SongDifficulty, SongUploader } from "./song-details-cache.model";
export abstract class RawSongDetailsDeserializer {
private constructor() {}
private static deserializeRawTag(rawTag: RawMapTag): MapTag {
switch (rawTag) {
case RawMapTag.DANCE: return MapStyle.Dance;
case RawMapTag.SWING: return MapStyle.Swing;
case RawMapTag.NIGHTCORE: return MapStyle.Nightcore;
case RawMapTag.FOLK: return MapStyle.Folk;
case RawMapTag.FAMILY: return MapStyle.Family;
case RawMapTag.AMBIENT: return MapStyle.Ambient;
case RawMapTag.FUNK: return MapStyle.Funk;
case RawMapTag.JAZZ: return MapStyle.Jazz;
case RawMapTag.SOUL: return MapStyle.Soul;
case RawMapTag.SPEEDCORE: return MapStyle.Speedcore;
case RawMapTag.PUNK: return MapStyle.Punk;
case RawMapTag.RB: return MapStyle.Rb;
case RawMapTag.HOLIDAY: return MapStyle.Holiday;
case RawMapTag.VOCALOID: return MapStyle.Vocaloid;
case RawMapTag.J_ROCK: return MapStyle.JRock;
case RawMapTag.TRANCE: return MapStyle.Trance;
case RawMapTag.DRUMBASS: return MapStyle.DrumBass;
case RawMapTag.COMEDY: return MapStyle.Comedy;
case RawMapTag.INSTRUMENTAL: return MapStyle.Instrumental;
case RawMapTag.HARDCORE: return MapStyle.Hardcore;
case RawMapTag.K_POP: return MapStyle.KPop;
case RawMapTag.INDIE: return MapStyle.Indie;
case RawMapTag.TECHNO: return MapStyle.Techno;
case RawMapTag.HOUSE: return MapStyle.House;
case RawMapTag.GAME: return MapStyle.Game;
case RawMapTag.FILM: return MapStyle.Film;
case RawMapTag.ALT: return MapStyle.Alt;
case RawMapTag.DUBSTEP: return MapStyle.Dubstep;
case RawMapTag.METAL: return MapStyle.Metal;
case RawMapTag.ANIME: return MapStyle.Anime;
case RawMapTag.HIPHOP: return MapStyle.Hiphop;
case RawMapTag.J_POP: return MapStyle.JPop;
case RawMapTag.ROCK: return MapStyle.Rock;
case RawMapTag.POP: return MapStyle.Pop;
case RawMapTag.ELECTRONIC: return MapStyle.Electronic;
case RawMapTag.CLASSICAL_ORCHESTRAL: return MapStyle.ClassicalOrchestral;
case RawMapTag.ACCURACY: return MapType.Accuracy;
case RawMapTag.BALANCED: return MapType.Balanced;
case RawMapTag.CHALLENGE: return MapType.Challenge;
case RawMapTag.DANCESTYLE: return MapType.Dancestyle;
case RawMapTag.FITNESS: return MapType.Fitness;
case RawMapTag.SPEED: return MapType.Speed;
case RawMapTag.TECH: return MapType.Tech;
default: return undefined;
}
}
private static deserializeRawTags(rawTags: RawMapTag[] = []): MapTag[] {
return rawTags.map(rawTag => this.deserializeRawTag(rawTag)).filter(Boolean);
}
private static deserializeRawDifficultyCharacteristic(rawCharacteristic: RawDifficultyCharacteristic): SongDetailDiffCharactertistic {
switch (rawCharacteristic) {
case RawDifficultyCharacteristic.STANDARD: return SongDetailDiffCharactertistic.Standard;
case RawDifficultyCharacteristic.ONE_SABER: return SongDetailDiffCharactertistic.OneSaber;
case RawDifficultyCharacteristic.NO_ARROWS: return SongDetailDiffCharactertistic.NoArrows;
case RawDifficultyCharacteristic.LAWLESS: return SongDetailDiffCharactertistic.Lawless;
case RawDifficultyCharacteristic.LIGHTSHOW: return SongDetailDiffCharactertistic.Lightshow;
case RawDifficultyCharacteristic.LEGACY: return SongDetailDiffCharactertistic.Legacy;
case RawDifficultyCharacteristic.NINETY_DEGREE: return SongDetailDiffCharactertistic._90Degree;
case RawDifficultyCharacteristic.THREESIXTY_DEGREE: return SongDetailDiffCharactertistic._360Degree;
default: return SongDetailDiffCharactertistic.Standard;
}
}
private static deserializeRawDifficultyLabel(rawLabel: RawDifficultyLabel): SongDiffName {
switch (rawLabel) {
case RawDifficultyLabel.EASY: return SongDiffName.Easy;
case RawDifficultyLabel.NORMAL: return SongDiffName.Normal;
case RawDifficultyLabel.HARD: return SongDiffName.Hard;
case RawDifficultyLabel.EXPERT: return SongDiffName.Expert;
case RawDifficultyLabel.EXPERT_PLUS: return SongDiffName.ExpertPlus;
default: return SongDiffName.ExpertPlus;
}
}
private static deserializeRawDifficulty(rawDifficulty: RawDifficulty): SongDifficulty {
return {
difficulty: this.deserializeRawDifficultyLabel(rawDifficulty.difficulty),
characteristic: this.deserializeRawDifficultyCharacteristic(rawDifficulty.characteristic),
label: rawDifficulty.label,
stars: Math.round(rawDifficulty.starsT100) / 100,
starsBL: Math.round(rawDifficulty.starsBlT100) / 100,
njs: Math.round(rawDifficulty.njsT100) / 100,
nps: Math.round(rawDifficulty.npsT100) / 100,
offset: Math.round(rawDifficulty.offsetT100) / 100,
chroma: rawDifficulty.chroma,
cinema: rawDifficulty.cinema,
me: rawDifficulty.me,
ne: rawDifficulty.ne,
bombs: rawDifficulty.bombs,
notes: rawDifficulty.notes,
obstacles: rawDifficulty.obstacles
};
}
private static deserializeRawDifficulties(rawDifficulties: RawDifficulty[] = []): SongDifficulty[] {
return rawDifficulties.map(rawDifficulty => this.deserializeRawDifficulty(rawDifficulty));
}
private static deserializeRawUploader(rawUploader: RawUploader): SongUploader {
return {
name: rawUploader.name,
id: rawUploader.id,
verified: rawUploader.verified
};
}
/**
* The map ID has been serialized as a number from its HEX value, so we need to deserialize it back to a string.
*/
private static deserializeMapId(rawMapId: number): string {
return rawMapId.toString(16);
}
/**
* Deserialize the raw song details to the song details model.
* @param {RawSongDetails} rawSongDetails the raw song details to deserialize (normally get from the proto message)
* @returns {SongDetails} the deserialized song details
* @throws Can throw an error if the raw song details is invalid or missing required fields
*/
public static deserialize(rawSongDetails: RawSongDetails): SongDetails {
if (!rawSongDetails) {
throw new Error("Invalid raw song details");
}
return {
id: this.deserializeMapId(rawSongDetails.idInt),
hash: rawSongDetails.hash,
duration: rawSongDetails.duration,
uploader: this.deserializeRawUploader(rawSongDetails.uploader),
uploadedAt: rawSongDetails.uploadedAt,
tags: this.deserializeRawTags(rawSongDetails.tags),
ranked: rawSongDetails.ranked,
qualified: rawSongDetails.qualified,
curated: rawSongDetails.curated,
rankedBL: rawSongDetails.rankedBL,
nominatedBL: rawSongDetails.nominatedBL,
qualifiedBL: rawSongDetails.qualifiedBL,
upVotes: rawSongDetails.upVotes,
downVotes: rawSongDetails.downVotes,
downloads: rawSongDetails.downloads,
automapper: rawSongDetails.automapper,
difficulties: this.deserializeRawDifficulties(rawSongDetails.difficulties)
};
}
}
@@ -0,0 +1,64 @@
import { MapTag } from "../beat-saver.model";
export interface SongDetails {
id: string;
hash: string;
duration: number;
uploader: SongUploader;
uploadedAt: number;
tags: MapTag[];
ranked: boolean;
qualified: boolean;
curated: boolean;
rankedBL: boolean;
nominatedBL: boolean;
qualifiedBL: boolean;
upVotes: number;
downVotes: number;
downloads: number;
automapper: boolean;
difficulties: SongDifficulty[];
}
export interface SongDifficulty {
difficulty: SongDiffName;
characteristic: SongDetailDiffCharactertistic;
label: string;
stars: number;
starsBL: number;
njs: number;
nps: number;
offset: number;
chroma: boolean;
cinema: boolean;
me: boolean;
ne: boolean;
bombs: number;
notes: number;
obstacles: number;
}
export interface SongUploader {
name: string;
id: number;
verified: boolean;
}
export enum SongDiffName {
Easy = "Easy",
Normal = "Normal",
Hard = "Hard",
Expert = "Expert",
ExpertPlus = "ExpertPlus",
}
export enum SongDetailDiffCharactertistic {
Standard = "Standard",
OneSaber = "OneSaber",
NoArrows = "NoArrows",
Lawless = "Lawless",
Lightshow = "Lightshow",
Legacy = "Legacy",
_90Degree = "90Degree",
_360Degree = "360Degree",
}