[feature-107] We can now download playlist's missing maps

This commit is contained in:
MathieuG-P
2024-03-19 18:01:07 +01:00
parent 7237e49c7f
commit 3da5af354e
9 changed files with 118 additions and 41 deletions
+12
View File
@@ -2,6 +2,7 @@ import { BSVersion } from "shared/bs-version.interface";
import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service";
import { IpcService } from "../services/ipc.service";
import { of, throwError } from "rxjs";
import { BPList } from "shared/models/playlists/playlist.interface";
const ipc = IpcService.getInstance();
@@ -10,6 +11,17 @@ ipc.on<string>("one-click-install-playlist", (req, reply) => {
reply(playlists.oneClickInstallPlaylist(req.args));
});
ipc.on<{playlist: BPList, version: BSVersion}>("install-playlist", (req, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
if(req.args.playlist?.customData?.syncURL){
return reply(playlists.downloadPlaylist(req.args.playlist.customData.syncURL, req.args.version));
}
reply(playlists.downloadPlaylistSongs(req.args.playlist.songs, req.args.version));
});
ipc.on("register-playlists-deep-link", (_, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
@@ -7,7 +7,7 @@ import { RequestService } from "../request.service";
import { LocalMapsManagerService } from "./maps/local-maps-manager.service";
import log from "electron-log";
import { WindowManagerService } from "../window-manager.service";
import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/models/playlists/playlist.interface";
import { readFileSync } from "fs";
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
import { copy, copyFile, ensureDir, pathExists, pathExistsSync, readdirSync, realpath } from "fs-extra";
@@ -185,28 +185,23 @@ export class LocalPlaylistsManagerService {
});
}
public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
public downloadPlaylistSongs(bpList: PlaylistSong[], version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
(async () => {
const bpListFilePath = await this.installBPListFile(bpListUrl, version);
const bpList = await this.readPlaylistFile(bpListFilePath);
const progress: Progression<DownloadPlaylistProgressionData> = {
total: bpList.songs.length,
total: bpList.length,
current: 0,
data: {
downloadedMaps: [],
currentDownload: null,
playlistInfos: bpList,
playlistPath: bpListFilePath,
playlistInfos: null,
playlistPath: null,
}
};
obs.next(progress);
for (const song of bpList.songs) {
for (const song of bpList) {
const [ mapDetail ] = await this.bsaver.getMapDetailsFromHashs([song.hash]);
if(!mapDetail) {
@@ -225,13 +220,29 @@ export class LocalPlaylistsManagerService {
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
(async () => {
const bpListFilePath = await this.installBPListFile(bpListUrl, version);
const bpList = await this.readPlaylistFile(bpListFilePath);
await lastValueFrom(this.downloadPlaylistSongs(bpList.songs, version).pipe(
tap({ next: p => obs.next(p) }),
));
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
public deletePlaylist(opt: {path: string, deleteMaps?: boolean}): Observable<Progression>{
console.log("AAAAAA");
return new Observable<Progression>(obs => {
(async () => {
@@ -247,8 +258,6 @@ export class LocalPlaylistsManagerService {
progress.current += 1
obs.next(progress);
},
error: err => obs.error(err),
complete: () => obs.next(progress),
}),
));
}
@@ -302,6 +302,8 @@ export class LocalMapsManagerService {
const localMap = await this.loadMapInfoFromPath(mapPath);
localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash);
this.ipc.send<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded", this.windows.getWindows("index.html").at(0), { map: localMap, version });
return localMap;
}
@@ -20,6 +20,7 @@ import { useConstant } from "renderer/hooks/use-constant.hook";
import { BsContentLoader } from "renderer/components/shared/bs-content-loader.component";
import { InstalledMapsContext } from "../maps-playlists-panel.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import equal from "fast-deep-equal";
type Props = {
version: BSVersion;
@@ -77,12 +78,6 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
useEffect(() => {
if (isActiveOnce) {
loadMaps();
mapsDownloader.addOnMapDownloadedListener((map, targerVersion) => {
if (targerVersion !== version) {
return;
}
setMaps((maps ? [map, ...maps] : [map]));
});
}
return () => {
@@ -93,6 +88,22 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
};
}, [isActiveOnce, version, linked]);
useEffect(() => {
if(isActiveOnce){
mapsDownloader.addOnMapDownloadedListener((map, targetVersion) => {
if (!equal(targetVersion, version)) {
return;
}
setMaps((maps ? [map, ...maps] : [map]));
});
}
return () => {
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
}
}, [isActiveOnce, version, maps])
useEffect(() => {
if (!isActiveOnce) {
return;
@@ -1,6 +1,6 @@
import { ModalComponent } from "renderer/services/modale.service"
import { PlaylistDetailsTemplate, PlaylistDetailsTemplateProps } from "./playlist-details-template.component"
import { Observable } from "rxjs"
import { Observable, first, lastValueFrom, map, shareReplay, switchMap, take, tap } from "rxjs"
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
import { BSVersion } from "shared/bs-version.interface";
import { useObservable } from "renderer/hooks/use-observable.hook";
@@ -13,6 +13,8 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
import { ProgressBarService } from "renderer/services/progress-bar.service";
interface Props {
version: BSVersion;
@@ -23,6 +25,8 @@ interface Props {
export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver, options}) => {
const audioPlayer = useService(AudioPlayerService);
const playlistDownloader = useService(PlaylistDownloaderService);
const progressBar = useService(ProgressBarService);
const localPlaylist = useObservable(() => options.data.localPlaylist$, null);
const installedMaps = useObservable(() => options.data.installedMaps$, null);
@@ -34,6 +38,23 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
audioPlayer.play(installedMaps.map(map => ({ src: map.songUrl, bpm: map.rawInfo?._beatsPerMinute ?? 0})));
};
const installPlaylist = () => {
const obs$ = playlistDownloader.installPlaylist(localPlaylist, options.data.version);
const progress$ = obs$.pipe(
map(progress => (progress.current / progress.total) * 100),
);
const obsWithProgress$ = obs$.pipe(
take(1),
tap(() => progressBar.show(progress$, true)),
switchMap(() => obs$),
tap({ complete: () => progressBar.hide(true) })
);
return lastValueFrom(obsWithProgress$);
}
const renderMaps = () => {
if (!installedMaps) {
// loading maps
@@ -54,14 +75,14 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
initial={{ height: 0 }}
animate={{ height: "7rem" }}
exit={{ height: 0 }}
transition={{delay: .2, duration: .25}}
transition={{delay: .25, duration: .25}}
className="shrink-0 w-full text-center overflow-hidden flex justify-center items-center"
>
<div className="size-[calc(100%-1rem)] bg-main-color-2 rounded-md translate-y-1.5 flex flex-row justify-center items-center gap-3">
<BsmImage image={BeatConflict} className="size-24"/>
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
<p>Certaines maps de cette playlist sont manquantes</p>
<BsmButton withBar={false} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="Télécharger.les.maps.manquantes"/>
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="Télécharger.les.maps.manquantes"/>
</div>
</div>
</motion.div>
@@ -30,7 +30,7 @@ export function BsmProgressBar() {
<AnimatePresence>
{" "}
{visible && (
<motion.div initial={{ y: "120%" }} animate={{ y: "0%" }} exit={{ y: "120%" }} className="w-full absolute h-14 flex justify-center items-center bottom-2 pointer-events-none z-10" style={style}>
<motion.div initial={{ y: "120%" }} animate={{ y: "0%" }} exit={{ y: "120%" }} className="w-full absolute h-14 flex justify-center items-center bottom-2 pointer-events-none z-[10000]" style={style}>
<div className={`flex items-center content-center justify-center bottom-9 z-10 rounded-full bg-light-main-color-2 dark:bg-main-color-2 shadow-center shadow-black transition-all duration-300 ${!progressValue && "h-14 w-14 "} ${!!progressValue && "h-5 w-3/4 p-[6px]"}`}>
{!!progressValue && (
<div className="relative h-full w-full rounded-full bg-black">
+5 -5
View File
@@ -7,7 +7,7 @@ import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/
export class IpcService {
private static instance: IpcService;
private readonly channelObservables: Map<string, Observable<IpcResponse<unknown>>>;
private readonly channelObservables: Map<string, Observable<unknown>>;
public static getInstance(): IpcService {
if (!IpcService.instance) {
@@ -45,13 +45,13 @@ export class IpcService {
}
// Also need a rework
public watch<T>(channel: string): Observable<IpcResponse<T>> {
public watch<T>(channel: string): Observable<T> {
if (this.channelObservables.has(channel)) {
return this.channelObservables.get(channel) as Observable<IpcResponse<T>>;
return this.channelObservables.get(channel) as Observable<T>;
}
const obs = new Observable<IpcResponse<T>>(observer => {
window.electron.ipcRenderer.on(channel, (res: IpcResponse<T>) => {
const obs = new Observable<T>(observer => {
window.electron.ipcRenderer.on(channel, (res) => {
observer.next(res);
});
});
@@ -1,6 +1,6 @@
import { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
import { map, filter } from "rxjs/operators";
import { BehaviorSubject, timer, Observable } from "rxjs";
import { BehaviorSubject, timer, Observable, lastValueFrom } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { ModalResponse, ModalService } from "./modale.service";
import { ProgressBarService } from "./progress-bar.service";
@@ -43,6 +43,11 @@ export class MapsDownloaderService {
this.mapsQueue$.pipe(filter(queue => queue.length === 0)).subscribe(() => {
this.queueMaxLenght = 0;
});
this.ipc.watch<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded").subscribe((data) => {
console.log(data);
this.downloadedListerners.forEach(func => func(data.map, data.version));
});
}
private async startDownloadMaps() {
@@ -53,11 +58,7 @@ export class MapsDownloaderService {
while (this.mapsQueue$.value.at(0)) {
const toDownload = this.mapsQueue$.value.at(0);
this.currentDownload$.next(toDownload);
const downloaded = await this.downloadMap(toDownload.map, toDownload.version)?.toPromise();
if (downloaded) {
this.downloadedListerners.forEach(func => func(downloaded, toDownload.version));
}
await lastValueFrom(this.downloadMap(toDownload.map, toDownload.version));
const newArr = [...this.mapsQueue$.value];
newArr.shift();
@@ -1,8 +1,10 @@
import { Observable, map, tap, throwError } from "rxjs";
import { DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
import { BehaviorSubject, Observable, filter, lastValueFrom, map, shareReplay, take, tap } from "rxjs";
import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
import { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
import { Progression } from "main/helpers/fs.helpers";
import equal from "fast-deep-equal";
import { BSVersion } from "shared/bs-version.interface";
export class PlaylistDownloaderService {
private static instance: PlaylistDownloaderService;
@@ -18,17 +20,36 @@ export class PlaylistDownloaderService {
private readonly progress: ProgressBarService;
private readonly ipc: IpcService;
private readonly playlistQueue$ = new BehaviorSubject<BPList[]>([]);
private constructor() {
this.progress = ProgressBarService.getInstance();
this.ipc = IpcService.getInstance();
}
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
public installPlaylist(bpList: BPList, version?: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
this.playlistQueue$.next([...this.playlistQueue$.value, bpList]);
if(!this.progress.require()){
return throwError(() => new Error("Download already in progress"));
const clear = () => {
this.playlistQueue$.next(this.playlistQueue$.value.filter(p => p !== bpList));
}
return new Observable<Progression<DownloadPlaylistProgressionData>>(subscriber => {
(async () => {
const playlist = await lastValueFrom(this.playlistQueue$.pipe(map(queue => queue.at(0)), filter(p => equal(bpList, p)), take(1)));
const download$ = this.ipc.sendV2<Progression<DownloadPlaylistProgressionData>, unknown>("install-playlist", { args: { version, playlist } });
await lastValueFrom(download$.pipe(tap(subscriber)));
})()
.then(() => subscriber.complete())
.catch(err => subscriber.error(err))
.finally(clear);
}).pipe(shareReplay(1))
}
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
const download$ = this.ipc.sendV2<Progression<DownloadPlaylistProgressionData>, string>("one-click-install-playlist", { args: bpListUrl });
const progress$ = download$.pipe(map(data => (data.current / data.total) * 100));