mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-27 12:51:03 +00:00
feat: migrating games to leveldb
This commit is contained in:
@@ -14,16 +14,16 @@ import { achievementsLogger } from "../logger";
|
||||
import { Cracker } from "@shared";
|
||||
import { IsNull, Not } from "typeorm";
|
||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
|
||||
const fileStats: Map<string, number> = new Map();
|
||||
const fltFiles: Map<string, Set<string>> = new Map();
|
||||
|
||||
const watchAchievementsWindows = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((games) => games.filter((game) => !game.isDeleted));
|
||||
|
||||
if (games.length === 0) return;
|
||||
|
||||
@@ -32,7 +32,7 @@ const watchAchievementsWindows = async () => {
|
||||
for (const game of games) {
|
||||
const gameAchievementFiles: AchievementFile[] = [];
|
||||
|
||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
||||
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
|
||||
|
||||
gameAchievementFiles.push(
|
||||
|
||||
@@ -3,8 +3,8 @@ import fs from "node:fs";
|
||||
import { app } from "electron";
|
||||
import type { AchievementFile } from "@types";
|
||||
import { Cracker } from "@shared";
|
||||
import { Game } from "@main/entity";
|
||||
import { achievementsLogger } from "../logger";
|
||||
import type { Game } from "@types";
|
||||
|
||||
const getAppDataPath = () => {
|
||||
if (process.platform === "win32") {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { DownloadProgress } from "@types";
|
||||
import type { Download, DownloadProgress } from "@types";
|
||||
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
|
||||
import { PythonRPC } from "../python-rpc";
|
||||
import {
|
||||
@@ -20,16 +20,20 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity
|
||||
import { RealDebridClient } from "./real-debrid";
|
||||
import path from "path";
|
||||
import { logger } from "../logger";
|
||||
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export class DownloadManager {
|
||||
private static downloadingGameId: number | null = null;
|
||||
|
||||
public static async startRPC(game?: Game, initialSeeding?: Game[]) {
|
||||
public static async startRPC(
|
||||
download?: Download,
|
||||
downloadsToSeed?: Download[]
|
||||
) {
|
||||
PythonRPC.spawn(
|
||||
game?.status === "active"
|
||||
? await this.getDownloadPayload(game).catch(() => undefined)
|
||||
download?.status === "active"
|
||||
? await this.getDownloadPayload(download).catch(() => undefined)
|
||||
: undefined,
|
||||
initialSeeding?.map((game) => ({
|
||||
downloadsToSeed?.map((download) => ({
|
||||
game_id: game.id,
|
||||
url: game.uri!,
|
||||
save_path: game.downloadPath!,
|
||||
@@ -105,6 +109,7 @@ export class DownloadManager {
|
||||
const game = await gameRepository.findOne({
|
||||
where: { id: gameId, isDeleted: false },
|
||||
});
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOneBy({
|
||||
id: 1,
|
||||
});
|
||||
@@ -141,7 +146,8 @@ export class DownloadManager {
|
||||
this.cancelDownload(gameId);
|
||||
}
|
||||
|
||||
await downloadQueueRepository.delete({ game });
|
||||
await downloadsSublevel.del(levelKeys.game(game.shop, game.objectId));
|
||||
|
||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||
order: {
|
||||
id: "DESC",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const clearGamesRemoteIds = () => {
|
||||
return gameRepository.update({}, { remoteId: null });
|
||||
export const clearGamesRemoteIds = async () => {
|
||||
const games = await gamesSublevel.values().all();
|
||||
|
||||
await gamesSublevel.batch(
|
||||
games.map((game) => ({
|
||||
type: "put",
|
||||
key: levelKeys.game(game.shop, game.objectId),
|
||||
value: {
|
||||
...game,
|
||||
remoteId: null,
|
||||
},
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Game } from "@main/entity";
|
||||
import type { Game } from "@types";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
export const createGame = async (game: Game) => {
|
||||
return HydraApi.post(`/profile/games`, {
|
||||
objectId: game.objectID,
|
||||
objectId: game.objectId,
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
shop: game.shop,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
}).then((response) => {
|
||||
const { id: remoteId, playTimeInMilliseconds, lastTimePlayed } = response;
|
||||
|
||||
gameRepository.update(
|
||||
{ objectID: game.objectID },
|
||||
{ remoteId, playTimeInMilliseconds, lastTimePlayed }
|
||||
);
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
remoteId,
|
||||
playTimeInMilliseconds,
|
||||
lastTimePlayed,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Game } from "@main/entity";
|
||||
import type { Game } from "@types";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
|
||||
export const updateGamePlaytime = async (
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { chunk } from "lodash-es";
|
||||
import { IsNull } from "typeorm";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { mergeWithRemoteGames } from "./merge-with-remote-games";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { AchievementWatcherManager } from "../achievements/achievement-watcher-manager";
|
||||
import { gamesSublevel } from "@main/level";
|
||||
|
||||
export const uploadGamesBatch = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: { remoteId: IsNull(), isDeleted: false },
|
||||
});
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((results) => {
|
||||
return results.filter(
|
||||
(game) => !game.isDeleted && game.remoteId === null
|
||||
);
|
||||
});
|
||||
|
||||
const gamesChunks = chunk(games, 200);
|
||||
|
||||
@@ -18,7 +22,7 @@ export const uploadGamesBatch = async () => {
|
||||
"/profile/games/batch",
|
||||
chunk.map((game) => {
|
||||
return {
|
||||
objectId: game.objectID,
|
||||
objectId: game.objectId,
|
||||
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
|
||||
shop: game.shop,
|
||||
lastTimePlayed: game.lastTimePlayed,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { createGame, updateGamePlaytime } from "./library-sync";
|
||||
import type { GameRunning } from "@types";
|
||||
import type { Game, GameRunning } from "@types";
|
||||
import { PythonRPC } from "./python-rpc";
|
||||
import { Game } from "@main/entity";
|
||||
import axios from "axios";
|
||||
import { exec } from "child_process";
|
||||
import { ProcessPayload } from "./download/types";
|
||||
import { gamesSublevel, levelKeys } from "@main/level";
|
||||
|
||||
const commands = {
|
||||
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
|
||||
@@ -14,7 +13,7 @@ const commands = {
|
||||
};
|
||||
|
||||
export const gamesPlaytime = new Map<
|
||||
number,
|
||||
string,
|
||||
{ lastTick: number; firstTick: number; lastSyncTick: number }
|
||||
>();
|
||||
|
||||
@@ -82,23 +81,28 @@ const findGamePathByProcess = (
|
||||
const pathSet = processMap.get(executable.exe);
|
||||
|
||||
if (pathSet) {
|
||||
pathSet.forEach((path) => {
|
||||
pathSet.forEach(async (path) => {
|
||||
if (path.toLowerCase().endsWith(executable.name)) {
|
||||
gameRepository.update(
|
||||
{ objectID: gameId, shop: "steam" },
|
||||
{ executablePath: path }
|
||||
);
|
||||
const gameKey = levelKeys.game("steam", gameId);
|
||||
const game = await gamesSublevel.get(gameKey);
|
||||
|
||||
if (game) {
|
||||
gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
executablePath: path,
|
||||
});
|
||||
}
|
||||
|
||||
if (isLinuxPlatform) {
|
||||
exec(commands.findWineDir, (err, out) => {
|
||||
if (err) return;
|
||||
|
||||
gameRepository.update(
|
||||
{ objectID: gameId, shop: "steam" },
|
||||
{
|
||||
if (game) {
|
||||
gamesSublevel.put(gameKey, {
|
||||
...game,
|
||||
winePrefixPath: out.trim().replace("/drive_c/windows", ""),
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -159,11 +163,12 @@ const getSystemProcessMap = async () => {
|
||||
};
|
||||
|
||||
export const watchProcesses = async () => {
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
const games = await gamesSublevel
|
||||
.values()
|
||||
.all()
|
||||
.then((results) => {
|
||||
return results.filter((game) => game.isDeleted === false);
|
||||
});
|
||||
|
||||
if (!games.length) return;
|
||||
|
||||
@@ -172,8 +177,8 @@ export const watchProcesses = async () => {
|
||||
for (const game of games) {
|
||||
const executablePath = game.executablePath;
|
||||
if (!executablePath) {
|
||||
if (gameExecutables[game.objectID]) {
|
||||
findGamePathByProcess(processMap, game.objectID);
|
||||
if (gameExecutables[game.objectId]) {
|
||||
findGamePathByProcess(processMap, game.objectId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -185,12 +190,12 @@ export const watchProcesses = async () => {
|
||||
const hasProcess = processMap.get(executable)?.has(executablePath);
|
||||
|
||||
if (hasProcess) {
|
||||
if (gamesPlaytime.has(game.id)) {
|
||||
if (gamesPlaytime.has(`${game.shop}-${game.objectId}`)) {
|
||||
onTickGame(game);
|
||||
} else {
|
||||
onOpenGame(game);
|
||||
}
|
||||
} else if (gamesPlaytime.has(game.id)) {
|
||||
} else if (gamesPlaytime.has(`${game.shop}-${game.objectId}`)) {
|
||||
onCloseGame(game);
|
||||
}
|
||||
}
|
||||
@@ -215,7 +220,7 @@ export const watchProcesses = async () => {
|
||||
function onOpenGame(game: Game) {
|
||||
const now = performance.now();
|
||||
|
||||
gamesPlaytime.set(game.id, {
|
||||
gamesPlaytime.set(`${game.shop}-${game.objectId}`, {
|
||||
lastTick: now,
|
||||
firstTick: now,
|
||||
lastSyncTick: now,
|
||||
@@ -230,16 +235,23 @@ function onOpenGame(game: Game) {
|
||||
|
||||
function onTickGame(game: Game) {
|
||||
const now = performance.now();
|
||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||
const gamePlaytime = gamesPlaytime.get(`${game.shop}-${game.objectId}`)!;
|
||||
|
||||
const delta = now - gamePlaytime.lastTick;
|
||||
|
||||
gameRepository.update(game.id, {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
|
||||
gamesPlaytime.set(game.id, {
|
||||
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
|
||||
...game,
|
||||
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
|
||||
lastTimePlayed: new Date(),
|
||||
});
|
||||
|
||||
gamesPlaytime.set(`${game.shop}-${game.objectId}`, {
|
||||
...gamePlaytime,
|
||||
lastTick: now,
|
||||
});
|
||||
@@ -255,7 +267,7 @@ function onTickGame(game: Game) {
|
||||
|
||||
gamePromise
|
||||
.then(() => {
|
||||
gamesPlaytime.set(game.id, {
|
||||
gamesPlaytime.set(`${game.shop}-${game.objectId}`, {
|
||||
...gamePlaytime,
|
||||
lastSyncTick: now,
|
||||
});
|
||||
@@ -265,8 +277,8 @@ function onTickGame(game: Game) {
|
||||
}
|
||||
|
||||
const onCloseGame = (game: Game) => {
|
||||
const gamePlaytime = gamesPlaytime.get(game.id)!;
|
||||
gamesPlaytime.delete(game.id);
|
||||
const gamePlaytime = gamesPlaytime.get(`${game.shop}-${game.objectId}`)!;
|
||||
gamesPlaytime.delete(`${game.shop}-${game.objectId}`);
|
||||
|
||||
if (game.remoteId) {
|
||||
updateGamePlaytime(
|
||||
|
||||
Reference in New Issue
Block a user