mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge branch 'main' of github.com:hydralauncher/hydra into fix/adding-sorting-to-repacks-modal
This commit is contained in:
commit
d8158bb80e
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
.vscode
|
.vscode
|
||||||
node_modules
|
node_modules
|
||||||
hydra-download-manager/
|
hydra-download-manager/
|
||||||
|
aria2/
|
||||||
fastlist.exe
|
fastlist.exe
|
||||||
__pycache__
|
__pycache__
|
||||||
dist
|
dist
|
||||||
|
@ -3,12 +3,10 @@ productName: Hydra
|
|||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
extraResources:
|
extraResources:
|
||||||
|
- aria2
|
||||||
- hydra-download-manager
|
- hydra-download-manager
|
||||||
- seeds
|
- seeds
|
||||||
- from: node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe
|
|
||||||
to: fastlist.exe
|
|
||||||
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
|
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
|
||||||
- from: resources/hydralauncher.vbs
|
|
||||||
files:
|
files:
|
||||||
- "!**/.vscode/*"
|
- "!**/.vscode/*"
|
||||||
- "!src/*"
|
- "!src/*"
|
||||||
@ -20,7 +18,6 @@ asarUnpack:
|
|||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
executableName: Hydra
|
executableName: Hydra
|
||||||
requestedExecutionLevel: requireAdministrator
|
|
||||||
target:
|
target:
|
||||||
- nsis
|
- nsis
|
||||||
- portable
|
- portable
|
||||||
@ -33,7 +30,6 @@ nsis:
|
|||||||
allowToChangeInstallationDirectory: true
|
allowToChangeInstallationDirectory: true
|
||||||
portable:
|
portable:
|
||||||
artifactName: ${name}-${version}-portable.${ext}
|
artifactName: ${name}-${version}-portable.${ext}
|
||||||
requestExecutionLevel: admin
|
|
||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
extendInfo:
|
extendInfo:
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps && node ./postinstall.cjs",
|
||||||
"build:unpack": "npm run build && electron-builder --dir",
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
"build:win": "electron-vite build && electron-builder --win",
|
"build:win": "electron-vite build && electron-builder --win",
|
||||||
"build:mac": "electron-vite build && electron-builder --mac",
|
"build:mac": "electron-vite build && electron-builder --mac",
|
||||||
@ -41,6 +41,7 @@
|
|||||||
"@sentry/electron": "^5.1.0",
|
"@sentry/electron": "^5.1.0",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
"@vanilla-extract/recipes": "^0.5.2",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
|
"aria2": "^4.1.2",
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"better-sqlite3": "^9.5.0",
|
"better-sqlite3": "^9.5.0",
|
||||||
@ -65,11 +66,11 @@
|
|||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"parse-torrent": "^11.0.16",
|
"parse-torrent": "^11.0.16",
|
||||||
"piscina": "^4.5.1",
|
"piscina": "^4.5.1",
|
||||||
"ps-list": "^8.1.1",
|
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
"react-redux": "^9.1.1",
|
"react-redux": "^9.1.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
|
"sudo-prompt": "^9.2.1",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"user-agents": "^1.1.193",
|
"user-agents": "^1.1.193",
|
||||||
"yaml": "^2.4.1",
|
"yaml": "^2.4.1",
|
||||||
|
50
postinstall.cjs
Normal file
50
postinstall.cjs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
const { default: axios } = require("axios");
|
||||||
|
const util = require("node:util");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
|
||||||
|
const exec = util.promisify(require("node:child_process").exec);
|
||||||
|
|
||||||
|
const downloadAria2 = async () => {
|
||||||
|
if (fs.existsSync("aria2")) {
|
||||||
|
console.log("Aria2 already exists, skipping download...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file =
|
||||||
|
process.platform === "win32"
|
||||||
|
? "aria2-1.37.0-win-64bit-build1.zip"
|
||||||
|
: "aria2-1.37.0-1-x86_64.pkg.tar.zst";
|
||||||
|
|
||||||
|
const downloadUrl =
|
||||||
|
process.platform === "win32"
|
||||||
|
? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
|
||||||
|
: "https://archlinux.org/packages/extra/x86_64/aria2/download/";
|
||||||
|
|
||||||
|
console.log(`Downloading ${file}...`);
|
||||||
|
|
||||||
|
const response = await axios.get(downloadUrl, { responseType: "stream" });
|
||||||
|
|
||||||
|
const stream = response.data.pipe(fs.createWriteStream(file));
|
||||||
|
|
||||||
|
stream.on("finish", async () => {
|
||||||
|
console.log(`Downloaded ${file}, extracting...`);
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
await exec(`npx extract-zip ${file}`);
|
||||||
|
console.log("Extracted. Renaming folder...");
|
||||||
|
|
||||||
|
fs.renameSync(file.replace(".zip", ""), "aria2");
|
||||||
|
} else {
|
||||||
|
await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
|
||||||
|
console.log("Extracted. Copying binary file...");
|
||||||
|
fs.mkdirSync("aria2");
|
||||||
|
fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
|
||||||
|
fs.rmSync("usr", { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Extracted ${file}, removing compressed downloaded file...`);
|
||||||
|
fs.rmSync(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
downloadAria2();
|
@ -3,3 +3,4 @@ cx_Freeze
|
|||||||
cx_Logging; sys_platform == 'win32'
|
cx_Logging; sys_platform == 'win32'
|
||||||
lief; sys_platform == 'win32'
|
lief; sys_platform == 'win32'
|
||||||
pywin32; sys_platform == 'win32'
|
pywin32; sys_platform == 'win32'
|
||||||
|
psutil
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
Set WshShell = CreateObject("WScript.Shell" )
|
|
||||||
WshShell.Run """%localappdata%\Programs\Hydra\Hydra.exe""", 0 'Must quote command if it has spaces; must escape quotes
|
|
||||||
Set WshShell = Nothing
|
|
@ -14,12 +14,3 @@ export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
|||||||
export const seedsPath = app.isPackaged
|
export const seedsPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, "seeds")
|
? path.join(process.resourcesPath, "seeds")
|
||||||
: path.join(__dirname, "..", "..", "seeds");
|
: path.join(__dirname, "..", "..", "seeds");
|
||||||
|
|
||||||
export const windowsStartupPath = path.join(
|
|
||||||
app.getPath("appData"),
|
|
||||||
"Microsoft",
|
|
||||||
"Windows",
|
|
||||||
"Start Menu",
|
|
||||||
"Programs",
|
|
||||||
"Startup"
|
|
||||||
);
|
|
||||||
|
80
src/main/declaration.d.ts
vendored
Normal file
80
src/main/declaration.d.ts
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
declare module "aria2" {
|
||||||
|
export type Aria2Status =
|
||||||
|
| "active"
|
||||||
|
| "waiting"
|
||||||
|
| "paused"
|
||||||
|
| "error"
|
||||||
|
| "complete"
|
||||||
|
| "removed";
|
||||||
|
|
||||||
|
export interface StatusResponse {
|
||||||
|
gid: string;
|
||||||
|
status: Aria2Status;
|
||||||
|
totalLength: string;
|
||||||
|
completedLength: string;
|
||||||
|
uploadLength: string;
|
||||||
|
bitfield: string;
|
||||||
|
downloadSpeed: string;
|
||||||
|
uploadSpeed: string;
|
||||||
|
infoHash?: string;
|
||||||
|
numSeeders?: string;
|
||||||
|
seeder?: boolean;
|
||||||
|
pieceLength: string;
|
||||||
|
numPieces: string;
|
||||||
|
connections: string;
|
||||||
|
errorCode?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
followedBy?: string[];
|
||||||
|
following: string;
|
||||||
|
belongsTo: string;
|
||||||
|
dir: string;
|
||||||
|
files: {
|
||||||
|
path: string;
|
||||||
|
length: string;
|
||||||
|
completedLength: string;
|
||||||
|
selected: string;
|
||||||
|
}[];
|
||||||
|
bittorrent?: {
|
||||||
|
announceList: string[][];
|
||||||
|
comment: string;
|
||||||
|
creationDate: string;
|
||||||
|
mode: "single" | "multi";
|
||||||
|
info: {
|
||||||
|
name: string;
|
||||||
|
verifiedLength: string;
|
||||||
|
verifyIntegrityPending: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Aria2 {
|
||||||
|
constructor(options: any);
|
||||||
|
open: () => Promise<void>;
|
||||||
|
call(
|
||||||
|
method: "addUri",
|
||||||
|
uris: string[],
|
||||||
|
options: { dir: string }
|
||||||
|
): Promise<string>;
|
||||||
|
call(
|
||||||
|
method: "tellStatus",
|
||||||
|
gid: string,
|
||||||
|
keys?: string[]
|
||||||
|
): Promise<StatusResponse>;
|
||||||
|
call(method: "pause", gid: string): Promise<string>;
|
||||||
|
call(method: "forcePause", gid: string): Promise<string>;
|
||||||
|
call(method: "unpause", gid: string): Promise<string>;
|
||||||
|
call(method: "remove", gid: string): Promise<string>;
|
||||||
|
call(method: "forceRemove", gid: string): Promise<string>;
|
||||||
|
call(method: "pauseAll"): Promise<string>;
|
||||||
|
call(method: "forcePauseAll"): Promise<string>;
|
||||||
|
listNotifications: () => [
|
||||||
|
"onDownloadStart",
|
||||||
|
"onDownloadPause",
|
||||||
|
"onDownloadStop",
|
||||||
|
"onDownloadComplete",
|
||||||
|
"onDownloadError",
|
||||||
|
"onBtDownloadComplete",
|
||||||
|
];
|
||||||
|
on: (event: string, callback: (params: any) => void) => void;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { HydraApi, TorrentDownloader, gamesPlaytime } from "@main/services";
|
import { HydraApi, PythonInstance, gamesPlaytime } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import { DownloadQueue, Game, UserAuth } from "@main/entity";
|
import { DownloadQueue, Game, UserAuth } from "@main/entity";
|
||||||
|
|
||||||
@ -24,11 +24,11 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
|
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
TorrentDownloader.kill();
|
PythonInstance.killTorrent();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
databaseOperations,
|
databaseOperations,
|
||||||
HydraApi.post("/auth/logout").catch(),
|
HydraApi.post("/auth/logout").catch(() => {}),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import "./library/open-game-installer-path";
|
|||||||
import "./library/update-executable-path";
|
import "./library/update-executable-path";
|
||||||
import "./library/remove-game";
|
import "./library/remove-game";
|
||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
import "./misc/is-user-logged-in";
|
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
|
@ -53,18 +53,7 @@ const addGameToLibrary = async (
|
|||||||
|
|
||||||
const game = await gameRepository.findOne({ where: { objectID } });
|
const game = await gameRepository.findOne({ where: { objectID } });
|
||||||
|
|
||||||
createGame(game!).then((response) => {
|
createGame(game!);
|
||||||
const {
|
|
||||||
id: remoteId,
|
|
||||||
playTimeInMilliseconds,
|
|
||||||
lastTimePlayed,
|
|
||||||
} = response.data;
|
|
||||||
|
|
||||||
gameRepository.update(
|
|
||||||
{ objectID },
|
|
||||||
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,39 +1,45 @@
|
|||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { getProcesses } from "@main/helpers";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { PythonInstance, logger } from "@main/services";
|
||||||
|
import sudo from "sudo-prompt";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
const getKillCommand = (pid: number) => {
|
||||||
|
if (process.platform == "win32") {
|
||||||
|
return `taskkill /PID ${pid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `kill -9 ${pid}`;
|
||||||
|
};
|
||||||
|
|
||||||
const closeGame = async (
|
const closeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
gameId: number
|
||||||
) => {
|
) => {
|
||||||
const processes = await getProcesses();
|
const processes = await PythonInstance.getProcessList();
|
||||||
const game = await gameRepository.findOne({
|
const game = await gameRepository.findOne({
|
||||||
where: { id: gameId, isDeleted: false },
|
where: { id: gameId, isDeleted: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!game) return false;
|
if (!game) return;
|
||||||
|
|
||||||
const executablePath = game.executablePath!;
|
|
||||||
|
|
||||||
const basename = path.win32.basename(executablePath);
|
|
||||||
const basenameWithoutExtension = path.win32.basename(
|
|
||||||
executablePath,
|
|
||||||
path.extname(executablePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
const gameProcess = processes.find((runningProcess) => {
|
const gameProcess = processes.find((runningProcess) => {
|
||||||
if (process.platform === "win32") {
|
return runningProcess.exe === game.executablePath;
|
||||||
return runningProcess.name === basename;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [basename, basenameWithoutExtension].includes(runningProcess.name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (gameProcess) return process.kill(gameProcess.pid);
|
if (gameProcess) {
|
||||||
return false;
|
try {
|
||||||
|
process.kill(gameProcess.pid);
|
||||||
|
} catch (err) {
|
||||||
|
sudo.exec(
|
||||||
|
getKillCommand(gameProcess.pid),
|
||||||
|
{ name: app.getName() },
|
||||||
|
(error, _stdout, _stderr) => {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("closeGame", closeGame);
|
registerEvent("closeGame", closeGame);
|
||||||
|
@ -4,6 +4,7 @@ import { IsNull, Not } from "typeorm";
|
|||||||
import createDesktopShortcut from "create-desktop-shortcuts";
|
import createDesktopShortcut from "create-desktop-shortcuts";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import { removeSymbolsFromName } from "@shared";
|
||||||
|
|
||||||
const createGameShortcut = async (
|
const createGameShortcut = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -22,7 +23,7 @@ const createGameShortcut = async (
|
|||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
filePath,
|
filePath,
|
||||||
name: game.title,
|
name: removeSymbolsFromName(game.title),
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDesktopShortcut({
|
return createDesktopShortcut({
|
||||||
|
@ -20,7 +20,7 @@ const removeRemoveGameFromLibrary = async (gameId: number) => {
|
|||||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||||
|
|
||||||
if (game?.remoteId) {
|
if (game?.remoteId) {
|
||||||
HydraApi.delete(`/games/${game.remoteId}`);
|
HydraApi.delete(`/games/${game.remoteId}`).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { registerEvent } from "../register-event";
|
|
||||||
import { HydraApi } from "@main/services";
|
|
||||||
|
|
||||||
const isUserLoggedIn = async (_event: Electron.IpcMainInvokeEvent) => {
|
|
||||||
return HydraApi.isLoggedIn();
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("isUserLoggedIn", isUserLoggedIn);
|
|
@ -3,7 +3,7 @@ import * as Sentry from "@sentry/electron/main";
|
|||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { UserProfile } from "@types";
|
import { UserProfile } from "@types";
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { userAuthRepository } from "@main/repository";
|
||||||
import { logger } from "@main/services";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
|
||||||
const getMe = async (
|
const getMe = async (
|
||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
@ -27,7 +27,10 @@ const getMe = async (
|
|||||||
return me;
|
return me;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error("getMe", err.message);
|
if (err instanceof UserNotLoggedInError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return userAuthRepository.findOne({ where: { id: 1 } });
|
return userAuthRepository.findOne({ where: { id: 1 } });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -26,9 +26,11 @@ const updateProfile = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
newProfileImagePath: string | null
|
newProfileImagePath: string | null
|
||||||
): Promise<UserProfile> => {
|
) => {
|
||||||
if (!newProfileImagePath) {
|
if (!newProfileImagePath) {
|
||||||
return (await patchUserProfile(displayName)).data;
|
return patchUserProfile(displayName).then(
|
||||||
|
(response) => response.data as UserProfile
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = fs.statSync(newProfileImagePath);
|
const stats = fs.statSync(newProfileImagePath);
|
||||||
@ -51,11 +53,11 @@ const updateProfile = async (
|
|||||||
});
|
});
|
||||||
return profileImageUrl;
|
return profileImageUrl;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => undefined);
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (await patchUserProfile(displayName, profileImageUrl)).data;
|
return patchUserProfile(displayName, profileImageUrl).then(
|
||||||
|
(response) => response.data as UserProfile
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateProfile", updateProfile);
|
registerEvent("updateProfile", updateProfile);
|
||||||
|
@ -95,18 +95,7 @@ const startGameDownload = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
createGame(updatedGame!).then((response) => {
|
createGame(updatedGame!);
|
||||||
const {
|
|
||||||
id: remoteId,
|
|
||||||
playTimeInMilliseconds,
|
|
||||||
lastTimePlayed,
|
|
||||||
} = response.data;
|
|
||||||
|
|
||||||
gameRepository.update(
|
|
||||||
{ objectID },
|
|
||||||
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { windowsStartupPath } from "@main/constants";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import AutoLaunch from "auto-launch";
|
import AutoLaunch from "auto-launch";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const autoLaunch = async (
|
const autoLaunch = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -15,23 +12,10 @@ const autoLaunch = async (
|
|||||||
name: app.getName(),
|
name: app.getName(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.platform == "win32") {
|
if (enabled) {
|
||||||
const destination = path.join(windowsStartupPath, "Hydra.vbs");
|
appLauncher.enable().catch(() => {});
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
const scriptPath = path.join(process.resourcesPath, "hydralauncher.vbs");
|
|
||||||
|
|
||||||
fs.copyFileSync(scriptPath, destination);
|
|
||||||
} else {
|
|
||||||
appLauncher.disable().catch();
|
|
||||||
fs.rmSync(destination);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (enabled) {
|
appLauncher.disable().catch(() => {});
|
||||||
appLauncher.enable().catch();
|
|
||||||
} else {
|
|
||||||
appLauncher.disable().catch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,17 +2,23 @@ import { userPreferencesRepository } from "@main/repository";
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) =>
|
) => {
|
||||||
userPreferencesRepository.upsert(
|
if (preferences.language) {
|
||||||
|
i18next.changeLanguage(preferences.language);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userPreferencesRepository.upsert(
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
...preferences,
|
...preferences,
|
||||||
},
|
},
|
||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("updateUserPreferences", updateUserPreferences);
|
registerEvent("updateUserPreferences", updateUserPreferences);
|
||||||
|
@ -57,5 +57,4 @@ export const requestWebPage = async (url: string) => {
|
|||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from "./ps";
|
|
||||||
export * from "./download-source";
|
export * from "./download-source";
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import psList from "ps-list";
|
|
||||||
import path from "node:path";
|
|
||||||
import childProcess from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { app } from "electron";
|
|
||||||
|
|
||||||
const TEN_MEGABYTES = 1000 * 1000 * 10;
|
|
||||||
const execFile = promisify(childProcess.execFile);
|
|
||||||
|
|
||||||
export const getProcesses = async () => {
|
|
||||||
if (process.platform == "win32") {
|
|
||||||
const binaryPath = app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, "fastlist.exe")
|
|
||||||
: path.join(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"..",
|
|
||||||
"node_modules",
|
|
||||||
"ps-list",
|
|
||||||
"vendor",
|
|
||||||
"fastlist-0.3.0-x64.exe"
|
|
||||||
);
|
|
||||||
|
|
||||||
const { stdout } = await execFile(binaryPath, {
|
|
||||||
maxBuffer: TEN_MEGABYTES,
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return stdout
|
|
||||||
.trim()
|
|
||||||
.split("\r\n")
|
|
||||||
.map((line) => line.split("\t"))
|
|
||||||
.map(([pid, ppid, name]) => ({
|
|
||||||
pid: Number.parseInt(pid, 10),
|
|
||||||
ppid: Number.parseInt(ppid, 10),
|
|
||||||
name,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
return psList();
|
|
||||||
}
|
|
||||||
};
|
|
@ -5,7 +5,7 @@ import i18n from "i18next";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import url from "node:url";
|
import url from "node:url";
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import { logger, TorrentDownloader, WindowManager } from "@main/services";
|
import { logger, PythonInstance, WindowManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import * as resources from "@locales";
|
import * as resources from "@locales";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
@ -72,6 +72,10 @@ app.whenReady().then(async () => {
|
|||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userPreferences?.language) {
|
||||||
|
i18n.changeLanguage(userPreferences.language);
|
||||||
|
}
|
||||||
|
|
||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||||
});
|
});
|
||||||
@ -116,7 +120,7 @@ app.on("window-all-closed", () => {
|
|||||||
|
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
TorrentDownloader.kill();
|
PythonInstance.kill();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { DownloadManager, RepacksManager, startMainLoop } from "./services";
|
import {
|
||||||
|
DownloadManager,
|
||||||
|
RepacksManager,
|
||||||
|
PythonInstance,
|
||||||
|
startMainLoop,
|
||||||
|
} from "./services";
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
downloadQueueRepository,
|
||||||
repackRepository,
|
repackRepository,
|
||||||
@ -12,8 +17,6 @@ import { MoreThan } from "typeorm";
|
|||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
|
|
||||||
startMainLoop();
|
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
RepacksManager.updateRepacks();
|
RepacksManager.updateRepacks();
|
||||||
|
|
||||||
@ -22,8 +25,8 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
if (userPreferences?.realDebridApiToken)
|
if (userPreferences?.realDebridApiToken)
|
||||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||||
|
|
||||||
HydraApi.setupApi().then(async () => {
|
HydraApi.setupApi().then(() => {
|
||||||
if (HydraApi.isLoggedIn()) uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
});
|
});
|
||||||
|
|
||||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||||
@ -35,8 +38,13 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextQueueItem?.game.status === "active")
|
if (nextQueueItem?.game.status === "active") {
|
||||||
DownloadManager.startDownload(nextQueueItem.game);
|
DownloadManager.startDownload(nextQueueItem.game);
|
||||||
|
} else {
|
||||||
|
PythonInstance.spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
startMainLoop();
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
20
src/main/services/aria2c.ts
Normal file
20
src/main/services/aria2c.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { app } from "electron";
|
||||||
|
|
||||||
|
export const startAria2 = () => {
|
||||||
|
const binaryPath = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, "aria2", "aria2c")
|
||||||
|
: path.join(__dirname, "..", "..", "aria2", "aria2c");
|
||||||
|
|
||||||
|
return spawn(
|
||||||
|
binaryPath,
|
||||||
|
[
|
||||||
|
"--enable-rpc",
|
||||||
|
"--rpc-listen-all",
|
||||||
|
"--file-allocation=none",
|
||||||
|
"--allow-overwrite=true",
|
||||||
|
],
|
||||||
|
{ stdio: "inherit", windowsHide: true }
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { TorrentDownloader } from "./torrent-downloader";
|
import { PythonInstance } from "./python-instance";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { downloadQueueRepository, gameRepository } from "@main/repository";
|
import { downloadQueueRepository, gameRepository } from "@main/repository";
|
||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
@ -16,7 +16,7 @@ export class DownloadManager {
|
|||||||
if (this.currentDownloader === Downloader.RealDebrid) {
|
if (this.currentDownloader === Downloader.RealDebrid) {
|
||||||
status = await RealDebridDownloader.getStatus();
|
status = await RealDebridDownloader.getStatus();
|
||||||
} else {
|
} else {
|
||||||
status = await TorrentDownloader.getStatus();
|
status = await PythonInstance.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
@ -63,9 +63,9 @@ export class DownloadManager {
|
|||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload() {
|
||||||
if (this.currentDownloader === Downloader.RealDebrid) {
|
if (this.currentDownloader === Downloader.RealDebrid) {
|
||||||
RealDebridDownloader.pauseDownload();
|
await RealDebridDownloader.pauseDownload();
|
||||||
} else {
|
} else {
|
||||||
await TorrentDownloader.pauseDownload();
|
await PythonInstance.pauseDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
@ -77,16 +77,16 @@ export class DownloadManager {
|
|||||||
RealDebridDownloader.startDownload(game);
|
RealDebridDownloader.startDownload(game);
|
||||||
this.currentDownloader = Downloader.RealDebrid;
|
this.currentDownloader = Downloader.RealDebrid;
|
||||||
} else {
|
} else {
|
||||||
TorrentDownloader.startDownload(game);
|
PythonInstance.startDownload(game);
|
||||||
this.currentDownloader = Downloader.Torrent;
|
this.currentDownloader = Downloader.Torrent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(gameId: number) {
|
static async cancelDownload(gameId: number) {
|
||||||
if (this.currentDownloader === Downloader.RealDebrid) {
|
if (this.currentDownloader === Downloader.RealDebrid) {
|
||||||
RealDebridDownloader.cancelDownload();
|
RealDebridDownloader.cancelDownload(gameId);
|
||||||
} else {
|
} else {
|
||||||
TorrentDownloader.cancelDownload(gameId);
|
PythonInstance.cancelDownload(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
@ -98,7 +98,7 @@ export class DownloadManager {
|
|||||||
RealDebridDownloader.startDownload(game);
|
RealDebridDownloader.startDownload(game);
|
||||||
this.currentDownloader = Downloader.RealDebrid;
|
this.currentDownloader = Downloader.RealDebrid;
|
||||||
} else {
|
} else {
|
||||||
TorrentDownloader.startDownload(game);
|
PythonInstance.startDownload(game);
|
||||||
this.currentDownloader = Downloader.Torrent;
|
this.currentDownloader = Downloader.Torrent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,123 +1,68 @@
|
|||||||
import path from "node:path";
|
import type { ChildProcess } from "node:child_process";
|
||||||
import fs from "node:fs";
|
|
||||||
import crypto from "node:crypto";
|
|
||||||
|
|
||||||
import axios, { type AxiosProgressEvent } from "axios";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
import { sleep } from "@main/helpers";
|
||||||
|
import { startAria2 } from "../aria2c";
|
||||||
|
import Aria2 from "aria2";
|
||||||
|
|
||||||
export class HttpDownload {
|
export class HttpDownload {
|
||||||
private abortController: AbortController;
|
private static connected = false;
|
||||||
public lastProgressEvent: AxiosProgressEvent;
|
private static aria2c: ChildProcess | null = null;
|
||||||
private trackerFilePath: string;
|
|
||||||
|
|
||||||
private trackerProgressEvent: AxiosProgressEvent | null = null;
|
private static aria2 = new Aria2({});
|
||||||
private downloadPath: string;
|
|
||||||
|
|
||||||
private downloadTrackersPath = path.join(
|
private static async connect() {
|
||||||
app.getPath("documents"),
|
this.aria2c = startAria2();
|
||||||
"Hydra",
|
|
||||||
"Downloads"
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
let retries = 0;
|
||||||
private url: string,
|
|
||||||
private savePath: string
|
|
||||||
) {
|
|
||||||
this.abortController = new AbortController();
|
|
||||||
|
|
||||||
const sha256Hasher = crypto.createHash("sha256");
|
while (retries < 4 && !this.connected) {
|
||||||
const hash = sha256Hasher.update(url).digest("hex");
|
try {
|
||||||
|
await this.aria2.open();
|
||||||
|
logger.log("Connected to aria2");
|
||||||
|
|
||||||
this.trackerFilePath = path.join(
|
this.connected = true;
|
||||||
this.downloadTrackersPath,
|
} catch (err) {
|
||||||
`${hash}.hydradownload`
|
await sleep(100);
|
||||||
);
|
logger.log("Failed to connect to aria2, retrying...");
|
||||||
|
retries++;
|
||||||
const filename = path.win32.basename(this.url);
|
}
|
||||||
this.downloadPath = path.join(this.savePath, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateTrackerFile() {
|
|
||||||
if (!fs.existsSync(this.downloadTrackersPath)) {
|
|
||||||
fs.mkdirSync(this.downloadTrackersPath, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
this.trackerFilePath,
|
|
||||||
JSON.stringify(this.lastProgressEvent),
|
|
||||||
{ encoding: "utf-8" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeTrackerFile() {
|
|
||||||
if (fs.existsSync(this.trackerFilePath)) {
|
|
||||||
fs.rm(this.trackerFilePath, (err) => {
|
|
||||||
logger.error(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async startDownload() {
|
public static getStatus(gid: string) {
|
||||||
// Check if there's already a tracker file and download file
|
if (this.connected) {
|
||||||
if (
|
return this.aria2.call("tellStatus", gid);
|
||||||
fs.existsSync(this.trackerFilePath) &&
|
|
||||||
fs.existsSync(this.downloadPath)
|
|
||||||
) {
|
|
||||||
this.trackerProgressEvent = JSON.parse(
|
|
||||||
fs.readFileSync(this.trackerFilePath, { encoding: "utf-8" })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.get(this.url, {
|
return null;
|
||||||
responseType: "stream",
|
|
||||||
signal: this.abortController.signal,
|
|
||||||
headers: {
|
|
||||||
Range: `bytes=${this.trackerProgressEvent?.loaded ?? 0}-`,
|
|
||||||
},
|
|
||||||
onDownloadProgress: (progressEvent) => {
|
|
||||||
const total =
|
|
||||||
this.trackerProgressEvent?.total ?? progressEvent.total ?? 0;
|
|
||||||
const loaded =
|
|
||||||
(this.trackerProgressEvent?.loaded ?? 0) + progressEvent.loaded;
|
|
||||||
|
|
||||||
const progress = loaded / total;
|
|
||||||
|
|
||||||
this.lastProgressEvent = {
|
|
||||||
...progressEvent,
|
|
||||||
total,
|
|
||||||
progress,
|
|
||||||
loaded,
|
|
||||||
};
|
|
||||||
this.updateTrackerFile();
|
|
||||||
|
|
||||||
if (progressEvent.progress === 1) {
|
|
||||||
this.removeTrackerFile();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
response.data.pipe(
|
|
||||||
fs.createWriteStream(this.downloadPath, {
|
|
||||||
flags: "a",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pauseDownload() {
|
public static disconnect() {
|
||||||
this.abortController.abort();
|
if (this.aria2c) {
|
||||||
|
this.aria2c.kill();
|
||||||
|
this.connected = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public cancelDownload() {
|
static async cancelDownload(gid: string) {
|
||||||
this.pauseDownload();
|
await this.aria2.call("forceRemove", gid);
|
||||||
|
}
|
||||||
|
|
||||||
fs.rm(this.downloadPath, (err) => {
|
static async pauseDownload(gid: string) {
|
||||||
if (err) logger.error(err);
|
await this.aria2.call("forcePause", gid);
|
||||||
});
|
}
|
||||||
fs.rm(this.trackerFilePath, (err) => {
|
|
||||||
if (err) logger.error(err);
|
static async resumeDownload(gid: string) {
|
||||||
});
|
await this.aria2.call("unpause", gid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async startDownload(downloadPath: string, downloadUrl: string) {
|
||||||
|
if (!this.connected) await this.connect();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
dir: downloadPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.aria2.call("addUri", [downloadUrl], options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export * from "./download-manager";
|
export * from "./download-manager";
|
||||||
export * from "./torrent-downloader";
|
export * from "./python-instance";
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import cp from "node:child_process";
|
import cp from "node:child_process";
|
||||||
|
|
||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { RPC_PASSWORD, RPC_PORT, startTorrentClient } from "./torrent-client";
|
import {
|
||||||
|
RPC_PASSWORD,
|
||||||
|
RPC_PORT,
|
||||||
|
startTorrentClient as startRPCClient,
|
||||||
|
} from "./torrent-client";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { DownloadProgress } from "@types";
|
import { DownloadProgress } from "@types";
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||||
@ -13,10 +17,11 @@ import {
|
|||||||
PauseDownloadPayload,
|
PauseDownloadPayload,
|
||||||
LibtorrentStatus,
|
LibtorrentStatus,
|
||||||
LibtorrentPayload,
|
LibtorrentPayload,
|
||||||
|
ProcessPayload,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export class TorrentDownloader {
|
export class PythonInstance {
|
||||||
private static torrentClient: cp.ChildProcess | null = null;
|
private static pythonProcess: cp.ChildProcess | null = null;
|
||||||
private static downloadingGameId = -1;
|
private static downloadingGameId = -1;
|
||||||
|
|
||||||
private static rpc = axios.create({
|
private static rpc = axios.create({
|
||||||
@ -26,18 +31,31 @@ export class TorrentDownloader {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
private static spawn(args: StartDownloadPayload) {
|
public static spawn(args?: StartDownloadPayload) {
|
||||||
this.torrentClient = startTorrentClient(args);
|
this.pythonProcess = startRPCClient(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static kill() {
|
public static kill() {
|
||||||
if (this.torrentClient) {
|
if (this.pythonProcess) {
|
||||||
this.torrentClient.kill();
|
this.pythonProcess.kill();
|
||||||
this.torrentClient = null;
|
this.pythonProcess = null;
|
||||||
this.downloadingGameId = -1;
|
this.downloadingGameId = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static killTorrent() {
|
||||||
|
if (this.pythonProcess) {
|
||||||
|
this.rpc.post("/action", { action: "kill-torrent" });
|
||||||
|
this.downloadingGameId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getProcessList() {
|
||||||
|
return (
|
||||||
|
(await this.rpc.get<ProcessPayload[] | null>("/process-list")).data || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static async getStatus() {
|
public static async getStatus() {
|
||||||
if (this.downloadingGameId === -1) return null;
|
if (this.downloadingGameId === -1) return null;
|
||||||
|
|
||||||
@ -113,7 +131,7 @@ export class TorrentDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
static async startDownload(game: Game) {
|
||||||
if (!this.torrentClient) {
|
if (!this.pythonProcess) {
|
||||||
this.spawn({
|
this.spawn({
|
||||||
game_id: game.id,
|
game_id: game.id,
|
||||||
magnet: game.uri!,
|
magnet: game.uri!,
|
@ -6,10 +6,10 @@ import { DownloadProgress } from "@types";
|
|||||||
import { HttpDownload } from "./http-download";
|
import { HttpDownload } from "./http-download";
|
||||||
|
|
||||||
export class RealDebridDownloader {
|
export class RealDebridDownloader {
|
||||||
|
private static downloads = new Map<number, string>();
|
||||||
private static downloadingGame: Game | null = null;
|
private static downloadingGame: Game | null = null;
|
||||||
|
|
||||||
private static realDebridTorrentId: string | null = null;
|
private static realDebridTorrentId: string | null = null;
|
||||||
private static httpDownload: HttpDownload | null = null;
|
|
||||||
|
|
||||||
private static async getRealDebridDownloadUrl() {
|
private static async getRealDebridDownloadUrl() {
|
||||||
if (this.realDebridTorrentId) {
|
if (this.realDebridTorrentId) {
|
||||||
@ -35,39 +35,47 @@ export class RealDebridDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async getStatus() {
|
public static async getStatus() {
|
||||||
const lastProgressEvent = this.httpDownload?.lastProgressEvent;
|
if (this.downloadingGame) {
|
||||||
|
const gid = this.downloads.get(this.downloadingGame.id)!;
|
||||||
|
const status = await HttpDownload.getStatus(gid);
|
||||||
|
|
||||||
if (lastProgressEvent) {
|
if (status) {
|
||||||
await gameRepository.update(
|
const progress =
|
||||||
{ id: this.downloadingGame!.id },
|
Number(status.completedLength) / Number(status.totalLength);
|
||||||
{
|
|
||||||
bytesDownloaded: lastProgressEvent.loaded,
|
await gameRepository.update(
|
||||||
fileSize: lastProgressEvent.total,
|
{ id: this.downloadingGame!.id },
|
||||||
progress: lastProgressEvent.progress,
|
{
|
||||||
status: "active",
|
bytesDownloaded: Number(status.completedLength),
|
||||||
|
fileSize: Number(status.totalLength),
|
||||||
|
progress,
|
||||||
|
status: "active",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
numPeers: 0,
|
||||||
|
numSeeds: 0,
|
||||||
|
downloadSpeed: Number(status.downloadSpeed),
|
||||||
|
timeRemaining: calculateETA(
|
||||||
|
Number(status.totalLength),
|
||||||
|
Number(status.completedLength),
|
||||||
|
Number(status.downloadSpeed)
|
||||||
|
),
|
||||||
|
isDownloadingMetadata: false,
|
||||||
|
isCheckingFiles: false,
|
||||||
|
progress,
|
||||||
|
gameId: this.downloadingGame!.id,
|
||||||
|
} as DownloadProgress;
|
||||||
|
|
||||||
|
if (progress === 1) {
|
||||||
|
this.downloads.delete(this.downloadingGame.id);
|
||||||
|
this.realDebridTorrentId = null;
|
||||||
|
this.downloadingGame = null;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const progress = {
|
return result;
|
||||||
numPeers: 0,
|
|
||||||
numSeeds: 0,
|
|
||||||
downloadSpeed: lastProgressEvent.rate,
|
|
||||||
timeRemaining: calculateETA(
|
|
||||||
lastProgressEvent.total ?? 0,
|
|
||||||
lastProgressEvent.loaded,
|
|
||||||
lastProgressEvent.rate ?? 0
|
|
||||||
),
|
|
||||||
isDownloadingMetadata: false,
|
|
||||||
isCheckingFiles: false,
|
|
||||||
progress: lastProgressEvent.progress,
|
|
||||||
gameId: this.downloadingGame!.id,
|
|
||||||
} as DownloadProgress;
|
|
||||||
|
|
||||||
if (lastProgressEvent.progress === 1) {
|
|
||||||
this.pauseDownload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return progress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.realDebridTorrentId && this.downloadingGame) {
|
if (this.realDebridTorrentId && this.downloadingGame) {
|
||||||
@ -101,25 +109,54 @@ export class RealDebridDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload() {
|
||||||
this.httpDownload?.pauseDownload();
|
const gid = this.downloads.get(this.downloadingGame!.id!);
|
||||||
|
if (gid) {
|
||||||
|
await HttpDownload.pauseDownload(gid);
|
||||||
|
}
|
||||||
|
|
||||||
this.realDebridTorrentId = null;
|
this.realDebridTorrentId = null;
|
||||||
this.downloadingGame = null;
|
this.downloadingGame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
static async startDownload(game: Game) {
|
||||||
this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!);
|
|
||||||
this.downloadingGame = game;
|
this.downloadingGame = game;
|
||||||
|
|
||||||
|
if (this.downloads.has(game.id)) {
|
||||||
|
await this.resumeDownload(game.id!);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!);
|
||||||
|
|
||||||
const downloadUrl = await this.getRealDebridDownloadUrl();
|
const downloadUrl = await this.getRealDebridDownloadUrl();
|
||||||
|
|
||||||
if (downloadUrl) {
|
if (downloadUrl) {
|
||||||
this.realDebridTorrentId = null;
|
this.realDebridTorrentId = null;
|
||||||
this.httpDownload = new HttpDownload(downloadUrl, game!.downloadPath!);
|
|
||||||
this.httpDownload.startDownload();
|
const gid = await HttpDownload.startDownload(
|
||||||
|
game.downloadPath!,
|
||||||
|
downloadUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
this.downloads.set(game.id!, gid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static cancelDownload() {
|
static async cancelDownload(gameId: number) {
|
||||||
return this.httpDownload?.cancelDownload();
|
const gid = this.downloads.get(gameId);
|
||||||
|
|
||||||
|
if (gid) {
|
||||||
|
await HttpDownload.cancelDownload(gid);
|
||||||
|
this.downloads.delete(gameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async resumeDownload(gameId: number) {
|
||||||
|
const gid = this.downloads.get(gameId);
|
||||||
|
|
||||||
|
if (gid) {
|
||||||
|
await HttpDownload.resumeDownload(gid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,12 +15,12 @@ export const BITTORRENT_PORT = "5881";
|
|||||||
export const RPC_PORT = "8084";
|
export const RPC_PORT = "8084";
|
||||||
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
|
||||||
|
|
||||||
export const startTorrentClient = (args: StartDownloadPayload) => {
|
export const startTorrentClient = (args?: StartDownloadPayload) => {
|
||||||
const commonArgs = [
|
const commonArgs = [
|
||||||
BITTORRENT_PORT,
|
BITTORRENT_PORT,
|
||||||
RPC_PORT,
|
RPC_PORT,
|
||||||
RPC_PASSWORD,
|
RPC_PASSWORD,
|
||||||
encodeURIComponent(JSON.stringify(args)),
|
args ? encodeURIComponent(JSON.stringify(args)) : "",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
|
@ -31,3 +31,8 @@ export interface LibtorrentPayload {
|
|||||||
status: LibtorrentStatus;
|
status: LibtorrentStatus;
|
||||||
gameId: number;
|
gameId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProcessPayload {
|
||||||
|
exe: string;
|
||||||
|
pid: number;
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ import url from "url";
|
|||||||
import { uploadGamesBatch } from "./library-sync";
|
import { uploadGamesBatch } from "./library-sync";
|
||||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
|
import { UserNotLoggedInError } from "@shared";
|
||||||
|
|
||||||
export class HydraApi {
|
export class HydraApi {
|
||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
@ -19,7 +20,7 @@ export class HydraApi {
|
|||||||
expirationTimestamp: 0,
|
expirationTimestamp: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
static isLoggedIn() {
|
private static isLoggedIn() {
|
||||||
return this.userAuth.authToken !== "";
|
return this.userAuth.authToken !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,14 +128,8 @@ export class HydraApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async revalidateAccessTokenIfExpired() {
|
private static async revalidateAccessTokenIfExpired() {
|
||||||
if (!this.userAuth.authToken) {
|
|
||||||
userAuthRepository.delete({ id: 1 });
|
|
||||||
logger.error("user is not logged in");
|
|
||||||
this.sendSignOutEvent();
|
|
||||||
throw new Error("user is not logged in");
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
if (this.userAuth.expirationTimestamp < now.getTime()) {
|
if (this.userAuth.expirationTimestamp < now.getTime()) {
|
||||||
try {
|
try {
|
||||||
const response = await this.instance.post(`/auth/refresh`, {
|
const response = await this.instance.post(`/auth/refresh`, {
|
||||||
@ -190,6 +185,8 @@ export class HydraApi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static async get(url: string) {
|
static async get(url: string) {
|
||||||
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.get(url, this.getAxiosConfig())
|
.get(url, this.getAxiosConfig())
|
||||||
@ -197,6 +194,8 @@ export class HydraApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async post(url: string, data?: any) {
|
static async post(url: string, data?: any) {
|
||||||
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.post(url, data, this.getAxiosConfig())
|
.post(url, data, this.getAxiosConfig())
|
||||||
@ -204,6 +203,8 @@ export class HydraApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async put(url: string, data?: any) {
|
static async put(url: string, data?: any) {
|
||||||
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.put(url, data, this.getAxiosConfig())
|
.put(url, data, this.getAxiosConfig())
|
||||||
@ -211,6 +212,8 @@ export class HydraApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async patch(url: string, data?: any) {
|
static async patch(url: string, data?: any) {
|
||||||
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.patch(url, data, this.getAxiosConfig())
|
.patch(url, data, this.getAxiosConfig())
|
||||||
@ -218,6 +221,8 @@ export class HydraApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async delete(url: string) {
|
static async delete(url: string) {
|
||||||
|
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||||
|
|
||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.delete(url, this.getAxiosConfig())
|
.delete(url, this.getAxiosConfig())
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
export const createGame = async (game: Game) => {
|
export const createGame = async (game: Game) => {
|
||||||
return HydraApi.post(`/games`, {
|
HydraApi.post(`/games`, {
|
||||||
objectId: game.objectID,
|
objectId: game.objectID,
|
||||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
});
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const {
|
||||||
|
id: remoteId,
|
||||||
|
playTimeInMilliseconds,
|
||||||
|
lastTimePlayed,
|
||||||
|
} = response.data;
|
||||||
|
|
||||||
|
gameRepository.update(
|
||||||
|
{ objectID: game.objectID },
|
||||||
|
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
@ -2,71 +2,63 @@ import { gameRepository } from "@main/repository";
|
|||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { getSteamAppAsset } from "@main/helpers";
|
import { getSteamAppAsset } from "@main/helpers";
|
||||||
import { logger } from "../logger";
|
|
||||||
import { AxiosError } from "axios";
|
|
||||||
|
|
||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
try {
|
return HydraApi.get("/games")
|
||||||
const games = await HydraApi.get("/games");
|
.then(async (response) => {
|
||||||
|
for (const game of response.data) {
|
||||||
for (const game of games.data) {
|
const localGame = await gameRepository.findOne({
|
||||||
const localGame = await gameRepository.findOne({
|
where: {
|
||||||
where: {
|
|
||||||
objectID: game.objectId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (localGame) {
|
|
||||||
const updatedLastTimePlayed =
|
|
||||||
localGame.lastTimePlayed == null ||
|
|
||||||
(game.lastTimePlayed &&
|
|
||||||
new Date(game.lastTimePlayed) > localGame.lastTimePlayed)
|
|
||||||
? game.lastTimePlayed
|
|
||||||
: localGame.lastTimePlayed;
|
|
||||||
|
|
||||||
const updatedPlayTime =
|
|
||||||
localGame.playTimeInMilliseconds < game.playTimeInMilliseconds
|
|
||||||
? game.playTimeInMilliseconds
|
|
||||||
: localGame.playTimeInMilliseconds;
|
|
||||||
|
|
||||||
gameRepository.update(
|
|
||||||
{
|
|
||||||
objectID: game.objectId,
|
objectID: game.objectId,
|
||||||
shop: "steam",
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
remoteId: game.id,
|
|
||||||
lastTimePlayed: updatedLastTimePlayed,
|
|
||||||
playTimeInMilliseconds: updatedPlayTime,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (steamGame) {
|
if (localGame) {
|
||||||
const iconUrl = steamGame?.clientIcon
|
const updatedLastTimePlayed =
|
||||||
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
localGame.lastTimePlayed == null ||
|
||||||
: null;
|
(game.lastTimePlayed &&
|
||||||
|
new Date(game.lastTimePlayed) > localGame.lastTimePlayed)
|
||||||
|
? game.lastTimePlayed
|
||||||
|
: localGame.lastTimePlayed;
|
||||||
|
|
||||||
gameRepository.insert({
|
const updatedPlayTime =
|
||||||
objectID: game.objectId,
|
localGame.playTimeInMilliseconds < game.playTimeInMilliseconds
|
||||||
title: steamGame?.name,
|
? game.playTimeInMilliseconds
|
||||||
remoteId: game.id,
|
: localGame.playTimeInMilliseconds;
|
||||||
shop: game.shop,
|
|
||||||
iconUrl,
|
gameRepository.update(
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
{
|
||||||
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
objectID: game.objectId,
|
||||||
|
shop: "steam",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
remoteId: game.id,
|
||||||
|
lastTimePlayed: updatedLastTimePlayed,
|
||||||
|
playTimeInMilliseconds: updatedPlayTime,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
|
||||||
|
name: "getById",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (steamGame) {
|
||||||
|
const iconUrl = steamGame?.clientIcon
|
||||||
|
? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
gameRepository.insert({
|
||||||
|
objectID: game.objectId,
|
||||||
|
title: steamGame?.name,
|
||||||
|
remoteId: game.id,
|
||||||
|
shop: game.shop,
|
||||||
|
iconUrl,
|
||||||
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
|
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
} catch (err) {
|
.catch(() => {});
|
||||||
if (err instanceof AxiosError) {
|
|
||||||
logger.error("getRemoteGames", err.message);
|
|
||||||
} else {
|
|
||||||
logger.error("getRemoteGames", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -6,8 +6,8 @@ export const updateGamePlaytime = async (
|
|||||||
deltaInMillis: number,
|
deltaInMillis: number,
|
||||||
lastTimePlayed: Date
|
lastTimePlayed: Date
|
||||||
) => {
|
) => {
|
||||||
return HydraApi.put(`/games/${game.remoteId}`, {
|
HydraApi.put(`/games/${game.remoteId}`, {
|
||||||
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
|
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
|
||||||
lastTimePlayed,
|
lastTimePlayed,
|
||||||
});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
@ -2,43 +2,32 @@ import { gameRepository } from "@main/repository";
|
|||||||
import { chunk } from "lodash-es";
|
import { chunk } from "lodash-es";
|
||||||
import { IsNull } from "typeorm";
|
import { IsNull } from "typeorm";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { logger } from "../logger";
|
|
||||||
import { AxiosError } from "axios";
|
|
||||||
|
|
||||||
import { mergeWithRemoteGames } from "./merge-with-remote-games";
|
import { mergeWithRemoteGames } from "./merge-with-remote-games";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
|
|
||||||
export const uploadGamesBatch = async () => {
|
export const uploadGamesBatch = async () => {
|
||||||
try {
|
const games = await gameRepository.find({
|
||||||
const games = await gameRepository.find({
|
where: { remoteId: IsNull(), isDeleted: false },
|
||||||
where: { remoteId: IsNull(), isDeleted: false },
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const gamesChunks = chunk(games, 200);
|
const gamesChunks = chunk(games, 200);
|
||||||
|
|
||||||
for (const chunk of gamesChunks) {
|
for (const chunk of gamesChunks) {
|
||||||
await HydraApi.post(
|
await HydraApi.post(
|
||||||
"/games/batch",
|
"/games/batch",
|
||||||
chunk.map((game) => {
|
chunk.map((game) => {
|
||||||
return {
|
return {
|
||||||
objectId: game.objectID,
|
objectId: game.objectID,
|
||||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
).catch(() => {});
|
||||||
}
|
|
||||||
|
|
||||||
await mergeWithRemoteGames();
|
|
||||||
|
|
||||||
if (WindowManager.mainWindow)
|
|
||||||
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof AxiosError) {
|
|
||||||
logger.error("uploadGamesBatch", err.response, err.message);
|
|
||||||
} else {
|
|
||||||
logger.error("uploadGamesBatch", err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await mergeWithRemoteGames();
|
||||||
|
|
||||||
|
if (WindowManager.mainWindow)
|
||||||
|
WindowManager.mainWindow.webContents.send("on-library-batch-complete");
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import { IsNull, Not } from "typeorm";
|
import { IsNull, Not } from "typeorm";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
import { getProcesses } from "@main/helpers";
|
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { createGame, updateGamePlaytime } from "./library-sync";
|
import { createGame, updateGamePlaytime } from "./library-sync";
|
||||||
import { GameRunning } from "@types";
|
import { GameRunning } from "@types";
|
||||||
|
import { PythonInstance } from "./download";
|
||||||
|
|
||||||
export const gamesPlaytime = new Map<
|
export const gamesPlaytime = new Map<
|
||||||
number,
|
number,
|
||||||
@ -21,23 +19,13 @@ export const watchProcesses = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (games.length === 0) return;
|
if (games.length === 0) return;
|
||||||
|
const processes = await PythonInstance.getProcessList();
|
||||||
const processes = await getProcesses();
|
|
||||||
|
|
||||||
for (const game of games) {
|
for (const game of games) {
|
||||||
const executablePath = game.executablePath!;
|
const executablePath = game.executablePath!;
|
||||||
const basename = path.win32.basename(executablePath);
|
|
||||||
const basenameWithoutExtension = path.win32.basename(
|
|
||||||
executablePath,
|
|
||||||
path.extname(executablePath)
|
|
||||||
);
|
|
||||||
|
|
||||||
const gameProcess = processes.find((runningProcess) => {
|
const gameProcess = processes.find((runningProcess) => {
|
||||||
if (process.platform === "win32") {
|
return executablePath == runningProcess.exe;
|
||||||
return runningProcess.name === basename;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [basename, basenameWithoutExtension].includes(runningProcess.name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (gameProcess) {
|
if (gameProcess) {
|
||||||
@ -60,12 +48,7 @@ export const watchProcesses = async () => {
|
|||||||
if (game.remoteId) {
|
if (game.remoteId) {
|
||||||
updateGamePlaytime(game, 0, new Date());
|
updateGamePlaytime(game, 0, new Date());
|
||||||
} else {
|
} else {
|
||||||
createGame({ ...game, lastTimePlayed: new Date() }).then(
|
createGame({ ...game, lastTimePlayed: new Date() });
|
||||||
(response) => {
|
|
||||||
const { id: remoteId } = response.data;
|
|
||||||
gameRepository.update({ objectID: game.objectID }, { remoteId });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gamesPlaytime.set(game.id, {
|
gamesPlaytime.set(game.id, {
|
||||||
@ -84,10 +67,7 @@ export const watchProcesses = async () => {
|
|||||||
game.lastTimePlayed!
|
game.lastTimePlayed!
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
createGame(game).then((response) => {
|
createGame(game);
|
||||||
const { id: remoteId } = response.data;
|
|
||||||
gameRepository.update({ objectID: game.objectID }, { remoteId });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
||||||
isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"),
|
isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"),
|
||||||
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
|
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
|
||||||
isUserLoggedIn: () => ipcRenderer.invoke("isUserLoggedIn"),
|
|
||||||
showOpenDialog: (options: Electron.OpenDialogOptions) =>
|
showOpenDialog: (options: Electron.OpenDialogOptions) =>
|
||||||
ipcRenderer.invoke("showOpenDialog", options),
|
ipcRenderer.invoke("showOpenDialog", options),
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
|
@ -93,12 +93,8 @@ export function App() {
|
|||||||
dispatch(setProfileBackground(profileBackground));
|
dispatch(setProfileBackground(profileBackground));
|
||||||
}
|
}
|
||||||
|
|
||||||
window.electron.isUserLoggedIn().then((isLoggedIn) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (isLoggedIn) {
|
if (response) updateUserDetails(response);
|
||||||
fetchUserDetails().then((response) => {
|
|
||||||
if (response) updateUserDetails(response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
|
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@ -100,7 +100,6 @@ declare global {
|
|||||||
|
|
||||||
/* Misc */
|
/* Misc */
|
||||||
openExternal: (src: string) => Promise<void>;
|
openExternal: (src: string) => Promise<void>;
|
||||||
isUserLoggedIn: () => Promise<boolean>;
|
|
||||||
getVersion: () => Promise<string>;
|
getVersion: () => Promise<string>;
|
||||||
ping: () => string;
|
ping: () => string;
|
||||||
getDefaultDownloadsPath: () => Promise<string>;
|
getDefaultDownloadsPath: () => Promise<string>;
|
||||||
|
@ -57,8 +57,14 @@ export function useUserDetails() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fetchUserDetails = useCallback(async () => {
|
const fetchUserDetails = useCallback(async () => {
|
||||||
return window.electron.getMe();
|
return window.electron.getMe().then((userDetails) => {
|
||||||
}, []);
|
if (userDetails == null) {
|
||||||
|
clearUserDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDetails;
|
||||||
|
});
|
||||||
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (displayName: string, imageProfileUrl: string | null) => {
|
async (displayName: string, imageProfileUrl: string | null) => {
|
||||||
|
@ -122,7 +122,7 @@ export function HeroPanelActions() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => setShowGameOptionsModal(true)}
|
onClick={() => setShowGameOptionsModal(true)}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={deleting || isGameRunning}
|
disabled={deleting}
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
<GearIcon />
|
<GearIcon />
|
||||||
|
@ -8,6 +8,13 @@ export enum DownloadSourceStatus {
|
|||||||
Errored,
|
Errored,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UserNotLoggedInError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("user not logged in");
|
||||||
|
this.name = "UserNotLoggedInError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const FORMAT = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const FORMAT = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
|
||||||
export const formatBytes = (bytes: number): string => {
|
export const formatBytes = (bytes: number): string => {
|
||||||
|
@ -30,6 +30,16 @@ class Downloader:
|
|||||||
self.torrent_handles[game_id] = None
|
self.torrent_handles[game_id] = None
|
||||||
self.downloading_game_id = -1
|
self.downloading_game_id = -1
|
||||||
|
|
||||||
|
def abort_session(self):
|
||||||
|
for game_id in self.torrent_handles:
|
||||||
|
torrent_handle = self.torrent_handles[game_id]
|
||||||
|
torrent_handle.pause()
|
||||||
|
self.session.remove_torrent(torrent_handle)
|
||||||
|
|
||||||
|
self.session.abort()
|
||||||
|
self.torrent_handles = {}
|
||||||
|
self.downloading_game_id = -1
|
||||||
|
|
||||||
def get_download_status(self):
|
def get_download_status(self):
|
||||||
if self.downloading_game_id == -1:
|
if self.downloading_game_id == -1:
|
||||||
return None
|
return None
|
||||||
|
@ -2,16 +2,20 @@ import sys
|
|||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
import json
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import psutil
|
||||||
from downloader import Downloader
|
from downloader import Downloader
|
||||||
|
|
||||||
torrent_port = sys.argv[1]
|
torrent_port = sys.argv[1]
|
||||||
http_port = sys.argv[2]
|
http_port = sys.argv[2]
|
||||||
rpc_password = sys.argv[3]
|
rpc_password = sys.argv[3]
|
||||||
initial_download = json.loads(urllib.parse.unquote(sys.argv[4]))
|
start_download_payload = sys.argv[4]
|
||||||
|
|
||||||
downloader = Downloader(torrent_port)
|
downloader = None
|
||||||
|
|
||||||
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
|
if start_download_payload:
|
||||||
|
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
||||||
|
downloader = Downloader(torrent_port)
|
||||||
|
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
rpc_password_header = 'x-hydra-rpc-password'
|
rpc_password_header = 'x-hydra-rpc-password'
|
||||||
@ -30,11 +34,28 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
status = downloader.get_download_status()
|
status = downloader.get_download_status()
|
||||||
|
|
||||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
self.wfile.write(json.dumps(status).encode('utf-8'))
|
||||||
if self.path == "/healthcheck":
|
|
||||||
|
elif self.path == "/healthcheck":
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
|
elif self.path == "/process-list":
|
||||||
|
if self.headers.get(self.rpc_password_header) != rpc_password:
|
||||||
|
self.send_response(401)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'username'])]
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(process_list).encode('utf-8'))
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
|
global downloader
|
||||||
|
|
||||||
if self.path == "/action":
|
if self.path == "/action":
|
||||||
if self.headers.get(self.rpc_password_header) != rpc_password:
|
if self.headers.get(self.rpc_password_header) != rpc_password:
|
||||||
self.send_response(401)
|
self.send_response(401)
|
||||||
@ -45,13 +66,19 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
post_data = self.rfile.read(content_length)
|
post_data = self.rfile.read(content_length)
|
||||||
data = json.loads(post_data.decode('utf-8'))
|
data = json.loads(post_data.decode('utf-8'))
|
||||||
|
|
||||||
|
if downloader is None:
|
||||||
|
downloader = Downloader(torrent_port)
|
||||||
|
|
||||||
if data['action'] == 'start':
|
if data['action'] == 'start':
|
||||||
downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
|
downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
|
||||||
elif data['action'] == 'pause':
|
elif data['action'] == 'pause':
|
||||||
downloader.pause_download(data['game_id'])
|
downloader.pause_download(data['game_id'])
|
||||||
elif data['action'] == 'cancel':
|
elif data['action'] == 'cancel':
|
||||||
downloader.cancel_download(data['game_id'])
|
downloader.cancel_download(data['game_id'])
|
||||||
|
elif data['action'] == 'kill-torrent':
|
||||||
|
downloader.abort_session()
|
||||||
|
downloader = None
|
||||||
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
|
25
yarn.lock
25
yarn.lock
@ -2665,6 +2665,14 @@ aria-query@^5.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal "^2.0.3"
|
dequal "^2.0.3"
|
||||||
|
|
||||||
|
aria2@^4.1.2:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/aria2/-/aria2-4.1.2.tgz#0ecbc50beea82856c88b4de71dac336154f67362"
|
||||||
|
integrity sha512-qTBr2RY8RZQmiUmbj2KXFvkErNxU4aTHZszszzwhE8svy2PEVX+IYR/c4Rp9Tuw4QkeU8cylGy6McV6Yl8i7Qw==
|
||||||
|
dependencies:
|
||||||
|
node-fetch "^2.6.1"
|
||||||
|
ws "^7.4.0"
|
||||||
|
|
||||||
array-buffer-byte-length@^1.0.1:
|
array-buffer-byte-length@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz"
|
||||||
@ -5813,7 +5821,7 @@ node-domexception@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz"
|
||||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||||
|
|
||||||
node-fetch@^2.6.7:
|
node-fetch@^2.6.1, node-fetch@^2.6.7:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||||
@ -6311,11 +6319,6 @@ proxy-from-env@^1.1.0:
|
|||||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
ps-list@^8.1.1:
|
|
||||||
version "8.1.1"
|
|
||||||
resolved "https://registry.npmjs.org/ps-list/-/ps-list-8.1.1.tgz"
|
|
||||||
integrity sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==
|
|
||||||
|
|
||||||
psl@^1.1.33:
|
psl@^1.1.33:
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"
|
resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"
|
||||||
@ -6999,6 +7002,11 @@ strtok3@^7.0.0:
|
|||||||
"@tokenizer/token" "^0.3.0"
|
"@tokenizer/token" "^0.3.0"
|
||||||
peek-readable "^5.0.0"
|
peek-readable "^5.0.0"
|
||||||
|
|
||||||
|
sudo-prompt@^9.2.1:
|
||||||
|
version "9.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd"
|
||||||
|
integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==
|
||||||
|
|
||||||
sumchecker@^3.0.1:
|
sumchecker@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz"
|
||||||
@ -7609,6 +7617,11 @@ wrappy@1:
|
|||||||
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
||||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||||
|
|
||||||
|
ws@^7.4.0:
|
||||||
|
version "7.5.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||||
|
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||||
|
|
||||||
ws@^8.16.0:
|
ws@^8.16.0:
|
||||||
version "8.17.0"
|
version "8.17.0"
|
||||||
resolved "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user