mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[bugfix] updating naming sheme of downloaded maps and playlists
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user