[feature-107] local playlist filtering & can select playlist to perform actions : export, delete, sync

This commit is contained in:
MathieuG-P
2024-06-09 22:45:03 +02:00
parent ab07334cb8
commit 901137ada9
19 changed files with 602 additions and 84 deletions
+17 -3
View File
@@ -1,4 +1,4 @@
import { CopyOptions, copy, createReadStream, ensureDir, move, realpath, stat, symlink } from "fs-extra";
import { CopyOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, stat, symlink } from "fs-extra";
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
import path from "path";
import { Observable, concatMap, from } from "rxjs";
@@ -217,13 +217,27 @@ export function rxCopy(src: string, dest: string, option?: CopyOptions): Observa
export async function ensurePathNotAlreadyExist(path: string): Promise<string> {
let destPath = path;
let folderExist = await pathExist(destPath);
let folderExist = await pathExists(destPath);
let i = 0;
while (folderExist) {
i++;
destPath = `${path} (${i})`;
folderExist = await pathExist(destPath);
folderExist = await pathExists(destPath);
}
return destPath;
}
export function ensurePathNotAlreadyExistSync(path: string): string {
let destPath = path;
let folderExist = pathExistsSync(destPath);
let i = 0;
while (folderExist) {
i++;
destPath = `${path} (${i})`;
folderExist = pathExistsSync(destPath);
}
return destPath;
+5 -1
View File
@@ -56,9 +56,13 @@ ipc.on("delete-playlist", (args, reply) => {
const maps = LocalMapsManagerService.getInstance();
reply(playlists.deletePlaylistFile(args.bpList).pipe(mergeMap(() => {
if(args.deleteMaps){
console.log("ALALALZELALZELAZELA");
return maps.deleteMapsFromHashs(args.version, args.bpList.songs.map(s => s.hash));
}
return of({ current: 0, total: 0 } as Progression);
})));
});
ipc.on("export-playlists", (args, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
reply(playlists.exportPlaylists(args));
});
+5 -5
View File
@@ -36,8 +36,7 @@ export class Archive {
public addDirectory(path: string, destPath?: string | false): void {
this.directories.push(path);
destPath = destPath === false ? false : _path.basename(path);
this.archive.directory(path, destPath);
this.archive.directory(path, destPath ?? _path.basename(path));
}
public addFile(path: string, destPath?: string): void {
@@ -46,13 +45,14 @@ export class Archive {
this.archive.file(path, { name: destPath });
}
public finalize(): Observable<Progression> {
const progress: Progression = {
public finalize(): Observable<Progression<string>> {
const progress: Progression<string> = {
total: 0,
current: 0,
data: this.output,
};
return new Observable<Progression>(observer => {
return new Observable<Progression<string>>(observer => {
(async () => {
progress.total = await this.loadTotalFiles();
@@ -11,7 +11,7 @@ import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/mo
import { readFileSync } from "fs";
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
import { copy, copyFile, ensureDir, pathExists, pathExistsSync, readdirSync, realpath, writeFile, writeFileSync } from "fs-extra";
import { Progression, pathExist, unlinkPath } from "../../helpers/fs.helpers";
import { Progression, ensurePathNotAlreadyExist, ensurePathNotAlreadyExistSync, pathExist, unlinkPath } from "../../helpers/fs.helpers";
import { FileAssociationService } from "../file-association.service";
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
import { sToMs } from "shared/helpers/time.helpers";
@@ -21,6 +21,8 @@ import { InstallationLocationService } from "../installation-location.service";
import sanitize from "sanitize-filename";
import { isValidUrl } from "shared/helpers/url.helpers";
import { allSettled } from "shared/helpers/promise.helpers";
import { Archive } from "main/models/archive.class";
import { CustomError } from "shared/models/exceptions/custom-error.class";
export class LocalPlaylistsManagerService {
private static instance: LocalPlaylistsManagerService;
@@ -289,6 +291,76 @@ export class LocalPlaylistsManagerService {
return from(unlinkPath(bpList.path));
}
public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, exportMaps?: boolean}): Observable<Progression<string>> {
if(!pathExistsSync(opt.dest)) {
throw new CustomError(`Destination folder not found ${opt.dest}`, "DEST_ENOENT");
}
if(opt.bpLists?.length === 0) {
throw new CustomError("No playlists to export", "NO_PLAYLISTS");
}
const versionName = opt.version ? opt.version.name ?? opt.version.BSVersion : "Shared";
const destName = opt.version ? `${versionName} Playlists` : "Playlists";
const zipDest = path.join(opt.dest, `${destName}.zip`);
const archive = new Archive(zipDest)
for(const bpList of opt.bpLists) {
if(!pathExistsSync(bpList.path)) {
throw new CustomError(`Playlist file not found ${bpList.path}`, "PLAYLIST_ENOENT");
}
archive.addFile(bpList.path, path.join(this.PLAYLISTS_FOLDER, path.basename(bpList.path)));
}
if(!opt.exportMaps) {
return archive.finalize();
}
const mapsHashsToExport = Array.from(
new Set<string>(opt.bpLists.reduce((acc, bpList) => acc.concat((bpList.songs ?? []).map(s => s.hash)), [])).values()
);
const zipMaps$ = new Observable<Progression<string>>(obs => {
(async () => {
const progress: Progression<string> = { total: mapsHashsToExport.length, current: 0, data: zipDest };
for(const hash of mapsHashsToExport) {
const mapInfo = await this.maps.getMapInfoFromHash(hash, opt.version);
if(!mapInfo || !pathExistsSync(mapInfo.path)) { continue; }
archive.addDirectory(
mapInfo.path,
path.join("Maps", path.basename(mapInfo.path)) // Dont't know why, but "CustomLevels" not work
);
progress.current += 1;
obs.next(progress);
}
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
return new Observable<Progression<string>>(obs => {
(async () => {
const maps$ = zipMaps$.pipe(tap({ next: p => obs.next(p) }));
const archive$ = archive.finalize().pipe(tap({ next: p => obs.next(p) }));
await lastValueFrom(maps$);
await lastValueFrom(archive$);
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
})
}
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
@@ -283,7 +283,7 @@ export class LocalMapsManagerService {
});
}
public async getMapInfoFromHash(hash: string, version: BSVersion): Promise<BsmLocalMap> {
public async getMapInfoFromHash(hash: string, version?: BSVersion): Promise<BsmLocalMap> {
const versionMapsPath = await this.getMapsFolderPath(version);
const mapInfo = this.songCache.getMapInfoFromHash(hash);