[bugfix] updating naming sheme of downloaded maps and playlists

This commit is contained in:
MathieuG-P
2024-11-01 16:00:41 +01:00
parent ed1960c469
commit aa69b3c683
10 changed files with 114 additions and 83 deletions
@@ -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<BPList> {
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<BPList>(source);
const res = await this.request.getJSON<BPList>(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);
@@ -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);
+1 -1
View File
@@ -33,7 +33,7 @@ export class BSVersionLibService {
}
private getRemoteVersions(): Promise<BSVersion[]> {
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL);
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL).then(res => res.data);
}
private async getLocalVersions(): Promise<BSVersion[]> {
@@ -41,7 +41,7 @@ export class BeatModsApiService {
if (this.aliasesCache.size) {
return this.aliasesCache;
}
return this.requestService.getJSON<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(rawAliases => {
return this.requestService.getJSON<Record<string, string[]>>(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<Mod[]>(this.getVersionModsUrl(alias)).then(mods => {
return this.requestService.getJSON<Mod[]>(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<Mod[]>(this.getAllModsUrl()).then(mods => {
return this.requestService.getJSON<Mod[]>(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<Mod[]>(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(mods => {
return this.requestService.getJSON<Mod[]>(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(({ data: mods }) => {
this.updateModsHashCache(mods);
return mods.at(0);
});
+47 -38
View File
@@ -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;
}
const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:; ?|$)/i;
const asciiFilenameRegex = /^filename=(["']?)(.*?[^\\])\1(?:; ?|$)/i;
const utf8Match = utf8FilenameRegex.exec(disposition);
if (utf8Match?.[1]) {
return decodeURIComponent(utf8Match[1]);
}
const filenameStart = disposition.toLowerCase().indexOf('filename=');
if (filenameStart < 0) {
return undefined;
}
const partialDisposition = disposition.slice(filenameStart);
return asciiFilenameRegex.exec(partialDisposition)?.[2];
}
private getInitWithOptions(options?: RequestInit): RequestInit {
return { ...this.defaultRequestInit, ...(options || {}) };
}
public async getJSON<T = unknown>(url: string): Promise<{ data: T, headers: IncomingHttpHeaders }> {
private requestOptionsFromDefaultInit(): RequestOptions {
return {
headers: this.defaultRequestInit.headers as Record<string, string>,
agent: this.defaultRequestInit.agent as Agent,
};
}
public async getJSON<T = unknown>(url: RequestInfo, options?: RequestInit): Promise<T> {
try {
const response = await fetch(url, this.getInitWithOptions(options));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} ${url}`);
}
return await response.json() as T;
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<Progression<string>> {
public downloadFile(url: string, dest: string, opt?:{ preferContentDisposition?: boolean }): Observable<Progression<string>> {
return new Observable<Progression<string>>(subscriber => {
const progress: Progression<string> = { current: 0, total: 0, data: dest };
const progress: Progression<string> = { 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();
}
+1 -1
View File
@@ -35,7 +35,7 @@ export class SupportersService {
}
private getRemoteSupporters(): Promise<Supporter[]> {
return this.requestService.getJSON(this.PATREONS_URL);
return this.requestService.getJSON<Supporter[]>(this.PATREONS_URL).then(res => res.data);
}
private async getLocalSupporters(): Promise<Supporter[]> {
@@ -71,7 +71,7 @@ export class BeatSaverApiService {
}
const paramsHashs = hashs.join(",");
const data = await this.request.getJSON<Record<Lowercase<T>, BsvMapDetail> | BsvMapDetail>(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`);
const { data } = (await this.request.getJSON<Record<Lowercase<T>, 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<BsvMapDetail> {
return this.request.getJSON<BsvMapDetail>(`${this.bsaverApiUrl}/maps/id/${id}`);
return (await this.request.getJSON<BsvMapDetail>(`${this.bsaverApiUrl}/maps/id/${id}`)).data;
}
public searchMaps(search: SearchParams): Promise<SearchResponse> {
const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`);
url.search = this.searchParamsToUrlParams(search).toString();
return this.request.getJSON<SearchResponse>(url.toString());
return this.request.getJSON<SearchResponse>(url.toString()).then(res => res.data);
}
public searchPlaylists(search: PlaylistSearchParams): Promise<PlaylistSearchResponse> {
const url = new URL(`${this.bsaverApiUrl}/playlists/search/${search?.page ?? 0}`);
url.search = new URLSearchParams(this.objectToStringRecord(search)).toString();
return this.request.getJSON<PlaylistSearchResponse>(url.toString());
return this.request.getJSON<PlaylistSearchResponse>(url.toString()).then(res => res.data);
}
public getPlaylistDetailsById(id: string, page = 0): Promise<BsvPlaylistPage> {
return this.request.getJSON<BsvPlaylistPage>(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`);
return this.request.getJSON<BsvPlaylistPage>(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`).then(res => res.data);
}
}
@@ -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<MSGetResponse>(url.toString());
return (await this.request.getJSON<MSGetResponse>(url.toString())).data;
}
}