[bugfix] fix playlist maps with malformed hash doesnt get loaded + improve playlist maps reading reliability

This commit is contained in:
MathieuG-P
2024-07-17 20:26:54 +02:00
parent 163857a14f
commit a6fa55719d
5 changed files with 87 additions and 25 deletions
@@ -7,7 +7,7 @@ import { RequestService } from "../request.service";
import { LocalMapsManagerService } from "./maps/local-maps-manager.service";
import log from "electron-log";
import { WindowManagerService } from "../window-manager.service";
import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/models/playlists/playlist.interface";
import { readFileSync, Stats } from "fs";
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
import { copy, ensureDir, pathExists, pathExistsSync, realpath, writeFileSync } from "fs-extra";
@@ -25,6 +25,8 @@ import { CustomError } from "shared/models/exceptions/custom-error.class";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { tryit } from "shared/helpers/error.helpers";
import recursiveReadDir from "recursive-readdir";
import { BsvMapDetail, SongDetails } from "shared/models/maps";
import { findHashInString } from "shared/helpers/string.helpers";
export class LocalPlaylistsManagerService {
private static instance: LocalPlaylistsManagerService;
@@ -138,10 +140,14 @@ export class LocalPlaylistsManagerService {
const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(source);
if(!bpList?.playlistTitle){
if(!bpList?.playlistTitle) {
throw new Error(`Invalid playlist file ${source}`);
}
bpList.songs = (bpList.songs ?? []).map(s => s.hash ? (
{ ...s, hash: findHashInString(s.hash) ?? s.hash }
) : s).filter(Boolean);
return bpList;
}
@@ -190,6 +196,25 @@ export class LocalPlaylistsManagerService {
});
}
private getSongDetailsFromPlaylistSong(song: PlaylistSong): SongDetails | undefined {
let songDetails: SongDetails;
const songHash = findHashInString(song.hash);
if(songHash){
songDetails = this.songDetails.getSongDetails(song.hash);
}
if(song.key && !songDetails){
songDetails = this.songDetails.getSongDetailsById(song.key);
}
const levelIdHash = findHashInString(song.levelid);
if(levelIdHash && !songDetails){
songDetails = this.songDetails.getSongDetails(levelIdHash);
}
return songDetails;
}
public getLocalBPListDetails(localBPList: LocalBPList): LocalBPListsDetails {
const tryExtractPlaylistId = (url: string) => {
@@ -204,23 +229,23 @@ export class LocalPlaylistsManagerService {
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined
}
const songsDetails = localBPList.songs?.map(s => {
if(s.hash){
return this.songDetails.getSongDetails(s.hash);
}
if(s.key){
return this.songDetails.getSongDetailsById(s.key);
}
return undefined;
}).filter(Boolean);
const mappers = new Set<number>();
if(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))));
bpListDetails.maxNps = Math.max(...songsDetails.map(s => Math.max(...s.difficulties.map(d => d.nps || 0))));
for(const song of localBPList.songs){
const songDetails = this.getSongDetailsFromPlaylistSong(song);
if(!songDetails) { continue; }
bpListDetails.duration += songDetails?.duration ?? 0;
mappers.add(songDetails.uploader?.id);
bpListDetails.minNps = Math.min(bpListDetails?.minNps ?? 0, Math.min(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
bpListDetails.maxNps = Math.max(bpListDetails?.maxNps ?? 0, Math.max(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
song.songDetails = songDetails;
}
bpListDetails.nbMappers = mappers.size;
return bpListDetails;
}
@@ -276,7 +301,28 @@ export class LocalPlaylistsManagerService {
continue;
}
const [ mapDetail ] = await this.bsaver.getMapDetailsFromHashs([song.hash]);
const mapDetail = await (async () => {
let mapDetail: BsvMapDetail;
const mapHash = findHashInString(song?.hash);
if(mapHash) {
mapDetail = (await this.bsaver.getMapDetailsFromHashs([findHashInString(mapHash)])).at(0);
}
if(song.key && !mapDetail) {
mapDetail = await this.bsaver.getMapDetailsById(song.key);
}
const levelIdHash = findHashInString(song?.levelid);
if(levelIdHash && !mapDetail) {
mapDetail = (await this.bsaver.getMapDetailsFromHashs([levelIdHash])).at(0);
}
return mapDetail;
})().catch(e => {
log.error(e);
return undefined as BsvMapDetail;
});
if(!mapDetail) {
continue;
@@ -23,14 +23,15 @@ export class BeatSaverService {
}
public async getMapDetailsFromHashs(hashs: string[]): Promise<BsvMapDetail[]> {
const filtredHashs = hashs.map(h => h.toLowerCase()).filter(hash => !Array.from(this.cachedMapsDetails.keys()).includes(hash));
const filtredHashs = hashs.map(h => h.toLowerCase()).filter(hash => !this.cachedMapsDetails.has(hash));
const chunkHash = splitIntoChunk(filtredHashs, 50);
const mapDetails = Array.from(this.cachedMapsDetails.entries()).reduce((res, [hash, details]) => {
if (hashs.includes(hash)) {
res.push(details);
const mapDetails = hashs.reduce((acc, hash) => {
const detail = this.cachedMapsDetails.get(hash.toLowerCase());
if (detail) {
acc.push(detail);
}
return res;
return acc;
}, [] as BsvMapDetail[]);
await Promise.allSettled(
@@ -282,8 +282,9 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
let map: BsmLocalMap;
if(playlistSong.hash){
map = maps.find(m => m.hash.toLowerCase() === playlistSong.hash.toLowerCase());
if(playlistSong.hash || playlistSong?.songDetails?.hash){
const hash = (playlistSong.hash || playlistSong.songDetails.hash).toLowerCase();
map = maps.find(m => m.hash.toLowerCase() === hash);
}
else if(playlistSong.key){
map = maps.find(m => m?.songDetails?.id === playlistSong.key);
@@ -36,6 +36,7 @@ import { DraggableVirtualScroll } from "renderer/components/shared/virtual-scrol
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png";
import { findHashInString } from "shared/helpers/string.helpers";
type Props = {
maps$: Observable<BsmLocalMap[]>;
@@ -115,7 +116,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
const maps = await lastValueFrom(maps$.pipe(take(1)));
const playlistMapsRes = (playlist?.songs ?? []).reduce((acc, song) => {
const songHash = song.hash?.toLowerCase();
const songHash = song?.hash?.toLowerCase() ?? song?.songDetails?.hash?.toLowerCase() ?? findHashInString(song.levelid)?.toLowerCase();
const map = maps.find(map => map.hash === songHash);
if(map){
+13
View File
@@ -0,0 +1,13 @@
const HashAlgorithmsLengths = {
sha1: 40,
} as const;
export function findHashInString(str: string, algorithm: keyof typeof HashAlgorithmsLengths = 'sha1'): string | undefined {
if(!str) { return undefined; }
const hashLength = HashAlgorithmsLengths[algorithm];
const regex = new RegExp(`[a-fA-F0-9]{${hashLength}}`, "g");
const match = str.match(regex);
return match ? match[0] : undefined;
}