[chore-521] Move create playlist action

This commit is contained in:
MathieuG-P
2024-10-20 18:10:01 +02:00
parent b59c37ebbd
commit 0efc56f8d6
24 changed files with 703 additions and 109 deletions
+2
View File
@@ -8,6 +8,7 @@ import crypto from "crypto";
import { execSync } from "child_process";
import { tryit } from "../../shared/helpers/error.helpers";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { ErrorObject } from "serialize-error";
export async function pathExist(path: string): Promise<boolean> {
try {
@@ -279,4 +280,5 @@ export interface Progression<T = unknown, D = unknown> {
diff?: number;
data?: T;
extra?: D;
lastError?: ErrorObject;
}
+5
View File
@@ -59,6 +59,11 @@ ipc.on("export-playlists", (args, reply) => {
reply(playlists.exportPlaylists(args));
});
ipc.on("import-playlists", (args, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
reply(playlists.importPlaylists(args));
});
ipc.on("install-playlist-file", (args, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
+5
View File
@@ -13,6 +13,11 @@ ipc.on("new-window", (args, reply) => {
reply(from(shell.openExternal(args)));
});
ipc.on("open-dialog", (args, reply) => {
// Use this ipc instead of "choose-folder" or "choose-file" to have more control over the dialog
reply(from(dialog.showOpenDialog(args)));
})
ipc.on("choose-folder", (args, reply) => {
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
});
@@ -26,6 +26,7 @@ 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";
import { serializeError } from "serialize-error";
export class LocalPlaylistsManagerService {
private static instance: LocalPlaylistsManagerService;
@@ -97,7 +98,7 @@ export class LocalPlaylistsManagerService {
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);
const playlistPath = path.join(playlistFolder, `${sanitize(opt.bpList.playlistTitle)}${path.extname(opt.dest)}`);
const playlistPath = path.join(playlistFolder, `${sanitize(opt.bpList.playlistTitle)}.bplist`);
return getUniqueFileNamePath(playlistPath);
})();
@@ -122,7 +123,7 @@ export class LocalPlaylistsManagerService {
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)}${path.extname(opt.dest)}`);
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`);
})();
writeFileSync(dest, JSON.stringify(bplist, null, 2));
@@ -137,13 +138,22 @@ export class LocalPlaylistsManagerService {
const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; });
if(!isLocalFile && !isValidUrl(source)) {
throw new Error(`Invalid source ${source}`);
throw new CustomError(`Invalid source (${source})`, "INVALID_SOURCE");
}
const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(source);
const bpList: BPList = await (async () => {
if(isLocalFile){
const res = await tryit(async () => JSON.parse(readFileSync(source).toString()));
if(res.error) {
throw CustomError.fromError(res.error, "CANNOT_PARSE_PLAYLIST");
}
return res.result;
}
return this.request.getJSON<BPList>(source);
})();
if(!bpList?.playlistTitle) {
throw new Error(`Invalid playlist file ${source}`);
throw new CustomError(`Invalid playlist file (${source})`, "INVALID_PLAYLIST_FILE");
}
bpList.songs = (bpList.songs ?? []).map(s => s.hash ? (
@@ -430,6 +440,49 @@ export class LocalPlaylistsManagerService {
return archive.finalize();
}
public importPlaylists({ version, paths }: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
return new Observable<Progression<LocalBPListsDetails>>(obs => {
let canceled = false;
(async () => {
const bplistPaths = paths.filter(p => this.acceptPlaylistFiletype(p));
const progress: Progression<LocalBPListsDetails> = { current: 0, total: bplistPaths.length, data: null };
obs.next(progress);
for(const playlistPath of bplistPaths) {
if(canceled) {
log.info("Playlist import canceled");
return;
}
const { result: localBPList, error } = await tryit(() => this.installBPListFile({ bplistSource: playlistPath, version }));
if(error) {
progress.lastError = serializeError(CustomError.fromError(error));
obs.next(progress);
log.error(error);
continue;
}
const bpListDetails = this.getLocalBPListDetails(localBPList.localBPList);
progress.current += 1;
progress.data = bpListDetails;
obs.next(progress);
}
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
return () => {
canceled = true;
}
});
}
public oneClickInstallPlaylist(bplistUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
@@ -348,14 +348,15 @@ export class LocalMapsManagerService {
const zip = new StreamZip.async({ file: zipPath });
const { result: zipEntries, error } = await tryit(() => zip.entries());
const zipEntriesValues = Object.values(zipEntries);
if(error) {
log.error("Could not read zip entries", zipPath, error);
await zip.close();
const res = await tryit(() => zip.close());
log.error("Could not read zip entries", zipPath, error, res?.error);
continue;
}
const zipEntriesValues = Object.values(zipEntries);
const mapsFolders = zipEntriesValues.reduce((acc, entry) => {
if(!/(^|\/)[Ii]nfo\.dat$/.test(entry.name)){ return acc; }
acc.push(path.dirname(entry.name));