From aa69b3c68342485a8a0dc619c395abc625d8ddaa Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:00:41 +0100 Subject: [PATCH] [bugfix] updating naming sheme of downloaded maps and playlists --- package-lock.json | 49 ++++++++---- package.json | 10 ++- .../local-playlists-manager.service.ts | 27 ++++--- .../maps/local-maps-manager.service.ts | 2 +- src/main/services/bs-version-lib.service.ts | 2 +- .../services/mods/beat-mods-api.service.ts | 8 +- src/main/services/request.service.ts | 79 +++++++++++-------- src/main/services/supporters.service.ts | 2 +- .../beat-saver/beat-saver-api.service.ts | 10 +-- .../model-saber/model-saber-api.service.ts | 2 +- 10 files changed, 111 insertions(+), 80 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95a116afc..93d359be1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "format-duration": "^3.0.2", "framer-motion": "^11.2.6", "fs-extra": "^11.2.0", - "got": "^14.4.2", + "got": "^14.4.3", "history": "^5.3.0", "is-elevated": "^4.0.0", "jszip": "^3.10.1", @@ -79,7 +79,7 @@ "@types/dompurify": "^3.0.5", "@types/got": "^9.6.12", "@types/jest": "^29.5.11", - "@types/node": "20.11.15", + "@types/node": "22.8.6", "@types/node-fetch": "^2.6.3", "@types/pako": "^2.0.1", "@types/react": "^18.0.33", @@ -5723,9 +5723,10 @@ "license": "MIT" }, "node_modules/@sindresorhus/is": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.0.tgz", - "integrity": "sha512-WDTlVTyvFivSOuyvMeedzg2hdoBLZ3f1uNVuEida2Rl9BrfjrIRjWA/VZIrMRLvSwJYCAlCRA3usDt1THytxWQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.1.tgz", + "integrity": "sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -6330,10 +6331,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.15", + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, "node_modules/@types/node-fetch": { @@ -11090,6 +11093,16 @@ "node": ">=12" } }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.5.tgz", + "integrity": "sha512-n8FYY/pRxu496441gIcAQFZPKXbhsd6VZygcq+PTSZ75eMh/Ke0hCAROdUa21qiFqKNsPPYic46yXDO1JGiPBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/electronmon": { "version": "2.0.2", "dev": true, @@ -13350,11 +13363,12 @@ } }, "node_modules/got": { - "version": "14.4.2", - "resolved": "https://registry.npmjs.org/got/-/got-14.4.2.tgz", - "integrity": "sha512-+Te/qEZ6hr7i+f0FNgXx/6WQteSM/QqueGvxeYQQFm0GDfoxLVJ/oiwUKYMTeioColWUTdewZ06hmrBjw6F7tw==", + "version": "14.4.3", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.3.tgz", + "integrity": "sha512-iTC0Z87yxSijWTh/IpvGpwOhIQK7+GgWkYrMRoN/hB9qeRj9RPuLGODwevs0p5idUf7nrxCVa5IlOmK3b8z+KA==", + "license": "MIT", "dependencies": { - "@sindresorhus/is": "^7.0.0", + "@sindresorhus/is": "^7.0.1", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^12.0.1", @@ -13364,7 +13378,7 @@ "lowercase-keys": "^3.0.0", "p-cancelable": "^4.0.1", "responselike": "^3.0.0", - "type-fest": "^4.19.0" + "type-fest": "^4.26.1" }, "engines": { "node": ">=20" @@ -13374,9 +13388,10 @@ } }, "node_modules/got/node_modules/type-fest": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz", - "integrity": "sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -21564,7 +21579,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, "node_modules/unique-filename": { diff --git a/package.json b/package.json index 388edc0fd..5342b2b54 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,9 @@ "afterSign": ".erb/scripts/notarize.js", "afterPack": ".erb/scripts/after-pack.js", "win": { - "signingHashAlgorithms": ["sha256"], + "signingHashAlgorithms": [ + "sha256" + ], "target": [ "nsis", "nsis-web" @@ -157,7 +159,7 @@ "@types/dompurify": "^3.0.5", "@types/got": "^9.6.12", "@types/jest": "^29.5.11", - "@types/node": "20.11.15", + "@types/node": "22.8.6", "@types/node-fetch": "^2.6.3", "@types/pako": "^2.0.1", "@types/react": "^18.0.33", @@ -249,7 +251,7 @@ "format-duration": "^3.0.2", "framer-motion": "^11.2.6", "fs-extra": "^11.2.0", - "got": "^14.4.2", + "got": "^14.4.3", "history": "^5.3.0", "is-elevated": "^4.0.0", "jszip": "^3.10.1", @@ -321,6 +323,6 @@ "logLevel": "quiet" }, "volta": { - "node": "20.17.0" + "node": "22.11.0" } } 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 ca1bad720..464a9aeb5 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -118,38 +118,41 @@ export class LocalPlaylistsManagerService { version?: BSVersion, dest?: string }): Promise<{path: string, localBPList: LocalBPList}> { - const bplist = await this.readPlaylistFromSource(opt.bplistSource); + const { bpList, filename } = await this.readPlaylistFromSource(opt.bplistSource); const dest = await (async () => { if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; } const playlistFolder = await this.getPlaylistsFolder(opt.version); - return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`); + return path.join(playlistFolder, filename); })(); - writeFileSync(dest, JSON.stringify(bplist, null, 2)); + writeFileSync(dest, JSON.stringify(bpList, null, 2)); - const localBPList: LocalBPList = { ...bplist, path: dest }; + const localBPList: LocalBPList = { ...bpList, path: dest }; return { path: dest, localBPList }; } - private async readPlaylistFromSource(source: string): Promise { + private async readPlaylistFromSource(source: string): Promise<{ bpList: BPList, filename: string }> { const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; }); if(!isLocalFile && !isValidUrl(source)) { throw new CustomError(`Invalid source (${source})`, "INVALID_SOURCE"); } - const bpList: BPList = await (async () => { + const { bpList, filename }: { bpList: BPList, filename: string } = await (async () => { if(isLocalFile){ - const res = await tryit(async () => JSON.parse(readFileSync(source).toString())); + const res = await tryit(async () => JSON.parse(readFileSync(source).toString()) as BPList); if(res.error) { throw CustomError.fromError(res.error, "CANNOT_PARSE_PLAYLIST"); } - return res.result; + return { bpList: res.result, filename: path.basename(source) }; } - return this.request.getJSON(source); + + const res = await this.request.getJSON(source); + const filename = this.request.getFilenameFromContentDisposition(res.headers["content-disposition"]) ?? `${res.data.playlistTitle}.bplist`; + return { bpList: res.data, filename }; })(); if(!bpList?.playlistTitle) { @@ -160,7 +163,7 @@ export class LocalPlaylistsManagerService { { ...s, hash: findHashInString(s.hash) ?? s.hash } ) : s).filter(Boolean); - return bpList; + return { bpList, filename }; } private openOneClickDownloadPlaylistWindow(downloadUrl: string): void { @@ -189,14 +192,14 @@ export class LocalPlaylistsManagerService { progress.total = playlistPaths.length; for (const playlistPath of playlistPaths) { - const {result: bpList, error} = await tryit(() => this.readPlaylistFromSource(playlistPath)); + const {result, error} = await tryit(() => this.readPlaylistFromSource(playlistPath)); if(error) { log.error(error); continue; } - const localBpList: LocalBPList = { ...bpList, path: playlistPath }; + const localBpList: LocalBPList = { ...result.bpList, path: playlistPath }; bpLists.push(localBpList); progress.current += 1; obs.next(progress); diff --git a/src/main/services/additional-content/maps/local-maps-manager.service.ts b/src/main/services/additional-content/maps/local-maps-manager.service.ts index 31de5b95f..e2c7508a9 100644 --- a/src/main/services/additional-content/maps/local-maps-manager.service.ts +++ b/src/main/services/additional-content/maps/local-maps-manager.service.ts @@ -440,7 +440,7 @@ export class LocalMapsManagerService { log.info("Downloading map", map.name, map.id); const zipUrl = map.versions.at(0).downloadURL; - const mapFolderName = sanitize(`${map.id}-${map.name}`); + const mapFolderName = sanitize(`${map.id} (${map.metadata.songName} - ${map.metadata.levelAuthorName})`); const mapsFolder = await this.getMapsFolderPath(version); const mapPath = path.join(mapsFolder, mapFolderName); diff --git a/src/main/services/bs-version-lib.service.ts b/src/main/services/bs-version-lib.service.ts index c4bc812e4..15a95a671 100644 --- a/src/main/services/bs-version-lib.service.ts +++ b/src/main/services/bs-version-lib.service.ts @@ -33,7 +33,7 @@ export class BSVersionLibService { } private getRemoteVersions(): Promise { - return this.requestService.getJSON(this.REMOTE_BS_VERSIONS_URL); + return this.requestService.getJSON(this.REMOTE_BS_VERSIONS_URL).then(res => res.data); } private async getLocalVersions(): Promise { diff --git a/src/main/services/mods/beat-mods-api.service.ts b/src/main/services/mods/beat-mods-api.service.ts index 40e1fb0f3..c1c7f6d99 100644 --- a/src/main/services/mods/beat-mods-api.service.ts +++ b/src/main/services/mods/beat-mods-api.service.ts @@ -41,7 +41,7 @@ export class BeatModsApiService { if (this.aliasesCache.size) { return this.aliasesCache; } - return this.requestService.getJSON>(this.BEAT_MODS_ALIAS).then(rawAliases => { + return this.requestService.getJSON>(this.BEAT_MODS_ALIAS).then(({ data: rawAliases }) => { Object.entries(rawAliases).forEach(([key, value]) => { this.aliasesCache.set( key, @@ -97,7 +97,7 @@ export class BeatModsApiService { const alias = await this.getAliasOfVersion(version); - return this.requestService.getJSON(this.getVersionModsUrl(alias)).then(mods => { + return this.requestService.getJSON(this.getVersionModsUrl(alias)).then(({ data: mods }) => { mods = mods.map(mod => this.asignDependencies(mod, mods)); this.versionModsCache.set(version.BSVersion, mods); @@ -111,7 +111,7 @@ export class BeatModsApiService { if (this.allModsCache) { return this.allModsCache; } - return this.requestService.getJSON(this.getAllModsUrl()).then(mods => { + return this.requestService.getJSON(this.getAllModsUrl()).then(({ data: mods }) => { this.allModsCache = mods; return this.allModsCache; }); @@ -122,7 +122,7 @@ export class BeatModsApiService { return Promise.resolve(this.modsHashCache.get(hash)); } - return this.requestService.getJSON(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(mods => { + return this.requestService.getJSON(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(({ data: mods }) => { this.updateModsHashCache(mods); return mods.at(0); }); diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index bb1768911..9f5f71640 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -1,15 +1,13 @@ -import { Agent, RequestOptions } from "https"; import { createWriteStream } from "fs"; import { Progression } from "main/helpers/fs.helpers"; import { Observable, shareReplay, tap } from "rxjs"; import log from "electron-log"; -import fetch, { RequestInfo, RequestInit } from "node-fetch"; import got, { Options } from "got"; -import { IncomingMessage } from "http"; -import { app } from "electron"; -import os from "os"; +import { IncomingHttpHeaders, IncomingMessage } from "http"; import { unlinkSync } from "fs-extra"; import { tryit } from "shared/helpers/error.helpers"; +import path from "path"; +import { pipeline } from "stream/promises"; export class RequestService { private static instance: RequestService; @@ -21,51 +19,64 @@ export class RequestService { return RequestService.instance; } - private readonly defaultRequestInit: RequestInit; + private constructor() {} - private constructor() { + public getFilenameFromContentDisposition(disposition: string): string | undefined { - this.defaultRequestInit = { - headers: { - "User-Agent": `BSManager/${app.getVersion()} (${os.type()} ${os.release()})` - }, - agent: new Agent({ family: 4 }), - }; - } + if(!disposition) { + return undefined; + } - private getInitWithOptions(options?: RequestInit): RequestInit { - return { ...this.defaultRequestInit, ...(options || {}) }; - } + const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:; ?|$)/i; + const asciiFilenameRegex = /^filename=(["']?)(.*?[^\\])\1(?:; ?|$)/i; - private requestOptionsFromDefaultInit(): RequestOptions { - return { - headers: this.defaultRequestInit.headers as Record, - agent: this.defaultRequestInit.agent as Agent, - }; - } + const utf8Match = utf8FilenameRegex.exec(disposition); + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1]); + } - public async getJSON(url: RequestInfo, options?: RequestInit): Promise { + const filenameStart = disposition.toLowerCase().indexOf('filename='); - try { - const response = await fetch(url, this.getInitWithOptions(options)); + if (filenameStart < 0) { + return undefined; + } - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status} ${url}`); - } + const partialDisposition = disposition.slice(filenameStart); + return asciiFilenameRegex.exec(partialDisposition)?.[2]; + } - return await response.json() as T; + public async getJSON(url: string): Promise<{ data: T, headers: IncomingHttpHeaders }> { + + try{ + const res = await got(url); + return { data: JSON.parse(res.body), headers: res.headers }; } catch (err) { log.error(err); throw err; } } - public downloadFile(url: string, dest: string): Observable> { + public downloadFile(url: string, dest: string, opt?:{ preferContentDisposition?: boolean }): Observable> { return new Observable>(subscriber => { - const progress: Progression = { current: 0, total: 0, data: dest }; + const progress: Progression = { current: 0, total: 0 }; const stream = got.stream(url) - const file = createWriteStream(dest); + + stream.on("response", response => { + const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers["content-disposition"]) : null; + + if (filename) { + dest = path.join(path.dirname(dest), filename); + } + + progress.data = dest; + + const file = createWriteStream(dest); + + pipeline(stream, file).catch(err => { + subscriber.error(err); + }); + }); stream.on("downloadProgress", ({ transferred, total }) => { progress.current = transferred; @@ -83,8 +94,6 @@ export class RequestService { subscriber.complete(); }); - stream.pipe(file); - return () => { stream.destroy(); } diff --git a/src/main/services/supporters.service.ts b/src/main/services/supporters.service.ts index 15d4d384d..fc9be5adc 100644 --- a/src/main/services/supporters.service.ts +++ b/src/main/services/supporters.service.ts @@ -35,7 +35,7 @@ export class SupportersService { } private getRemoteSupporters(): Promise { - return this.requestService.getJSON(this.PATREONS_URL); + return this.requestService.getJSON(this.PATREONS_URL).then(res => res.data); } private async getLocalSupporters(): Promise { diff --git a/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts index 8ccb2eeda..3c7541cc2 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts @@ -71,7 +71,7 @@ export class BeatSaverApiService { } const paramsHashs = hashs.join(","); - const data = await this.request.getJSON, BsvMapDetail> | BsvMapDetail>(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`); + const { data } = (await this.request.getJSON, BsvMapDetail> | BsvMapDetail>(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`)); if ((data as BsvMapDetail).id) { const key = (data as BsvMapDetail).versions.at(0).hash.toLowerCase(); @@ -86,22 +86,22 @@ export class BeatSaverApiService { } public async getMapDetailsById(id: string): Promise { - return this.request.getJSON(`${this.bsaverApiUrl}/maps/id/${id}`); + return (await this.request.getJSON(`${this.bsaverApiUrl}/maps/id/${id}`)).data; } public searchMaps(search: SearchParams): Promise { const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`); url.search = this.searchParamsToUrlParams(search).toString(); - return this.request.getJSON(url.toString()); + return this.request.getJSON(url.toString()).then(res => res.data); } public searchPlaylists(search: PlaylistSearchParams): Promise { const url = new URL(`${this.bsaverApiUrl}/playlists/search/${search?.page ?? 0}`); url.search = new URLSearchParams(this.objectToStringRecord(search)).toString(); - return this.request.getJSON(url.toString()); + return this.request.getJSON(url.toString()).then(res => res.data); } public getPlaylistDetailsById(id: string, page = 0): Promise { - return this.request.getJSON(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`); + return this.request.getJSON(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`).then(res => res.data); } } diff --git a/src/main/services/thrid-party/model-saber/model-saber-api.service.ts b/src/main/services/thrid-party/model-saber/model-saber-api.service.ts index 122be8c22..d60a81d0e 100644 --- a/src/main/services/thrid-party/model-saber/model-saber-api.service.ts +++ b/src/main/services/thrid-party/model-saber/model-saber-api.service.ts @@ -58,6 +58,6 @@ export class ModelSaberApiService { const url = new URL(this.ENDPOINTS.get, this.API_URL); url.search = this.buildUrlQuery(query).toString(); - return this.request.getJSON(url.toString()); + return (await this.request.getJSON(url.toString())).data; } }