import { BehaviorSubject, Observable, Subject, Subscription, distinctUntilChanged, filter, lastValueFrom, map, shareReplay, take, takeUntil, 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"; import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"; import { removeIndex } from "shared/helpers/array.helpers"; import { ModalResponse, ModalService } from "./modale.service"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { DownloadPlaylistModal } from "renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component"; import { IpcRequestType } from "shared/models/ipc/ipc-routes"; export class PlaylistDownloaderService { private static instance: PlaylistDownloaderService; public static getInstance(): PlaylistDownloaderService { if (!PlaylistDownloaderService.instance) { PlaylistDownloaderService.instance = new PlaylistDownloaderService(); } return PlaylistDownloaderService.instance; } private readonly progress: ProgressBarService; private readonly ipc: IpcService; private readonly modal: ModalService; private readonly cancel$ = new Subject(); private readonly downloadQueue$ = new BehaviorSubject([]); private readonly _currentDownload$ = new BehaviorSubject(null); private constructor() { this.progress = ProgressBarService.getInstance(); this.ipc = IpcService.getInstance(); this.modal = ModalService.getInstance(); } public downloadPlaylist(info: IpcRequestType<"download-playlist">): Observable> { this.downloadQueue$.next([...this.downloadQueue$.value, { info }]); return new Observable>(subscriber => { const subs: Subscription[] = []; let canShowProgress = false; (async () => { const queueInfo = await lastValueFrom(this.downloadQueue$.pipe(map(queue => queue.at(0)), filter(qInfo => equal(info.downloadSource, qInfo.info.downloadSource) && equal(info.version, qInfo.info.version)), take(1))); const download$ = this.ipc.sendV2("download-playlist", info).pipe(takeUntil(this.cancel$)); subs.push(download$.pipe(map(progress => progress?.data?.playlist), filter(Boolean), take(1)).subscribe(playlist => { this._currentDownload$.next({ ...queueInfo, downloaded: playlist }); })); canShowProgress = !this.progress.isVisible; if (canShowProgress) { this.progress.show(download$); } await lastValueFrom(download$.pipe(tap(subscriber))); })() .then(() => subscriber.complete()) .catch(err => subscriber.error(err)) .finally(() => { this._currentDownload$.next(null); this.downloadQueue$.next(this.downloadQueue$.value.filter(qInfo => !equal(qInfo.info.version, info.version) || !equal(qInfo.info.downloadSource, info.downloadSource))); if (canShowProgress) { this.progress.hide(); } }); return () => subs.forEach(s => s.unsubscribe()); }).pipe(shareReplay(1)); } public cancelDownload(downloadSource: string, version?: BSVersion) { const currentDownload = this.downloadQueue$.value.at(0); if(!currentDownload) { return; } if(equal(currentDownload.info.downloadSource, downloadSource) && equal(currentDownload.info.version, version)){ return this.cancel$.next(); } const indexToRemove = this.downloadQueue$.value.findIndex(qInfo => (equal(qInfo.info.downloadSource, downloadSource) && equal(qInfo.info.version, version))); if(indexToRemove === -1){ return; } const newArr = removeIndex(indexToRemove, [...this.downloadQueue$.value]); this.downloadQueue$.next(newArr); } public get currentDownload$(): Observable { return this._currentDownload$.asObservable(); } public $isPlaylistInQueue(downloadSource: string, version?: BSVersion): Observable { return this.downloadQueue$.pipe(map(queue => queue.some(qInfo => qInfo && (equal(qInfo.info.downloadSource, downloadSource) && equal(qInfo.info.version, version))), distinctUntilChanged())); } public $isPlaylistDownloading(downloadSource: string, version?: BSVersion): Observable { return this.currentDownload$.pipe(map(qInfo => { if(!qInfo){ return false; } if(!equal(qInfo.info.downloadSource, downloadSource)){ return false; } if(version && !equal(qInfo.info.version, version)){ return false; } return true; }), distinctUntilChanged()); } public oneClickInstallPlaylist(bpListUrl: string): Observable> { const download$ = this.ipc.sendV2("one-click-install-playlist", bpListUrl); const progress$ = download$.pipe(map(data => (data.current / data.total) * 100)); this.progress.show(progress$); return download$.pipe(tap({ error: () => this.progress.hide(), complete: () => this.progress.hide() })); } public openDownloadPlaylistModal(version: BSVersion, ownedPlaylists$: Observable, ownedMaps$: Observable): Promise> { return this.modal.openModal(DownloadPlaylistModal, { data: { version, ownedPlaylists$, ownedMaps$ } }) } public installPlaylistFile(bplist: BPList, version?: BSVersion, dest?: string){ return this.ipc.sendV2("install-playlist-file", { bplist, version, dest }); } } export type PlaylistQueueInfo = { info: Omit, "ignoreSongsHashs">; downloaded?: LocalBPListsDetails; }