[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
+2 -2
View File
@@ -680,7 +680,7 @@
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/ae11d195bbe41a0f244d7b4d055a0ab89f88e5bb.png",
"ReleaseDate": "1702309893",
"year": "2023",
"recommended": true
"recommended": true
},
{
"BSVersion": "1.34.5",
@@ -709,7 +709,7 @@
"ReleaseDate": "1709831174",
"year": "2024"
},
{
{
"BSVersion": "1.36.0",
"BSManifest": "8533509040167838423",
"OculusBinaryId": "6863072637125790",
+121
View File
@@ -0,0 +1,121 @@
syntax = "proto3";
package song_details_cache_v1;
message SongDetailsCache {
repeated SongDetails songs = 1;
uint32 lastUpdated = 2;
uint32 total = 3;
}
message SongDetails {
uint32 idInt = 1;
string hash = 2;
uint32 duration = 3;
Uploader uploader = 4;
uint32 uploadedAt = 5;
repeated MapTag tags = 6;
bool ranked = 7;
bool qualified = 8;
bool curated = 9;
bool rankedBL = 10;
bool nominatedBL = 11;
bool qualifiedBL = 12;
uint32 upVotes = 13;
uint32 downVotes = 14;
uint32 downloads = 15;
bool automapper = 16;
repeated Difficulty difficulties = 17;
}
message Difficulty {
DifficultyLabel difficulty = 1;
DifficultyCharacteristic characteristic = 2;
string label = 3;
uint32 starsT100 = 4;
uint32 starsBlT100 = 5;
uint32 njsT100 = 6;
uint32 npsT100 = 7;
int32 offsetT100 = 8;
bool chroma = 9;
bool cinema = 10;
bool me = 11;
bool ne = 12;
uint32 bombs = 13;
uint32 notes = 14;
uint32 obstacles = 15;
}
message Uploader {
string name = 1;
uint32 id = 2;
bool verified = 3;
}
enum DifficultyLabel {
UNKNOWN_LABEL = 0; // Default value for undefined/unknown labels
EASY = 1;
NORMAL = 2;
HARD = 3;
EXPERT = 4;
EXPERT_PLUS = 5;
}
enum DifficultyCharacteristic {
UNKNOWN_CHARACTERISTIC = 0; // Default value for undefined/unknown characteristics
STANDARD = 1;
ONE_SABER = 2;
NO_ARROWS = 3;
LAWLESS = 4;
LIGHTSHOW = 5;
LEGACY = 6;
NINETY_DEGREE = 7;
THREESIXTY_DEGREE = 8;
}
enum MapTag {
UNKNOWN_TAG = 0; // Default value for undefined/unknown tags
DANCE = 1;
SWING = 2;
NIGHTCORE = 3;
FOLK = 4;
FAMILY = 5;
AMBIENT = 6;
FUNK = 7;
JAZZ = 8;
SOUL = 9;
SPEEDCORE = 10;
PUNK = 11;
RB = 12;
HOLIDAY = 13;
VOCALOID = 14;
J_ROCK = 15;
TRANCE = 16;
DRUMBASS = 17;
COMEDY = 18;
INSTRUMENTAL = 19;
HARDCORE = 20;
K_POP = 21;
INDIE = 22;
TECHNO = 23;
HOUSE = 24;
GAME = 25;
FILM = 26;
ALT = 27;
DUBSTEP = 28;
METAL = 29;
ANIME = 30;
HIPHOP = 31;
J_POP = 32;
ROCK = 33;
POP = 34;
ELECTRONIC = 35;
CLASSICAL_ORCHESTRAL = 36;
ACCURACY = 37;
BALANCED = 38;
CHALLENGE = 39;
DANCESTYLE = 40;
FITNESS = 41;
SPEED = 42;
TECH = 43;
}
-65
View File
@@ -1,65 +0,0 @@
syntax = "proto3";
package song_details_cache_v1;
message SongDetailsCache {
repeated SongDetails songs = 1;
int64 lastUpdated = 2;
uint32 total = 3;
}
message SongDetails {
string id = 1;
string hash = 2;
string name = 3;
MapDetailMetadata metadata = 4;
Uploader uploader = 5;
int32 uploadedAt = 6;
repeated string tags = 7;
float bpm = 8;
bool ranked = 9;
bool qualified = 10;
bool curated = 11;
bool rankedBL = 12;
bool nominatedBL = 13;
bool qualifiedBL = 14;
int32 upVotes = 15;
int32 downVotes = 16;
int32 downloads = 17;
int32 duration = 18;
bool automapper = 19;
repeated Difficulty difficulties = 20;
}
message Difficulty {
string difficulty = 1;
string characteristic = 2;
string label = 3;
float stars = 4;
float starsBL = 5;
float njs = 6;
float nps = 7;
float offset = 8;
bool chroma = 9;
bool cinema = 10;
bool me = 11;
bool ne = 12;
int32 bombs = 13;
int32 notes = 14;
int32 obstacles = 15;
}
message Uploader {
string name = 1;
int32 id = 2;
bool verified = 3;
}
message MapDetailMetadata {
float bpm = 1;
int32 duration = 2;
string levelAuthorName = 3;
string songAuthorName = 4;
string songName = 5;
string songSubName = 6;
}
@@ -166,9 +166,9 @@ export class LocalPlaylistsManagerService {
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined
}
const songsDetails = localBPList.songs?.map(s => this.songDetails.getSongDetails(s.hash));
const songsDetails = localBPList.songs?.map(s => this.songDetails.getSongDetails(s.hash)).filter(Boolean);
if(songsDetails){
if(songsDetails && songsDetails.length){
bpListDetails.duration = songsDetails.reduce((acc, song) => acc + song.duration, 0);
bpListDetails.nbMappers = new Set(songsDetails.map(s => s.uploader.id)).size;
bpListDetails.minNps = Math.min(...songsDetails.map(s => Math.min(...s.difficulties.map(d => d.nps || 0))));
@@ -8,8 +8,10 @@ import { CACHE_PATH, HTTP_STATUS_CODES } from "main/constants";
import log from "electron-log";
import protobuf from "protobufjs";
import { UtilsService } from "../../utils.service";
import { SongDetails, SongDetailsCache } from "shared/models/maps/song-details-cache.model";
import { SongDetails } from "shared/models/maps/song-details-cache/song-details-cache.model";
import { inflate } from "pako";
import { RawSongDetailsCache } from "shared/models/maps/song-details-cache/raw-song-details-cache.model";
import { RawSongDetailsDeserializer } from "shared/models/maps/song-details-cache/raw-song-details-deserializer.class";
export class SongDetailsCacheService {
@@ -65,18 +67,18 @@ export class SongDetailsCacheService {
}
private async readProtoMessageCacheFile(filePath: string): Promise<Record<string, SongDetails>> {
const protobufRoot = await protobuf.load(this.getProtoShemaPath());
const protobufRoot = protobuf.loadSync(this.getProtoShemaPath());
const cacheMessage = protobufRoot.lookupType("SongDetailsCache");
const buffer = await readFile(filePath);
const messageBuffer = cacheMessage.decode(buffer);
const messageObj = cacheMessage.toObject(messageBuffer) as SongDetailsCache;
const messageObj = cacheMessage.toObject(messageBuffer) as RawSongDetailsCache;
const res: Record<string, SongDetails> = {};
for(const song of messageObj.songs){
res[song.hash.toLocaleLowerCase()] = song;
for(const rawSong of messageObj.songs){
res[rawSong.hash.toLocaleLowerCase()] = RawSongDetailsDeserializer.deserialize(rawSong);
}
return res;
@@ -138,7 +140,7 @@ export class SongDetailsCacheService {
}
private getProtoShemaPath(): string {
return this.utils.getAssetsPath(path.join("protos", "song_details_cache_v1.proto"))
return this.utils.getAssetsPath(path.join("proto", "song_details_cache_v1.proto"))
}
public get loaded$(): Observable<boolean> {
@@ -1,8 +1,6 @@
import { MapFilter, MapSpecificity, MapStyle, MapTag, MapType } from "shared/models/maps/beat-saver.model";
import { motion } from "framer-motion";
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { MAP_TYPES } from "renderer/partials/maps/map-tags/map-types";
import { MAP_STYLES } from "renderer/partials/maps/map-tags/map-styles";
import { BsmCheckbox } from "../../shared/bsm-checkbox.component";
import { minToS } from "../../../../shared/helpers/time.helpers";
import dateFormat from "dateformat";
@@ -210,14 +208,14 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat
<section className="grow capitalize">
<h2 className="uppercase text-sm mb-1">{t("maps.map-filter-panel.tags")}</h2>
<div className="w-full flex flex-row flex-wrap items-start justify-start content-start gap-1 mb-2">
{MAP_TYPES.map(tag => (
{Object.values(MapType).map(tag => (
<span key={tag} onClick={() => handleTagClick(tag)} className={`text-[12.5px] text-black rounded-md px-1 font-bold cursor-pointer ${!isTagActivated(tag) && "opacity-40 hover:opacity-90"}`} style={{ backgroundColor: isTagExcluded(tag) ? MAP_DIFFICULTIES_COLORS.Expert : MAP_DIFFICULTIES_COLORS.Normal }}>
{translateMapType(tag)}
</span>
))}
</div>
<div className="w-full flex flex-row flex-wrap items-start justify-start content-start gap-1">
{MAP_STYLES.map(tag => (
{Object.values(MapStyle).map(tag => (
<span key={tag} onClick={() => handleTagClick(tag)} className={`text-[12.5px] text-black rounded-md px-1 font-bold cursor-pointer ${!isTagActivated(tag) && "opacity-40 hover:opacity-90"}`} style={{ backgroundColor: isTagExcluded(tag) ? MAP_DIFFICULTIES_COLORS.Expert : MAP_DIFFICULTIES_COLORS.Easy }}>
{translateMapStyle(tag)}
</span>
@@ -249,10 +249,10 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
return true;
}
if (!map?.songDetails?.metadata?.duration) {
if (!map?.songDetails?.duration) {
return false;
}
return map.songDetails?.metadata.duration >= filter.minDuration;
return map.songDetails?.duration >= filter.minDuration;
})();
if (!fitMinDuration) {
@@ -263,10 +263,10 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
if (!filter?.maxDuration) {
return true;
}
if (!map?.songDetails?.metadata?.duration) {
if (!map?.songDetails?.duration) {
return false;
}
return map.songDetails?.metadata.duration <= filter.maxDuration;
return map.songDetails?.duration <= filter.maxDuration;
})();
if (!fitMaxDuration) {
@@ -346,7 +346,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
}
const searchCheck = (() => {
return ((map.rawInfo?._songName ?? map.songDetails?.name) || "")?.toLowerCase().includes(search.toLowerCase()) || ((map.rawInfo?._songAuthorName ?? map.songDetails?.metadata?.songAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()) || ((map.rawInfo?._levelAuthorName ?? map.songDetails?.metadata?.levelAuthorName) || "")?.toLowerCase().includes(search.toLowerCase());
return (map.rawInfo?._songName || "")?.toLowerCase().includes(search.toLowerCase()) || (map.rawInfo?._songAuthorName || "")?.toLowerCase().includes(search.toLowerCase()) || (map.rawInfo?._levelAuthorName || "")?.toLowerCase().includes(search.toLowerCase());
})();
if (!searchCheck) {
@@ -1,5 +1,4 @@
import { BsmImage } from "../../shared/bsm-image.component";
import { BsvMapDifficultyType } from "shared/models/maps/beat-saver.model";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsmLink } from "../../shared/bsm-link.component";
import { BsmIcon } from "../../svgs/bsm-icon.component";
@@ -89,7 +88,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
})();
const parseDiffLabel = (diffLabel: string) => {
if (MAP_DIFFICULTIES.includes(diffLabel as BsvMapDifficultyType)) {
if (MAP_DIFFICULTIES.includes(diffLabel as SongDiffName)) {
return t(`maps.difficulties.${diffLabel}`);
}
return diffLabel;
@@ -135,16 +134,16 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
<>
<BsmIcon className="h-4 w-4 mr-px" icon="bsMapDifficulty" />
<div className="flex py-[2px] gap-[1px] h-full">
{diffSet.map(diff => (
<span key={crypto.randomUUID()} className="h-full w-[6px] rounded-full" style={{ backgroundColor: MAP_DIFFICULTIES_COLORS[diff.type] }} />
{diffSet.map((diff, index) => (
<span key={index} className="h-full w-[6px] rounded-full" style={{ backgroundColor: MAP_DIFFICULTIES_COLORS[diff.type] }} />
))}
</div>
</>
);
}
if (diffSets.length > 1) {
return diffSets.map(([diffType, diffSet]) => (
<Fragment key={crypto.randomUUID()}>
return diffSets.map(([diffType, diffSet], index) => (
<Fragment key={index}>
<BsmIcon className="h-full w-fit mr-px" icon={diffType} />
<span className="mr-2 font-bold text-[15px] h-full flex items-center pb-px">{diffSet.length}</span>
</Fragment>
@@ -160,10 +159,10 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
<AnimatePresence>
{(diffsPanelHovered || bottomBarHovered) && (
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-light-main-color-3 dark:bg-main-color-3 text-main-color-1 dark:text-current brightness-125 rounded-md flex flex-col gap-3 scrollbar-default shadow-sm shadow-black" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
{Array.from(diffs.entries()).map(([charac, diffSet]) => (
<ol key={crypto.randomUUID()} className="flex flex-col w-full gap-1">
{diffSet.map(({ type, name, stars }) => (
<li key={`${type}${name}${stars}`} className="w-full h-4 flex items-center gap-1">
{Array.from(diffs.entries()).map(([charac, diffSet], index) => (
<ol key={index} className="flex flex-col w-full gap-1">
{diffSet.map(({ type, name, stars }, index) => (
<li key={`${type}${name}${stars}${index}`} className="w-full h-4 flex items-center gap-1">
<BsmIcon className="h-full w-fit p-px" icon={charac} />
<span className="h-full px-2 flex items-center text-xs font-bold bg-current rounded-full" style={{ color: MAP_DIFFICULTIES_COLORS[type] }}>
{stars ? <span className="h-full block brightness-[.25]"> {stars.toFixed(2)}</span> : <span className="h-full brightness-[.25] leading-4 pb-[2px] capitalize">{parseDiffLabel(type)}</span>}
@@ -5,7 +5,7 @@ import { BehaviorSubject } from "rxjs";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { ParsedMapDiff, MapItem } from "./map-item.component";
import equal from "fast-deep-equal/es6";
import { SongDetailDiffCharactertistic, SongDetails } from "shared/models/maps/song-details-cache.model";
import { SongDetailDiffCharactertistic, SongDetails } from "shared/models/maps/song-details-cache/song-details-cache.model";
import { BsvMapDetail, RawMapInfoData } from "shared/models/maps";
type Props = {
@@ -33,7 +33,7 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel
autor={map.rawInfo._levelAuthorName}
songAutor={map.rawInfo._songAuthorName}
bpm={map.rawInfo._beatsPerMinute}
duration={map.songDetails?.metadata.duration}
duration={map.songDetails?.duration}
selected={selectedMaps.some(selected => selected.hash === map.hash)}
diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
mapId={map.songDetails?.id}
@@ -22,6 +22,7 @@ import { PlaylistDownloaderService } from "renderer/services/playlist-downloader
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { NotificationService } from "renderer/services/notification.service";
import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playlist/delete-playlist-modal.component";
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
type Props = {
version: BSVersion;
@@ -39,8 +40,10 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
const modals = useService(ModalService);
const ipc = useService(IpcService);
const progress = useService(ProgressBarService);
const osDiagnostic = useService(OsDiagnosticService);
const notification = useService(NotificationService);
const isOnline = useObservable(() => osDiagnostic.isOnline$, false);
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true });
const { maps$, playlists$, setPlaylists } = useContext(InstalledMapsContext);
@@ -164,7 +167,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
isInQueue$={playlistDownloader.$isPlaylistInQueue(p, version)}
onClickOpen={() => openPlaylistDetails(p.path)}
onClickDelete={() => deletePlaylist(p)}
onClickSync={() => installPlaylist(p)}
onClickSync={isOnline && (() => installPlaylist(p))}
onClickOpenFile={() => viewPlaylistFile(p.path)}
onClickCancelDownload={() => playlistDownloader.cancelDownload(p, version)}
/>
@@ -1,7 +1,7 @@
import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { FilterPanel } from "renderer/components/maps-playlists-panel/maps/filter-panel.component";
import { MapItem, ParsedMapDiff } from "renderer/components/maps-playlists-panel/maps/map-item.component";
import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component";
@@ -11,7 +11,7 @@ import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.ser
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
import { ModalComponent } from "renderer/services/modale.service";
import { BSVersion } from "shared/bs-version.interface";
import { BsvMapCharacteristic, BsvMapDetail, MapFilter, SearchOrder, SearchParams } from "shared/models/maps/beat-saver.model";
import { BsvMapDetail, MapFilter, SearchOrder, SearchParams } from "shared/models/maps/beat-saver.model";
import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflictImg from "../../../../../assets/images/apngs/beat-conflict.png";
import equal from "fast-deep-equal/es6";
@@ -106,7 +106,7 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
autor={map.rawInfo._levelAuthorName}
songAutor={map.rawInfo._songAuthorName}
bpm={map.rawInfo._beatsPerMinute}
duration={map.songDetails?.metadata.duration}
duration={map.songDetails?.duration}
diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
mapId={map.songDetails?.id}
ranked={map.songDetails?.ranked}
@@ -32,7 +32,6 @@ import { ThumbUpFillIcon } from "./icons/thumb-up-fill-icon.component";
import { TimerFillIcon } from "./icons/timer-fill.component";
import { CheckCircleIcon } from "./icons/check-circle-icon.component";
import { PauseIcon } from "./icons/pause-icon.component";
import { BsvMapCharacteristic } from "shared/models/maps/beat-saver.model";
import { LightshowIcon } from "./icons/lightshow-icon.component";
import { LawlessIcon } from "./icons/lawless-icon.component";
import { NoArrowIcon } from "./icons/no-arrow-icon.component";
@@ -60,8 +59,9 @@ import { ChevronTopIcon } from "./icons/chevron-top-icon.component";
import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
import { ShortcutIcon } from "./icons/shortcut-icon.component";
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache/song-details-cache.model";
export type BsmIconType = BsvMapCharacteristic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
// TODO : Very ugly very messy, need to find a better way to do this
@@ -1,6 +1,6 @@
import { BsvMapDifficultyType } from "shared/models/maps/beat-saver.model";
import { SongDiffName } from "shared/models/maps/song-details-cache/song-details-cache.model";
export const MAP_DIFFICULTIES_COLORS: Record<BsvMapDifficultyType, string> = {
export const MAP_DIFFICULTIES_COLORS: Record<SongDiffName, string> = {
Easy: "#00FF9E",
Normal: "#00FFFF",
Hard: "#FFA700",
@@ -1,3 +1,2 @@
import { BsvMapDifficultyType } from "shared/models/maps/beat-saver.model";
export const MAP_DIFFICULTIES: BsvMapDifficultyType[] = ["Easy", "Normal", "Hard", "Expert", "ExpertPlus"];
import { SongDiffName } from "shared/models/maps/song-details-cache/song-details-cache.model";
export const MAP_DIFFICULTIES = Object.values(SongDiffName);
@@ -1,3 +0,0 @@
import { MapStyle } from "shared/models/maps/beat-saver.model";
export const MAP_STYLES: 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"];
+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",
}