mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge branch 'v1.5.0' into feature/playlists/107
This commit is contained in:
@@ -11,14 +11,13 @@ import sanitize from "sanitize-filename";
|
||||
import { Progression, ensureFolderExist, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { MODEL_FILE_EXTENSIONS, MODEL_TYPES, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { Observable, Subscription, lastValueFrom, of } from "rxjs";
|
||||
import { Observable, Subscription, lastValueFrom } from "rxjs";
|
||||
import { readdir } from "fs/promises";
|
||||
import md5File from "md5-file";
|
||||
import { allSettled } from "../../../shared/helpers/promise.helpers";
|
||||
import { ModelSaberService } from "../thrid-party/model-saber/model-saber.service";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { Archive } from "../../models/archive.class";
|
||||
import { IpcService } from "../ipc.service";
|
||||
|
||||
export class LocalModelsManagerService {
|
||||
private static instance: LocalModelsManagerService;
|
||||
@@ -40,7 +39,6 @@ export class LocalModelsManagerService {
|
||||
private readonly installPaths: InstallationLocationService;
|
||||
private readonly request: RequestService;
|
||||
private readonly modelSaber: ModelSaberService;
|
||||
private readonly ipc: IpcService;
|
||||
|
||||
private constructor() {
|
||||
this.deepLink = DeepLinkService.getInstance();
|
||||
@@ -49,26 +47,14 @@ export class LocalModelsManagerService {
|
||||
this.request = RequestService.getInstance();
|
||||
this.installPaths = InstallationLocationService.getInstance();
|
||||
this.modelSaber = ModelSaberService.getInstance();
|
||||
this.ipc = IpcService.getInstance();
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ModelSaber, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ModelSaber, link);
|
||||
const url = new URL(link);
|
||||
|
||||
const type = url.host;
|
||||
const url = new URL(link);
|
||||
const id = url.pathname.replace("/", "").split("/").at(0);
|
||||
|
||||
this.openOneClickDownloadModelWindow(id, type);
|
||||
});
|
||||
}
|
||||
|
||||
private openOneClickDownloadModelWindow(id: string, type: string) {
|
||||
this.windows.openWindow("oneclick-download-model.html").then(window => {
|
||||
|
||||
this.ipc.once("one-click-model-info", (_, reply) => {
|
||||
reply(of({ id, type }));
|
||||
}, window.webContents.ipc);
|
||||
|
||||
this.windows.openWindow(`oneclick-download-model.html?modelId=${id}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export class LocalPlaylistsManagerService {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
|
||||
const url = new URL(link);
|
||||
const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") : "";
|
||||
this.openOneClickDownloadPlaylistWindow(bplistUrl);
|
||||
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${bplistUrl}`);
|
||||
});
|
||||
|
||||
this.fileAssociation.registerFileAssociation(".bplist", filePath => {
|
||||
|
||||
@@ -14,18 +14,18 @@ import sanitize from "sanitize-filename";
|
||||
import { DeepLinkService } from "../../deep-link.service";
|
||||
import log from "electron-log";
|
||||
import { WindowManagerService } from "../../window-manager.service";
|
||||
import { Observable, lastValueFrom, of } from "rxjs";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { Archive } from "../../../models/archive.class";
|
||||
import { deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
|
||||
import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
import { FolderLinkerService } from "../../folder-linker.service";
|
||||
import { allSettled } from "../../../../shared/helpers/promise.helpers";
|
||||
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
|
||||
import { IpcService } from "../../ipc.service";
|
||||
import { SongDetailsCacheService } from "./song-details-cache.service";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { SongCacheService } from "./song-cache.service";
|
||||
import { IpcService } from "../../ipc.service";
|
||||
import { pathToFileURL } from "url";
|
||||
import { sToMs } from "../../../../shared/helpers/time.helpers";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
@@ -53,10 +53,10 @@ export class LocalMapsManagerService {
|
||||
private readonly reqService: RequestService;
|
||||
private readonly deepLink: DeepLinkService;
|
||||
private readonly windows: WindowManagerService;
|
||||
private readonly ipc: IpcService;
|
||||
private readonly linker: FolderLinkerService;
|
||||
private readonly songDetailsCache: SongDetailsCacheService;
|
||||
private readonly songCache: SongCacheService;
|
||||
private readonly ipc: IpcService;
|
||||
|
||||
private constructor() {
|
||||
this.localVersion = BSLocalVersionService.getInstance();
|
||||
@@ -66,18 +66,22 @@ export class LocalMapsManagerService {
|
||||
this.deepLink = DeepLinkService.getInstance();
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
this.linker = FolderLinkerService.getInstance();
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.songDetailsCache = SongDetailsCacheService.getInstance();
|
||||
this.songCache = SongCacheService.getInstance();
|
||||
this.ipc = IpcService.getInstance();
|
||||
|
||||
const handleOneClick = (mapId: string, isHash = false) => {
|
||||
this.windows.openWindow(`oneclick-download-map.html?mapId=${mapId}&isHash=${isHash}`);
|
||||
}
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
|
||||
this.openOneClickDownloadMapWindow(new URL(link).host);
|
||||
handleOneClick(new URL(link).host);
|
||||
});
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ScoreSaber, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ScoreSaber, link);
|
||||
this.openOneClickDownloadMapWindow(new URL(link).host, true);
|
||||
handleOneClick(new URL(link).host, true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,14 +161,6 @@ export class LocalMapsManagerService {
|
||||
return { zip, zipPath };
|
||||
}
|
||||
|
||||
private openOneClickDownloadMapWindow(mapId: string, isHash = false): void {
|
||||
this.windows.openWindow("oneclick-download-map.html").then(window => {
|
||||
this.ipc.once("one-click-map-info", async (_, reply) => {
|
||||
reply(of({ id: mapId, isHash }));
|
||||
}, window.webContents.ipc);
|
||||
});
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMapsProgress> {
|
||||
const progression: BsmLocalMapsProgress = {
|
||||
total: 0,
|
||||
@@ -307,7 +303,7 @@ export class LocalMapsManagerService {
|
||||
return localMap;
|
||||
}
|
||||
|
||||
public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string) {
|
||||
public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string): Promise<Observable<Progression>> {
|
||||
const archive = new Archive(outPath);
|
||||
|
||||
if (!maps || maps.length === 0) {
|
||||
|
||||
@@ -128,7 +128,7 @@ export class BSLauncherService {
|
||||
}
|
||||
|
||||
private createShortcutPngBuffer(color: Color): Promise<Buffer>{
|
||||
|
||||
|
||||
const svgBuffer = Buffer.from(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406.4 406.4" height="406.4" width="406.4">
|
||||
<rect rx="69.453" height="406.4" width="406.4" fill="${color.hex()}"/>
|
||||
@@ -141,8 +141,8 @@ export class BSLauncherService {
|
||||
|
||||
/**
|
||||
* Create .png file for the shortcut with the given color
|
||||
* @param {Color} color
|
||||
* @returns {Promise<string>} Path of the icon
|
||||
* @param {Color} color
|
||||
* @returns {Promise<string>} Path of the icon
|
||||
*/
|
||||
private async createShortcutPng(color: Color): Promise<string>{
|
||||
const pngBuffer = this.createShortcutPngBuffer(color);
|
||||
@@ -211,25 +211,25 @@ export class BSLauncherService {
|
||||
const bsPath: string = await (async () => {
|
||||
const bsPath = await this.localVersionService.getInstalledVersionPath(launchOption.version);
|
||||
return bsPath ?? this.localVersionService.getVersionPath(launchOption.version);
|
||||
})().catch(e => {
|
||||
log.error(e);
|
||||
})().catch(e => {
|
||||
log.error(e);
|
||||
return null;
|
||||
});
|
||||
|
||||
launchOption.version = (await this.localVersionService.getVersionOfBSFolder(bsPath, {
|
||||
steam: launchOption.version.steam,
|
||||
oculus: launchOption.version.oculus,
|
||||
oculus: launchOption.version.oculus,
|
||||
})) ?? launchOption.version;
|
||||
|
||||
launchOption.version = {...(await this.remoteVersion.getVersionDetails(launchOption.version.BSVersion)), ...launchOption.version};
|
||||
|
||||
this.ipc.once("shortcut-launch-options", (_data, reply) => {
|
||||
|
||||
this.ipc.once("shortcut-launch-options", (_, reply) => {
|
||||
reply(of(launchOption));
|
||||
});
|
||||
|
||||
this.windows.openWindow("shortcut-launch.html");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
type ShortcutParams = {
|
||||
@@ -246,9 +246,9 @@ type ShortcutParams = {
|
||||
|
||||
/**
|
||||
* Create .desktop file for url shortcut (only for linux)
|
||||
* @param {string} shortcutPath
|
||||
* @param options
|
||||
* @returns
|
||||
* @param {string} shortcutPath
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
function createDesktopUrlShortcut(shortcutPath: string, options?: {
|
||||
url: string
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { BSVersion } from "../../../shared/bs-version.interface";
|
||||
import { WindowManagerService } from "../window-manager.service";
|
||||
import { minToMs, msToS } from "../../../shared/helpers/time.helpers";
|
||||
import log from "electron-log";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { OculusDownloader } from "../../models/oculus-downloader.class";
|
||||
import { Cookie, session } from "electron";
|
||||
import { Progression, ensurePathNotAlreadyExist } from "../../helpers/fs.helpers";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import { Observable, finalize, from, map, of, switchMap } from "rxjs";
|
||||
import { Observable, finalize, map, of, switchMap } from "rxjs";
|
||||
import path from "path";
|
||||
import { DownloadInfo } from "./bs-steam-downloader.service";
|
||||
import { BsStore } from "../../../shared/models/bs-store.enum";
|
||||
@@ -26,90 +22,19 @@ export class BsOculusDownloaderService {
|
||||
}
|
||||
|
||||
private readonly oculusDownloader: OculusDownloader;
|
||||
|
||||
private readonly windows: WindowManagerService;
|
||||
private readonly versions: BSLocalVersionService;
|
||||
|
||||
private constructor() {
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
this.versions = BSLocalVersionService.getInstance();
|
||||
|
||||
this.oculusDownloader = new OculusDownloader();
|
||||
}
|
||||
|
||||
private isUserTokenValid(token: string): boolean{
|
||||
return isOculusTokenValid(token, log.info);
|
||||
}
|
||||
|
||||
private isCookieValid(cookie: Cookie): boolean {
|
||||
|
||||
if(!cookie){
|
||||
return false;
|
||||
}
|
||||
|
||||
return cookie.expirationDate > msToS(Date.now());
|
||||
}
|
||||
|
||||
public async getAuthToken(): Promise<string | undefined> {
|
||||
const cookie = await session.defaultSession.cookies.get({ name: "oc_www_at" }).then(a => a?.at(0));
|
||||
|
||||
if(this.isCookieValid(cookie) && this.isUserTokenValid(cookie.value)){
|
||||
return cookie.value;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getUserTokenFromMetaAuth(keepToken: boolean): Promise<string>{
|
||||
|
||||
const redirectUrl = "https://developer.oculus.com/manage/";
|
||||
const loginUrl = `https://auth.oculus.com/login/?redirect_uri=${encodeURIComponent(redirectUrl)}`;
|
||||
const window = await this.windows.openWindow(loginUrl, { frame: true, width: 650, height: 800 });
|
||||
|
||||
let timout: NodeJS.Timeout;
|
||||
|
||||
const promise = new Promise<string>((resolve, reject) => {
|
||||
timout = setTimeout(() => {
|
||||
reject(new CustomError("Trying to get Oculus user token timed out", "META_LOGIN_TIMED_OUT"));
|
||||
window.close();
|
||||
}, minToMs(5));
|
||||
|
||||
window.webContents.on("did-navigate", async (_, url) => {
|
||||
if(!url.startsWith(redirectUrl)){ return; }
|
||||
|
||||
const token = (await window.webContents.session.cookies.get({ name: "oc_www_at" })).at(0)?.value;
|
||||
|
||||
if(!this.isUserTokenValid(token)){
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(token);
|
||||
});
|
||||
|
||||
window.on("closed", () => {
|
||||
reject(new CustomError("Oculus login window closed by user", "META_LOGIN_WINDOW_CLOSED_BY_USER"));
|
||||
});
|
||||
}).finally(() => {
|
||||
|
||||
clearTimeout(timout);
|
||||
|
||||
if(!keepToken){
|
||||
this.clearAuthToken();
|
||||
}
|
||||
|
||||
if(!window.isDestroyed() && window.isClosable()){
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private async createDownloadVersion(version: BSVersion): Promise<{version: BSVersion, dest: string}>{
|
||||
const dest = await ensurePathNotAlreadyExist(await this.versions.getVersionPath(version));
|
||||
return {
|
||||
version: {
|
||||
...version,
|
||||
...version,
|
||||
...(path.basename(dest) !== version.BSVersion && { name: path.basename(dest) }),
|
||||
metadata: { store: BsStore.OCULUS, id: "" }
|
||||
},
|
||||
@@ -125,9 +50,7 @@ export class BsOculusDownloaderService {
|
||||
|
||||
let downloadVersion: BSVersion
|
||||
|
||||
const tokenObs$ = downloadInfo.token ? of(downloadInfo.token) : from(this.getUserTokenFromMetaAuth(downloadInfo.stay));
|
||||
|
||||
return tokenObs$.pipe(
|
||||
return of(downloadInfo.token).pipe(
|
||||
switchMap(token => {
|
||||
isOculusTokenValid(token, log.info); // Log token validity
|
||||
if(!downloadInfo.isVerification){
|
||||
@@ -138,7 +61,7 @@ export class BsOculusDownloaderService {
|
||||
switchMap(({token, version, dest}) => {
|
||||
downloadVersion = version;
|
||||
return this.oculusDownloader.downloadApp({ accessToken: token, binaryId: version.OculusBinaryId, destination: dest }).pipe(map(
|
||||
progress => ({...progress, data: version})
|
||||
progress => ({...progress, data: version})
|
||||
));
|
||||
}),
|
||||
finalize(() => downloadVersion && this.versions.initVersionMetadata(downloadVersion, { store: BsStore.OCULUS })),
|
||||
@@ -146,43 +69,9 @@ export class BsOculusDownloaderService {
|
||||
);
|
||||
}
|
||||
|
||||
public autoDownloadVersion(downloadInfo: DownloadInfo): Observable<Progression<BSVersion>>{
|
||||
|
||||
let downloadVersion: BSVersion
|
||||
|
||||
const tokenObs$ = downloadInfo.token ? of(downloadInfo.token) : from(this.getAuthToken());
|
||||
|
||||
return tokenObs$.pipe(
|
||||
map(token => {
|
||||
if(!token){
|
||||
throw new CustomError("No Meta auth token was found in cookies for auto download", "NO_META_AUTH_TOKEN");
|
||||
}
|
||||
return token;
|
||||
}),
|
||||
switchMap(token => {
|
||||
if(!downloadInfo.isVerification){
|
||||
return this.createDownloadVersion(downloadInfo.bsVersion).then(({version, dest}) => ({token, version, dest}));
|
||||
}
|
||||
return this.versions.getVersionPath(downloadInfo.bsVersion).then(path => ({token, version: downloadInfo.bsVersion, dest: path}));
|
||||
}),
|
||||
switchMap(({token, version, dest}) => {
|
||||
downloadVersion = version;
|
||||
return this.oculusDownloader.downloadApp({ accessToken: token, binaryId: version.OculusBinaryId, destination: dest }).pipe(
|
||||
map(progress => ({...progress, data: version})),
|
||||
);
|
||||
}),
|
||||
finalize(() => downloadVersion && this.versions.initVersionMetadata(downloadVersion, { store: BsStore.OCULUS })),
|
||||
finalize(() => this.oculusDownloader.stopDownload()),
|
||||
);
|
||||
}
|
||||
|
||||
public clearAuthToken(): Promise<void>{
|
||||
return session.defaultSession.clearStorageData({ storages: ["cookies"], origin: ".oculus.com" })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface OculusDownloadInfo {
|
||||
version: BSVersion;
|
||||
stay?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { BrowserWindow, IpcMainEvent, ipcMain } from "electron";
|
||||
import { BrowserWindow, IpcMainEvent, WebContents, ipcMain } from "electron";
|
||||
import { Observable } from "rxjs";
|
||||
import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/models/ipc/ipc-response.interface";
|
||||
import { IpcReplier } from "shared/models/ipc/ipc-request.interface";
|
||||
import { serializeError } from 'serialize-error';
|
||||
import log from "electron-log";
|
||||
import { IpcChannels, IpcRequestType, IpcResponseType } from "shared/models/ipc/ipc-routes";
|
||||
|
||||
export class IpcService {
|
||||
private static instance: IpcService;
|
||||
@@ -30,11 +31,11 @@ export class IpcService {
|
||||
return `${channel}_teardown`;
|
||||
}
|
||||
|
||||
private buildProxyListener<T>(listener: IpcListener<T>) {
|
||||
return (event: IpcMainEvent, req: IpcRequest<T>) => {
|
||||
private buildProxyListener<C extends IpcChannels>(listener: IpcListenerFromChannel<C>) {
|
||||
return (event: IpcMainEvent, req: IpcRequest<IpcRequestType<C>>) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
const replier = (data: Observable<unknown>) => this.connectStream(req.responceChannel, window, data);
|
||||
listener(req, replier);
|
||||
listener(req.args, replier, event.sender);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,24 +55,26 @@ export class IpcService {
|
||||
complete: () => this.send(this.getCompleteChannel(channel), window)
|
||||
})
|
||||
|
||||
const unsubscribeOnDestroy = () => sub.unsubscribe();
|
||||
const unsubscribe = () => sub.unsubscribe();
|
||||
|
||||
window.webContents.once("destroyed", unsubscribeOnDestroy);
|
||||
window.webContents.ipc.once(this.getTearDownChannel(channel), () => sub.unsubscribe());
|
||||
window.webContents.once("destroyed", unsubscribe);
|
||||
window.webContents.ipc.once(this.getTearDownChannel(channel), unsubscribe);
|
||||
|
||||
sub.add(() => {
|
||||
window.webContents.removeListener("destroyed", unsubscribeOnDestroy);
|
||||
window.webContents.ipc.removeAllListeners(this.getTearDownChannel(channel));
|
||||
window.webContents.removeListener("destroyed", unsubscribe);
|
||||
});
|
||||
}
|
||||
|
||||
public on<T>(channel: string, listener: IpcListener<T>, ipc = ipcMain): void {
|
||||
ipc.on(channel, this.buildProxyListener(listener));
|
||||
public on<C extends IpcChannels>(channel: C, listener: IpcListenerFromChannel<C>, ipc = ipcMain): void {
|
||||
ipc.on(channel as string, this.buildProxyListener(listener));
|
||||
}
|
||||
|
||||
public once<T>(channel: string, listener: IpcListener<T>, ipc = ipcMain): void {
|
||||
ipc.once(channel, this.buildProxyListener(listener));
|
||||
public once<C extends IpcChannels>(channel: C, listener: IpcListenerFromChannel<C>, ipc = ipcMain): void {
|
||||
ipc.once(channel as string, this.buildProxyListener(listener));
|
||||
}
|
||||
}
|
||||
|
||||
type IpcListener<T = unknown> = (req: IpcRequest<T>, replier: IpcReplier) => void | Promise<void>;
|
||||
|
||||
type IpcListener<TRequest = unknown, TResponse = unknown> = (req: TRequest, replier: IpcReplier<TResponse>, webContents: WebContents) => void | Promise<void>;
|
||||
type IpcListenerFromChannel<C extends IpcChannels> = IpcListener<IpcRequestType<C>, IpcResponseType<C>>;
|
||||
|
||||
@@ -155,8 +155,13 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
log.info("START IPA PROCESS", `start /wait /min "" "${ipaPath}" ${args.join(" ")}`);
|
||||
const processIPA = spawn(`start /wait /min "" "${ipaPath}" ${args.join(" ")}`, { cwd: versionPath, detached: true, shell: true });
|
||||
const cmd = process.platform === 'linux'
|
||||
? `screen -dmS "BSIPA" dotnet ${ipaPath} ${args.join(" ")}` // Must run through screen, otherwise BSIPA tries to move console cursor and crashes.
|
||||
: `start /wait /min "" "${ipaPath}" ${args.join(" ")}`;
|
||||
|
||||
log.info("START IPA PROCESS", cmd);
|
||||
const processIPA = spawn(cmd, { cwd: versionPath, detached: true, shell: true });
|
||||
|
||||
processIPA.once("exit", code => {
|
||||
if (code === 0) {
|
||||
log.info("Ipa process exist with code 0");
|
||||
@@ -230,7 +235,7 @@ export class BsModsManagerService {
|
||||
log.error("Error while extracting mod zip", e);
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
log.info("Mod zip extraction end", mod.name, "to", destDir, "success:", extracted);
|
||||
|
||||
const res = isBSIPA
|
||||
@@ -241,7 +246,9 @@ export class BsModsManagerService {
|
||||
}))
|
||||
: extracted;
|
||||
|
||||
res && this.nbInstalledMods++;
|
||||
if(res){
|
||||
this.nbInstalledMods++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Agent, get } from "https";
|
||||
import { Agent, RequestOptions, get } from "https";
|
||||
import { createWriteStream, unlink } from "fs";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable, shareReplay, tap } from "rxjs";
|
||||
import log from "electron-log";
|
||||
import fetch, { RequestInfo, RequestInit } from "node-fetch";
|
||||
import got, { GotOptions } from "got";
|
||||
import got, { Options } from "got";
|
||||
import { IncomingMessage } from "http";
|
||||
import { app } from "electron";
|
||||
import os from "os";
|
||||
|
||||
export class RequestService {
|
||||
private static instance: RequestService;
|
||||
@@ -17,16 +19,33 @@ export class RequestService {
|
||||
return RequestService.instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
private readonly defaultRequestInit: RequestInit;
|
||||
|
||||
private get ipv4Agent(){
|
||||
return new Agent({ family: 4 });
|
||||
private constructor() {
|
||||
|
||||
this.defaultRequestInit = {
|
||||
headers: {
|
||||
"User-Agent": `BSManager/${app.getVersion()} (${os.type()} ${os.release()})`
|
||||
},
|
||||
agent: new Agent({ family: 4 }),
|
||||
};
|
||||
}
|
||||
|
||||
private getInitWithOptions(options?: RequestInit): RequestInit {
|
||||
return { ...this.defaultRequestInit, ...(options || {}) };
|
||||
}
|
||||
|
||||
private requestOptionsFromDefaultInit(): RequestOptions {
|
||||
return {
|
||||
headers: this.defaultRequestInit.headers as Record<string, string>,
|
||||
agent: this.defaultRequestInit.agent as Agent,
|
||||
};
|
||||
}
|
||||
|
||||
public async getJSON<T = unknown>(url: RequestInfo, options?: RequestInit): Promise<T> {
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {...options, agent: this.ipv4Agent});
|
||||
const response = await fetch(url, this.getInitWithOptions(options));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status} ${url}`);
|
||||
@@ -52,7 +71,7 @@ export class RequestService {
|
||||
});
|
||||
file.on("error", err => unlink(dest, () => subscriber.error(err)));
|
||||
|
||||
const req = get(url, { agent: this.ipv4Agent }, res => {
|
||||
const req = get(url, this.requestOptionsFromDefaultInit(), res => {
|
||||
progress.total = parseInt(res.headers?.["content-length"] || "0", 10);
|
||||
|
||||
res.on("data", chunk => {
|
||||
@@ -69,9 +88,9 @@ export class RequestService {
|
||||
}).pipe(tap({ error: e => log.error(e, url, dest) }), shareReplay(1));
|
||||
}
|
||||
|
||||
public downloadBuffer(url: string, options?: GotOptions<string>): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
return new Observable<Progression<Buffer>>(subscriber => {
|
||||
const progress: Progression<Buffer> = {
|
||||
public downloadBuffer(url: string, options?: Options & { isStream?: true }): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
return new Observable<Progression<Buffer, IncomingMessage>>(subscriber => {
|
||||
const progress: Progression<Buffer, IncomingMessage> = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
data: null,
|
||||
@@ -113,6 +132,6 @@ export class RequestService {
|
||||
req.destroy();
|
||||
}
|
||||
|
||||
}).pipe(tap({ error: e => log.error(e) }), shareReplay(1));
|
||||
}).pipe(tap({ error: log.error }), shareReplay(1))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user