diff --git a/.eslintrc.js b/.eslintrc.js index 86e6ab35..2c33df04 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,7 +33,8 @@ module.exports = { "@typescript-eslint/no-unused-expressions": "off", "no-param-reassign": "off", "no-useless-escape": "off", - "jsx-a11y/no-noninteractive-element-interactions": "off" + "jsx-a11y/no-noninteractive-element-interactions": "off", + "no-async-promise-executor": "off" }, parserOptions: { ecmaVersion: 2020, diff --git a/src/main/ipcs/bs-playlist-ipcs.ts b/src/main/ipcs/bs-playlist-ipcs.ts new file mode 100644 index 00000000..233000c3 --- /dev/null +++ b/src/main/ipcs/bs-playlist-ipcs.ts @@ -0,0 +1,22 @@ +import { ipcMain } from "electron"; +import { IpcRequest } from "shared/models/ipc"; +import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service"; +import { UtilsService } from "../services/utils.service"; + +ipcMain.on("one-click-install-playlist", async (event, request: IpcRequest) => { + + console.log("OUI"); + + const utils = UtilsService.getInstance(); + const playlists = LocalPlaylistsManagerService.getInstance(); + + console.log(request.args); + + playlists.oneClickInstallPlaylist(request.args).then(() => { + console.log("C FINI"); + utils.ipcSend(request.responceChannel, {success: true}); + }).catch(e => { + console.log(e); + utils.ipcSend(request.responceChannel, {success: false, error: e}); + }); +}); \ No newline at end of file diff --git a/src/main/ipcs/index.ts b/src/main/ipcs/index.ts index ebb87582..78cfbd31 100644 --- a/src/main/ipcs/index.ts +++ b/src/main/ipcs/index.ts @@ -8,4 +8,5 @@ import './launcher-ipcs'; import './window-manager-ipcs'; import './bs-mods-ipcs'; import './bs-maps-ipcs'; -import './beat-saver-ipcs' \ No newline at end of file +import './beat-saver-ipcs'; +import './bs-playlist-ipcs'; \ No newline at end of file diff --git a/src/main/services/additional-content/local-maps-manager.service.ts b/src/main/services/additional-content/local-maps-manager.service.ts index b4c86f0d..2079a51d 100644 --- a/src/main/services/additional-content/local-maps-manager.service.ts +++ b/src/main/services/additional-content/local-maps-manager.service.ts @@ -61,7 +61,7 @@ export class LocalMapsManagerService { }); } - private async getMapsFolderPath(version?: BSVersion): Promise{ + public async getMapsFolderPath(version?: BSVersion): Promise{ if(version){ return path.join(await this.localVersion.getVersionPath(version), this.LEVELS_ROOT_FOLDER, this.CUSTOM_LEVELS_FOLDER); } const sharedMapsPath = path.join(this.installLocation.sharedMapsPath, this.CUSTOM_LEVELS_FOLDER); if(!(await this.utils.pathExist(sharedMapsPath))){ diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index b7790156..599a7feb 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -1,7 +1,6 @@ import path from "path"; import { BehaviorSubject, Observable } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; -import { BsvPlaylist, BsvPlaylistPage } from "shared/models/maps/beat-saver.model"; import { BSLocalVersionService } from "../bs-local-version.service"; import { DeepLinkService } from "../deep-link.service"; import { RequestService } from "../request.service"; @@ -12,8 +11,11 @@ import { isValidUrl } from "../../helpers/url.helpers"; import { ipcMain } from "electron"; import { WindowManagerService } from "../window-manager.service"; import { IpcRequest } from "shared/models/ipc"; -import { BPList } from "shared/models/playlists/playlist.interface"; -import { readFileSync } from "fs"; +import { BPList, DownloadPlaylistProgression } from "shared/models/playlists/playlist.interface"; +import { copyFileSync, readFileSync } from "fs"; +import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service"; +import { copySync } from "fs-extra"; +import { timer } from "rxjs"; export class LocalPlaylistsManagerService { @@ -36,6 +38,7 @@ export class LocalPlaylistsManagerService { private readonly request: RequestService; private readonly deepLink: DeepLinkService; private readonly windows: WindowManagerService; + private readonly bsaver: BeatSaverService; private constructor(){ this.maps = LocalMapsManagerService.getInstance(); @@ -44,6 +47,7 @@ export class LocalPlaylistsManagerService { this.request = RequestService.getInstance(); this.deepLink = DeepLinkService.getInstance(); this.windows = WindowManagerService.getInstance(); + this.bsaver = BeatSaverService.getInstance(); this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => { @@ -69,29 +73,54 @@ export class LocalPlaylistsManagerService { return splited[idIndex + 1]; } - private async downloadPlaylistFile(playlist: BsvPlaylist, version: BSVersion): Promise{ + private async getPlaylistsFolder(version?: BSVersion){ + if(!version){ throw "Playlists are not available to be linked yet" } + const versionFolder = await this.versions.getVersionPath(version); - const bpListUrl = playlist.downloadURL; - const bsListDest = path.join(versionFolder, this.PLAYLISTS_FOLDER, path.basename(bpListUrl)); + const folder = path.join(versionFolder, this.PLAYLISTS_FOLDER); - return this.request.downloadFile(bpListUrl, bsListDest); + await this.utils.createFolderIfNotExist(folder) + + return folder; + + } + + private async installBPListFile(bpListUrlOrPath: string, version: BSVersion): Promise{ + + const playlistFolder = await this.getPlaylistsFolder(version); + + const bpListDest = path.join(playlistFolder, path.basename(bpListUrlOrPath)); + + console.log("INSTALL BPLIST", bpListUrlOrPath, playlistFolder); + + if(this.utils.pathExist(bpListUrlOrPath)){ + console.log("PATH EXIST", bpListUrlOrPath); + console.log("COPY", bpListUrlOrPath, bpListDest); + copyFileSync(bpListUrlOrPath, bpListDest); + } + else{ + await this.request.downloadFile(bpListUrlOrPath, bpListDest); + } + + return bpListDest; } private async readPlaylistFile(path: string): Promise{ - if(!this.utils.pathExist(path)){ return null; } + if(!this.utils.pathExist(path)){ throw `bplist file not exist at ${path}`; } - const content = readFileSync(path).toJSON(); + const rawContent = readFileSync(path).toString(); + return JSON.parse(rawContent); - // TODO } private openOneClickDownloadPlaylistWindow(downloadUrl: string): void{ + // TODO make once ipcMain.on("one-click-playlist-info", async (event, req: IpcRequest) => { this.utils.ipcSend(req.responceChannel, {success: true, data: {bpListUrl: downloadUrl, id: this.getPlaylistIdFromDownloadUrl(downloadUrl)}}); }); @@ -100,30 +129,98 @@ export class LocalPlaylistsManagerService { } - public downloadPlaylist(playlistPage: BsvPlaylistPage, version: BSVersion): Observable{ + public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable{ - const res = new BehaviorSubject([]); + console.log("DOWNLOAD PLAYLIST"); + + const res = new BehaviorSubject({progression: 0, current: null, downloadedMaps: [], mapsPath: [], bpListPath: ""}); + + const sub = res.subscribe(process => { + console.log("PROGRESS", process); + this.utils.ipcSend("download-playlist-progress", {success: true, data: process}); + }, err => { + this.utils.ipcSend("download-playlist-progress", {success: false, error: err}); + }); const observer = async () => { - try{ - const bpListPath = await this.downloadPlaylistFile(playlistPage.playlist, version); - for(const map of playlistPage.maps){ - const mapPath = await this.maps.downloadMap(map.map, version); - res.next([...res.value, mapPath]); + try{ + + const bpListPath = await this.installBPListFile(bpListUrl, version); + + res.next({...res.value, bpListPath}); + + const bpList = await this.readPlaylistFile(bpListPath); + + for(const song of bpList.songs){ + + if(!song.key){ continue; } + + const map = await this.bsaver.getMapDetailsById(song.key); + + res.next({ + ...res.value, + current: map, + progression: ((res.value.downloadedMaps.length + .5) / bpList.songs.length) * 100 + }); + + const mapPath = await this.maps.downloadMap(map, version); + + const progression = ((res.value.downloadedMaps.length + 1) / bpList.songs.length) * 100; + + res.next({ + ...res.value, + downloadedMaps: [...res.value.downloadedMaps, map], + mapsPath: [...res.value.mapsPath, mapPath], + progression + }); + } - res.next([...res.value, bpListPath]); - res.complete(); } catch(e){ res.error(e); } + + res.complete(); + } - observer(); + observer().finally(() => sub.unsubscribe()); return res.asObservable(); } + public async oneClickInstallPlaylist(bpListUrl: string): Promise{ + + const versions = await this.versions.getInstalledVersions(); + + await timer(2); + + const firstVersion = versions.shift(); + const fistVersionLinked = await this.maps.versionIsLinked(firstVersion); + + const {bpListPath, mapsPath} = await this.downloadPlaylist(bpListUrl, firstVersion).toPromise(); + + for(const version of versions){ + + await this.installBPListFile(bpListPath, version); + + const versionIsLinked = await this.maps.versionIsLinked(version); + + if(fistVersionLinked && versionIsLinked){ continue; } + + for(const mapPath of mapsPath){ + const versionMapsFolder = await this.maps.getMapsFolderPath(version); + const mapDest = path.join(versionMapsFolder, path.basename(mapPath)); + + console.log("COPY MAP", mapPath, mapDest); + + copySync(mapPath, mapDest, {overwrite: true}); + } + + } + + } + } \ No newline at end of file diff --git a/src/main/services/utils.service.ts b/src/main/services/utils.service.ts index 5735bc4c..05570a25 100644 --- a/src/main/services/utils.service.ts +++ b/src/main/services/utils.service.ts @@ -98,8 +98,10 @@ export class UtilsService{ public ipcSend(channel: string, response: IpcResponse): void{ try { + console.log(channel, ...Array.from(this.windows.keys()), response); Array.from(this.windows.values()).forEach(window => window.webContents.send(channel, response)); } catch (error) { + console.log("ERREUR", error); log.error(error); } } diff --git a/src/main/services/window-manager.service.ts b/src/main/services/window-manager.service.ts index 9ae7afd4..2f60d335 100644 --- a/src/main/services/window-manager.service.ts +++ b/src/main/services/window-manager.service.ts @@ -37,8 +37,6 @@ export class WindowManagerService{ public openWindow(windowType: AppWindow, options?: BrowserWindowConstructorOptions): Promise{ const window = new BrowserWindow({...this.appWindowsOptions[windowType], ...this.baseWindowOption, ...options}); - console.log({...this.appWindowsOptions[windowType], ...this.baseWindowOption, ...options}); - const promise = window.loadURL(resolveHtmlPath(windowType)); window.removeMenu(); window.setMenu(null); diff --git a/src/renderer/services/ipc.service.ts b/src/renderer/services/ipc.service.ts index 7dfde289..0867fd95 100644 --- a/src/renderer/services/ipc.service.ts +++ b/src/renderer/services/ipc.service.ts @@ -34,10 +34,17 @@ export class IpcService { } public watch(channel: string): Observable>{ + console.log("watch", channel); if(this.channelObservables.has(channel)){ return this.channelObservables.get(channel) as Observable>; } + console.log("allo"); + + window.electron.ipcRenderer.on(channel, (res: IpcResponse) => { + console.log("oui", res); + }); const obs = new Observable>(observer => { - window.electron.ipcRenderer.on(channel, res => { + window.electron.ipcRenderer.on(channel, (res: IpcResponse) => { + console.log("oui", res); observer.next(res); }); }) diff --git a/src/renderer/services/playlist-downloader.service.ts b/src/renderer/services/playlist-downloader.service.ts new file mode 100644 index 00000000..fd6e11b4 --- /dev/null +++ b/src/renderer/services/playlist-downloader.service.ts @@ -0,0 +1,59 @@ +import { map, multicast, tap } from "rxjs/operators"; +import { Observable, Subject } from "rxjs"; +import { DownloadPlaylistProgression } from "shared/models/playlists/playlist.interface"; +import { IpcService } from "./ipc.service"; +import { ProgressBarService } from "./progress-bar.service"; +import { ProgressionInterface } from "shared/models/progress-bar"; + +export class PlaylistDownloaderService{ + + private static instance: PlaylistDownloaderService; + + public static getInstance(): PlaylistDownloaderService{ + if(!PlaylistDownloaderService.instance){ PlaylistDownloaderService.instance = new PlaylistDownloaderService(); } + return PlaylistDownloaderService.instance; + } + + private readonly progressWatcher$ = new Subject(); + + private readonly progress: ProgressBarService; + private readonly ipc: IpcService; + + private constructor(){ + this.progress = ProgressBarService.getInstance(); + this.ipc = IpcService.getInstance(); + + this.ipc.watch("download-playlist-progress").pipe(map(val => val.data)).subscribe(progress => { + this.progressWatcher$.next(progress); + }) + } + + private get downloadProgression$(): Observable{ + return this.progressWatcher$.pipe(map(value => { + return {progression: value?.progression ?? 0, label: value?.current?.name ?? ""}; + })); + } + + public oneClickInstallPlaylist(bpListUrl: string): Observable{ + + if(!this.progress.require()){ return null; } + + const res = new Subject(); + + this.progress.show(this.downloadProgression$, true); + + const sub = this.progressWatcher$.subscribe(progress => {console.log("progress"); res.next(progress)}) + + this.ipc.send("one-click-install-playlist", {args: bpListUrl}).then(oneClickRes => { + sub.unsubscribe(); + if(!oneClickRes.success){ + return res.error(oneClickRes.error); + } + res.complete(); + }) + + return res.asObservable(); + + } + +} \ No newline at end of file diff --git a/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx b/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx index 564f528b..bca0bce8 100644 --- a/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx +++ b/src/renderer/windows/OneClick/OneClickDownloadPlaylist.tsx @@ -3,6 +3,7 @@ import { BsmProgressBar } from "renderer/components/progress-bar/bsm-progress-ba import { BsmImage } from "renderer/components/shared/bsm-image.component"; import TitleBar from "renderer/components/title-bar/title-bar.component"; import { IpcService } from "renderer/services/ipc.service" +import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service"; import { ProgressBarService } from "renderer/services/progress-bar.service"; import { ThemeService } from "renderer/services/theme.service"; import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"; @@ -15,6 +16,7 @@ export default function OneClickDownloadPlaylist() { const bSaver = BeatSaverService.getInstance(); const progress = ProgressBarService.getInstance(); const themeService = ThemeService.getInstance(); + const playlistDownloader = PlaylistDownloaderService.getInstance(); const [playlist, setPlaylist] = useState(null); @@ -28,18 +30,25 @@ export default function OneClickDownloadPlaylist() { else { document.documentElement.classList.remove('dark'); } }); - progress.open(); - - ipc.send<{bpListUrl: string, id: string}>("one-click-playlist-info").then(res => { + const promise = new Promise(async (resolve, reject) => { - console.log(res); - - const playlistId = res.data.id; - const downloadUrl = res.data.bpListUrl; + const infos = await ipc.send<{bpListUrl: string, id: string}>("one-click-playlist-info"); - bSaver.getPlaylistDetailsById(playlistId).then(playlist => setPlaylist(() => playlist)); + if(!infos.success){ reject(infos.error); } + + bSaver.getPlaylistDetailsById(infos.data.id).then(details => setPlaylist(details)); + + const res = await playlistDownloader.oneClickInstallPlaylist(infos.data.bpListUrl).toPromise(); + + console.log("OMG C FINI"); + + resolve(res); }); + + + + }, []); diff --git a/src/shared/models/playlists/playlist.interface.ts b/src/shared/models/playlists/playlist.interface.ts index 18c72818..8d9d5fd5 100644 --- a/src/shared/models/playlists/playlist.interface.ts +++ b/src/shared/models/playlists/playlist.interface.ts @@ -1,3 +1,5 @@ +import { BsvMapDetail } from "../maps" + export interface BPList { playlistTitle: string, playlistAuthor: string, @@ -12,4 +14,12 @@ export interface PlaylistSong { hash: string, songName: string, uploader?: string +} + +export interface DownloadPlaylistProgression { + mapsPath: string[], + downloadedMaps: BsvMapDetail[], + bpListPath: string, + current: BsvMapDetail, + progression: number } \ No newline at end of file