feat: migrating games to leveldb

This commit is contained in:
Chubby Granny Chaser
2025-01-19 17:59:39 +00:00
parent c115040e90
commit 1f0e195854
34 changed files with 410 additions and 343 deletions

View File

@@ -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(

View File

@@ -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") {

View File

@@ -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",

View File

@@ -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,
},
}))
);
};

View File

@@ -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,
});
});
};

View File

@@ -1,4 +1,4 @@
import { Game } from "@main/entity";
import type { Game } from "@types";
import { HydraApi } from "../hydra-api";
export const updateGamePlaytime = async (

View File

@@ -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,

View File

@@ -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(