From a6fa55719dcae0c3b38a473973de71c2f235c309 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:26:54 +0200 Subject: [PATCH] [bugfix] fix playlist maps with malformed hash doesnt get loaded + improve playlist maps reading reliability --- .../local-playlists-manager.service.ts | 80 +++++++++++++++---- .../beat-saver/beat-saver.service.ts | 11 +-- .../local-playlists-list-panel.component.tsx | 5 +- .../edit-playlist-modal.component.tsx | 3 +- src/shared/helpers/string.helpers.ts | 13 +++ 5 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 src/shared/helpers/string.helpers.ts diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index 373edf80..8506a7ea 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -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(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(); - 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; diff --git a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts index 316b4673..33e29e0f 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts @@ -23,14 +23,15 @@ export class BeatSaverService { } public async getMapDetailsFromHashs(hashs: string[]): Promise { - 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( diff --git a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx index e5e8d91f..e0ac0509 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx @@ -282,8 +282,9 @@ export const LocalPlaylistsListPanel = forwardRef( 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); diff --git a/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx index 2e6e1031..000b81fe 100644 --- a/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component.tsx @@ -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; @@ -115,7 +116,7 @@ export const EditPlaylistModal: ModalComponent = ({ 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){ diff --git a/src/shared/helpers/string.helpers.ts b/src/shared/helpers/string.helpers.ts new file mode 100644 index 00000000..91120a7f --- /dev/null +++ b/src/shared/helpers/string.helpers.ts @@ -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; +}