diff --git a/src/main/events/cloud-save/get-game-backup-preview.ts b/src/main/events/cloud-save/get-game-backup-preview.ts index daffa487..0dc471e3 100644 --- a/src/main/events/cloud-save/get-game-backup-preview.ts +++ b/src/main/events/cloud-save/get-game-backup-preview.ts @@ -1,19 +1,14 @@ import { registerEvent } from "../register-event"; import type { GameShop } from "@types"; import { Ludusavi } from "@main/services"; -import { gameRepository } from "@main/repository"; +import { gamesSublevel, levelKeys } from "@main/level"; const getGameBackupPreview = async ( _event: Electron.IpcMainInvokeEvent, objectId: string, shop: GameShop ) => { - const game = await gameRepository.findOne({ - where: { - objectID: objectId, - shop, - }, - }); + const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath); }; diff --git a/src/main/events/cloud-save/upload-save-game.ts b/src/main/events/cloud-save/upload-save-game.ts index b3a514f5..7c77e67a 100644 --- a/src/main/events/cloud-save/upload-save-game.ts +++ b/src/main/events/cloud-save/upload-save-game.ts @@ -10,7 +10,8 @@ import os from "node:os"; import { backupsPath } from "@main/constants"; import { app } from "electron"; import { normalizePath } from "@main/helpers"; -import { gameRepository } from "@main/repository"; +import { gamesSublevel } from "@main/level"; +import { levelKeys } from "@main/level"; const bundleBackup = async ( shop: GameShop, @@ -46,12 +47,7 @@ const uploadSaveGame = async ( shop: GameShop, downloadOptionTitle: string | null ) => { - const game = await gameRepository.findOne({ - where: { - objectID: objectId, - shop, - }, - }); + const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); const bundleLocation = await bundleBackup( shop, diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index 898c25cd..dd9a6bd6 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -2,18 +2,19 @@ import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -import type { GameShop } from "@types"; +import type { Game, GameShop } from "@types"; import { steamGamesWorker } from "@main/workers"; import { createGame } from "@main/services/library-sync"; import { steamUrlBuilder } from "@shared"; import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements"; +import { gamesSublevel, levelKeys } from "@main/level"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, + shop: GameShop, objectId: string, - title: string, - shop: GameShop + title: string ) => { return gameRepository .update( @@ -36,17 +37,15 @@ const addGameToLibrary = async ( ? steamUrlBuilder.icon(objectId, steamGame.clientIcon) : null; - await gameRepository.insert({ + const game: Game = { title, iconUrl, - objectID: objectId, + objectId, shop, - }); - } + }; - const game = await gameRepository.findOne({ - where: { objectID: objectId }, - }); + await gamesSublevel.put(levelKeys.game(shop, objectId), game); + } updateLocalUnlockedAchivements(game!); diff --git a/src/main/events/library/close-game.ts b/src/main/events/library/close-game.ts index f69bf120..d01f3f4f 100644 --- a/src/main/events/library/close-game.ts +++ b/src/main/events/library/close-game.ts @@ -1,10 +1,11 @@ -import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import { logger } from "@main/services"; import sudo from "sudo-prompt"; import { app } from "electron"; import { PythonRPC } from "@main/services/python-rpc"; import { ProcessPayload } from "@main/services/download/types"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { GameShop } from "@types"; const getKillCommand = (pid: number) => { if (process.platform == "win32") { @@ -16,15 +17,14 @@ const getKillCommand = (pid: number) => { const closeGame = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { const processes = (await PythonRPC.rpc.get("/process-list")).data || []; - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - }); + const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); if (!game) return; diff --git a/src/main/events/library/create-game-shortcut.ts b/src/main/events/library/create-game-shortcut.ts index 4e6935f4..6e278871 100644 --- a/src/main/events/library/create-game-shortcut.ts +++ b/src/main/events/library/create-game-shortcut.ts @@ -1,18 +1,18 @@ -import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; -import { IsNull, Not } from "typeorm"; import createDesktopShortcut from "create-desktop-shortcuts"; import path from "node:path"; import { app } from "electron"; import { removeSymbolsFromName } from "@shared"; +import { GameShop } from "@types"; +import { gamesSublevel, levelKeys } from "@main/level"; const createGameShortcut = async ( _event: Electron.IpcMainInvokeEvent, - id: number + shop: GameShop, + objectId: string ): Promise => { - const game = await gameRepository.findOne({ - where: { id, executablePath: Not(IsNull()) }, - }); + const gameKey = levelKeys.game(shop, objectId); + const game = await gamesSublevel.get(gameKey); if (game) { const filePath = game.executablePath; diff --git a/src/main/events/library/delete-game-folder.ts b/src/main/events/library/delete-game-folder.ts index bdae9b3e..9c290fe0 100644 --- a/src/main/events/library/delete-game-folder.ts +++ b/src/main/events/library/delete-game-folder.ts @@ -1,37 +1,27 @@ import path from "node:path"; import fs from "node:fs"; -import { gameRepository } from "@main/repository"; - import { getDownloadsPath } from "../helpers/get-downloads-path"; import { logger } from "@main/services"; import { registerEvent } from "../register-event"; +import { GameShop } from "@types"; +import { downloadsSublevel, levelKeys } from "@main/level"; const deleteGameFolder = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ): Promise => { - const game = await gameRepository.findOne({ - where: [ - { - id: gameId, - isDeleted: false, - status: "removed", - }, - { - id: gameId, - progress: 1, - isDeleted: false, - }, - ], - }); + const downloadKey = levelKeys.game(shop, objectId); - if (!game) return; + const download = await downloadsSublevel.get(downloadKey); - if (game.folderName) { + if (!download) return; + + if (download.folderName) { const folderPath = path.join( - game.downloadPath ?? (await getDownloadsPath()), - game.folderName + download.downloadPath ?? (await getDownloadsPath()), + download.folderName ); if (fs.existsSync(folderPath)) { @@ -52,10 +42,7 @@ const deleteGameFolder = async ( } } - await gameRepository.update( - { id: gameId }, - { downloadPath: null, folderName: null, status: null, progress: 0 } - ); + await downloadsSublevel.del(downloadKey); }; registerEvent("deleteGameFolder", deleteGameFolder); diff --git a/src/main/events/library/get-game-by-object-id.ts b/src/main/events/library/get-game-by-object-id.ts index d68aac69..57e4a2bd 100644 --- a/src/main/events/library/get-game-by-object-id.ts +++ b/src/main/events/library/get-game-by-object-id.ts @@ -1,16 +1,17 @@ -import { gameRepository } from "@main/repository"; - import { registerEvent } from "../register-event"; +import { levelKeys } from "@main/level"; +import { gamesSublevel } from "@main/level"; +import type { GameShop } from "@types"; const getGameByObjectId = async ( _event: Electron.IpcMainInvokeEvent, + shop: GameShop, objectId: string -) => - gameRepository.findOne({ - where: { - objectID: objectId, - isDeleted: false, - }, - }); +) => { + const gameKey = levelKeys.game(shop, objectId); + const game = await gamesSublevel.get(gameKey); + + return game; +}; registerEvent("getGameByObjectId", getGameByObjectId); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index ad982308..73dc2e04 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -1,17 +1,14 @@ -import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { gamesSublevel } from "@main/level"; -const getLibrary = async () => - gameRepository.find({ - where: { - isDeleted: false, - }, - relations: { - downloadQueue: true, - }, - order: { - createdAt: "desc", - }, - }); +const getLibrary = async () => { + // TODO: Add sorting + return gamesSublevel + .values() + .all() + .then((results) => { + return results.filter((game) => game.isDeleted === false); + }); +}; registerEvent("getLibrary", getLibrary); diff --git a/src/main/events/library/open-game-executable-path.ts b/src/main/events/library/open-game-executable-path.ts index 09a0837c..96a993a6 100644 --- a/src/main/events/library/open-game-executable-path.ts +++ b/src/main/events/library/open-game-executable-path.ts @@ -1,14 +1,14 @@ import { shell } from "electron"; -import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { GameShop } from "@types"; const openGameExecutablePath = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - }); + const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); if (!game || !game.executablePath) return; diff --git a/src/main/events/library/open-game-installer-path.ts b/src/main/events/library/open-game-installer-path.ts index dd7383ad..971bd3ca 100644 --- a/src/main/events/library/open-game-installer-path.ts +++ b/src/main/events/library/open-game-installer-path.ts @@ -1,16 +1,16 @@ import { shell } from "electron"; import path from "node:path"; -import { gameRepository } from "@main/repository"; import { getDownloadsPath } from "../helpers/get-downloads-path"; import { registerEvent } from "../register-event"; +import { GameShop } from "@types"; +import { gamesSublevel, levelKeys } from "@main/level"; const openGameInstallerPath = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - }); + const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); if (!game || !game.folderName || !game.downloadPath) return true; diff --git a/src/main/events/library/open-game-installer.ts b/src/main/events/library/open-game-installer.ts index b21a6f16..949b4364 100644 --- a/src/main/events/library/open-game-installer.ts +++ b/src/main/events/library/open-game-installer.ts @@ -4,11 +4,11 @@ import fs from "node:fs"; import { writeFile } from "node:fs/promises"; import { spawnSync, exec } from "node:child_process"; -import { gameRepository } from "@main/repository"; - import { generateYML } from "../helpers/generate-lutris-yaml"; import { getDownloadsPath } from "../helpers/get-downloads-path"; import { registerEvent } from "../register-event"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { GameShop } from "@types"; const executeGameInstaller = (filePath: string) => { if (process.platform === "win32") { @@ -26,13 +26,12 @@ const executeGameInstaller = (filePath: string) => { const openGameInstaller = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - }); + const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); - if (!game || !game.folderName) return true; + if (!game || game.isDeleted || !game.folderName) return true; const gamePath = path.join( game.downloadPath ?? (await getDownloadsPath()), @@ -40,7 +39,8 @@ const openGameInstaller = async ( ); if (!fs.existsSync(gamePath)) { - await gameRepository.update({ id: gameId }, { status: null }); + // TODO: LEVELDB Remove download? + // await gameRepository.update({ id: gameId }, { status: null }); return true; } diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index a8fc8b01..dd398819 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -1,26 +1,27 @@ import { registerEvent } from "../register-event"; -import { gameRepository } from "../../repository"; -import { HydraApi, logger } from "@main/services"; +import { HydraApi } from "@main/services"; +import { levelKeys } from "@main/level"; +import { gamesSublevel } from "@main/level"; +import type { GameShop } from "@types"; const removeGameFromLibrary = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - gameRepository.update( - { id: gameId }, - { isDeleted: true, executablePath: null } - ); + const gameKey = levelKeys.game(shop, objectId); + const game = await gamesSublevel.get(gameKey); - removeRemoveGameFromLibrary(gameId).catch((err) => { - logger.error("removeRemoveGameFromLibrary", err); - }); -}; + if (game) { + await gamesSublevel.put(gameKey, { + ...game, + isDeleted: true, + executablePath: null, + }); -const removeRemoveGameFromLibrary = async (gameId: number) => { - const game = await gameRepository.findOne({ where: { id: gameId } }); - - if (game?.remoteId) { - HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); + if (game?.remoteId) { + HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); + } } }; diff --git a/src/main/events/library/remove-game.ts b/src/main/events/library/remove-game.ts index 687366c5..afc205f7 100644 --- a/src/main/events/library/remove-game.ts +++ b/src/main/events/library/remove-game.ts @@ -1,21 +1,15 @@ import { registerEvent } from "../register-event"; -import { gameRepository } from "../../repository"; +import { downloadsSublevel } from "@main/level"; +import { GameShop } from "@types"; +import { levelKeys } from "@main/level"; const removeGame = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - await gameRepository.update( - { - id: gameId, - }, - { - status: "removed", - downloadPath: null, - bytesDownloaded: 0, - progress: 0, - } - ); + const downloadKey = levelKeys.game(shop, objectId); + await downloadsSublevel.del(downloadKey); }; registerEvent("removeGame", removeGame); diff --git a/src/main/events/library/reset-game-achievements.ts b/src/main/events/library/reset-game-achievements.ts index 0ea26adf..b3d2daa2 100644 --- a/src/main/events/library/reset-game-achievements.ts +++ b/src/main/events/library/reset-game-achievements.ts @@ -1,17 +1,22 @@ -import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import { findAchievementFiles } from "@main/services/achievements/find-achivement-files"; import fs from "fs"; import { achievementsLogger, HydraApi, WindowManager } from "@main/services"; import { getUnlockedAchievements } from "../user/get-unlocked-achievements"; -import { gameAchievementsSublevel, levelKeys } from "@main/level"; +import { + gameAchievementsSublevel, + gamesSublevel, + levelKeys, +} from "@main/level"; +import type { GameShop } from "@types"; const resetGameAchievements = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { try { - const game = await gameRepository.findOne({ where: { id: gameId } }); + const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); if (!game) return; @@ -24,37 +29,34 @@ const resetGameAchievements = async ( } } - const levelKey = levelKeys.game(game.shop, game.objectID); + const levelKey = levelKeys.game(game.shop, game.objectId); await gameAchievementsSublevel .get(levelKey) .then(async (gameAchievements) => { if (gameAchievements) { - await gameAchievementsSublevel.put( - levelKeys.game(game.shop, game.objectID), - { - ...gameAchievements, - unlockedAchievements: [], - } - ); + await gameAchievementsSublevel.put(levelKey, { + ...gameAchievements, + unlockedAchievements: [], + }); } }); await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then( () => achievementsLogger.log( - `Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}` + `Deleted achievements from ${game.remoteId} - ${game.objectId} - ${game.title}` ) ); const gameAchievements = await getUnlockedAchievements( - game.objectID, + game.objectId, game.shop, true ); WindowManager.mainWindow?.webContents.send( - `on-update-achievements-${game.objectID}-${game.shop}`, + `on-update-achievements-${game.objectId}-${game.shop}`, gameAchievements ); } catch (error) { diff --git a/src/main/events/library/select-game-wine-prefix.ts b/src/main/events/library/select-game-wine-prefix.ts index d9f01c08..87bc2cde 100644 --- a/src/main/events/library/select-game-wine-prefix.ts +++ b/src/main/events/library/select-game-wine-prefix.ts @@ -1,13 +1,24 @@ -import { gameRepository } from "@main/repository"; - import { registerEvent } from "../register-event"; +import { gamesSublevel } from "@main/level"; +import { levelKeys } from "@main/level"; +import type { GameShop } from "@types"; const selectGameWinePrefix = async ( _event: Electron.IpcMainInvokeEvent, - id: number, + shop: GameShop, + objectId: string, winePrefixPath: string | null ) => { - return gameRepository.update({ id }, { winePrefixPath: winePrefixPath }); + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + + if (!game) return; + + await gamesSublevel.put(gameKey, { + ...game, + winePrefixPath: winePrefixPath, + }); }; registerEvent("selectGameWinePrefix", selectGameWinePrefix); diff --git a/src/main/events/library/update-executable-path.ts b/src/main/events/library/update-executable-path.ts index aee80771..e753706b 100644 --- a/src/main/events/library/update-executable-path.ts +++ b/src/main/events/library/update-executable-path.ts @@ -1,25 +1,27 @@ -import { gameRepository } from "@main/repository"; - import { registerEvent } from "../register-event"; import { parseExecutablePath } from "../helpers/parse-executable-path"; +import { gamesSublevel, levelKeys } from "@main/level"; +import type { GameShop } from "@types"; const updateExecutablePath = async ( _event: Electron.IpcMainInvokeEvent, - id: number, + shop: GameShop, + objectId: string, executablePath: string | null ) => { const parsedPath = executablePath ? parseExecutablePath(executablePath) : null; - return gameRepository.update( - { - id, - }, - { - executablePath: parsedPath, - } - ); + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + if (!game) return; + + await gamesSublevel.put(gameKey, { + ...game, + executablePath: parsedPath, + }); }; registerEvent("updateExecutablePath", updateExecutablePath); diff --git a/src/main/events/library/update-launch-options.ts b/src/main/events/library/update-launch-options.ts index b33d031c..9e0277f3 100644 --- a/src/main/events/library/update-launch-options.ts +++ b/src/main/events/library/update-launch-options.ts @@ -1,19 +1,24 @@ -import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { levelKeys } from "@main/level"; +import { gamesSublevel } from "@main/level"; +import { GameShop } from "@types"; const updateLaunchOptions = async ( _event: Electron.IpcMainInvokeEvent, - id: number, + shop: GameShop, + objectId: string, launchOptions: string | null ) => { - return gameRepository.update( - { - id, - }, - { + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + + if (game) { + await gamesSublevel.put(gameKey, { + ...game, launchOptions: launchOptions?.trim() != "" ? launchOptions : null, - } - ); + }); + } }; registerEvent("updateLaunchOptions", updateLaunchOptions); diff --git a/src/main/events/library/verify-executable-path.ts b/src/main/events/library/verify-executable-path.ts index 22295ac7..a48a0d38 100644 --- a/src/main/events/library/verify-executable-path.ts +++ b/src/main/events/library/verify-executable-path.ts @@ -1,13 +1,17 @@ -import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { gamesSublevel } from "@main/level"; const verifyExecutablePathInUse = async ( _event: Electron.IpcMainInvokeEvent, executablePath: string ) => { - return gameRepository.findOne({ - where: { executablePath }, - }); + for await (const game of gamesSublevel.values()) { + if (game.executablePath === executablePath) { + return true; + } + } + + return false; }; registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse); diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts index ce61c4e2..a96a464c 100644 --- a/src/main/level/sublevels/index.ts +++ b/src/main/level/sublevels/index.ts @@ -1,3 +1,4 @@ +export * from "./downloads"; export * from "./games"; export * from "./game-shop-cache"; export * from "./game-achievements"; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts index f2bb6f3c..5a787f13 100644 --- a/src/main/level/sublevels/keys.ts +++ b/src/main/level/sublevels/keys.ts @@ -9,4 +9,5 @@ export const levelKeys = { gameShopCacheItem: (shop: GameShop, objectId: string, language: string) => `${shop}:${objectId}:${language}`, gameAchievements: "gameAchievements", + downloads: "downloads", }; diff --git a/src/main/main.ts b/src/main/main.ts index add619e1..5f5ec768 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,16 +1,13 @@ import { DownloadManager, Ludusavi, startMainLoop } from "./services"; -import { - downloadQueueRepository, - gameRepository, - userPreferencesRepository, -} from "./repository"; +import { userPreferencesRepository } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/download/real-debrid"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; import { Aria2 } from "./services/aria2"; +import { downloadsSublevel } from "./level/sublevels/downloads"; +import { sortBy } from "lodash-es"; import { Downloader } from "@shared"; -import { IsNull, Not } from "typeorm"; const loadState = async (userPreferences: UserPreferences | null) => { import("./events"); @@ -27,25 +24,24 @@ const loadState = async (userPreferences: UserPreferences | null) => { uploadGamesBatch(); }); - const [nextQueueItem] = await downloadQueueRepository.find({ - order: { - id: "DESC", - }, - relations: { - game: true, - }, - }); + const downloads = await downloadsSublevel + .values() + .all() + .then((games) => { + return sortBy(games, "timestamp", "DESC"); + }); - const seedList = await gameRepository.find({ - where: { - shouldSeed: true, - downloader: Downloader.Torrent, - progress: 1, - uri: Not(IsNull()), - }, - }); + const [nextItemOnQueue] = downloads; - await DownloadManager.startRPC(nextQueueItem?.game, seedList); + const downloadsToSeed = downloads.filter( + (download) => + download.shouldSeed && + download.downloader === Downloader.Torrent && + download.progress === 1 && + download.uri !== null + ); + + await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed); startMainLoop(); }; diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 6a1eb11c..754847c9 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -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 = new Map(); const fltFiles: Map> = 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( diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 4fc6a4cd..0578065c 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -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") { diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 134a74e6..f85b018e 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -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", diff --git a/src/main/services/library-sync/clear-games-remote-id.ts b/src/main/services/library-sync/clear-games-remote-id.ts index f26d65f1..20989bc9 100644 --- a/src/main/services/library-sync/clear-games-remote-id.ts +++ b/src/main/services/library-sync/clear-games-remote-id.ts @@ -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, + }, + })) + ); }; diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts index 6c701c9a..54718c1d 100644 --- a/src/main/services/library-sync/create-game.ts +++ b/src/main/services/library-sync/create-game.ts @@ -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, + }); }); }; diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts index 28c3bed3..3689b302 100644 --- a/src/main/services/library-sync/update-game-playtime.ts +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -1,4 +1,4 @@ -import { Game } from "@main/entity"; +import type { Game } from "@types"; import { HydraApi } from "../hydra-api"; export const updateGamePlaytime = async ( diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index 79559a35..90c97e8b 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -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, diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index c6cb7e10..a9bf9b92 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -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( diff --git a/src/preload/index.ts b/src/preload/index.ts index 316397d2..cda910b3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -22,16 +22,16 @@ contextBridge.exposeInMainWorld("electron", { /* Torrenting */ startGameDownload: (payload: StartGameDownloadPayload) => ipcRenderer.invoke("startGameDownload", payload), - cancelGameDownload: (gameId: number) => - ipcRenderer.invoke("cancelGameDownload", gameId), - pauseGameDownload: (gameId: number) => - ipcRenderer.invoke("pauseGameDownload", gameId), - resumeGameDownload: (gameId: number) => - ipcRenderer.invoke("resumeGameDownload", gameId), - pauseGameSeed: (gameId: number) => - ipcRenderer.invoke("pauseGameSeed", gameId), - resumeGameSeed: (gameId: number) => - ipcRenderer.invoke("resumeGameSeed", gameId), + cancelGameDownload: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("cancelGameDownload", shop, objectId), + pauseGameDownload: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("pauseGameDownload", shop, objectId), + resumeGameDownload: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("resumeGameDownload", shop, objectId), + pauseGameSeed: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("pauseGameSeed", shop, objectId), + resumeGameSeed: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("resumeGameSeed", shop, objectId), onDownloadProgress: (cb: (value: DownloadProgress) => void) => { const listener = ( _event: Electron.IpcRendererEvent, @@ -98,40 +98,61 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("putDownloadSource", objectIds), /* Library */ - addGameToLibrary: (objectId: string, title: string, shop: GameShop) => - ipcRenderer.invoke("addGameToLibrary", objectId, title, shop), - createGameShortcut: (id: number) => - ipcRenderer.invoke("createGameShortcut", id), - updateExecutablePath: (id: number, executablePath: string | null) => - ipcRenderer.invoke("updateExecutablePath", id, executablePath), - updateLaunchOptions: (id: number, launchOptions: string | null) => - ipcRenderer.invoke("updateLaunchOptions", id, launchOptions), - selectGameWinePrefix: (id: number, winePrefixPath: string | null) => - ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath), + addGameToLibrary: (shop: GameShop, objectId: string, title: string) => + ipcRenderer.invoke("addGameToLibrary", shop, objectId, title), + createGameShortcut: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("createGameShortcut", shop, objectId), + updateExecutablePath: ( + shop: GameShop, + objectId: string, + executablePath: string | null + ) => + ipcRenderer.invoke("updateExecutablePath", shop, objectId, executablePath), + updateLaunchOptions: ( + shop: GameShop, + objectId: string, + launchOptions: string | null + ) => ipcRenderer.invoke("updateLaunchOptions", shop, objectId, launchOptions), + selectGameWinePrefix: ( + shop: GameShop, + objectId: string, + winePrefixPath: string | null + ) => + ipcRenderer.invoke("selectGameWinePrefix", shop, objectId, winePrefixPath), verifyExecutablePathInUse: (executablePath: string) => ipcRenderer.invoke("verifyExecutablePathInUse", executablePath), getLibrary: () => ipcRenderer.invoke("getLibrary"), - openGameInstaller: (gameId: number) => - ipcRenderer.invoke("openGameInstaller", gameId), - openGameInstallerPath: (gameId: number) => - ipcRenderer.invoke("openGameInstallerPath", gameId), - openGameExecutablePath: (gameId: number) => - ipcRenderer.invoke("openGameExecutablePath", gameId), + openGameInstaller: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("openGameInstaller", shop, objectId), + openGameInstallerPath: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("openGameInstallerPath", shop, objectId), + openGameExecutablePath: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("openGameExecutablePath", shop, objectId), openGame: ( - gameId: number, + shop: GameShop, + objectId: string, executablePath: string, launchOptions: string | null - ) => ipcRenderer.invoke("openGame", gameId, executablePath, launchOptions), - closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), - removeGameFromLibrary: (gameId: number) => - ipcRenderer.invoke("removeGameFromLibrary", gameId), - removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId), - deleteGameFolder: (gameId: number) => - ipcRenderer.invoke("deleteGameFolder", gameId), + ) => + ipcRenderer.invoke( + "openGame", + shop, + objectId, + executablePath, + launchOptions + ), + closeGame: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("closeGame", shop, objectId), + removeGameFromLibrary: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("removeGameFromLibrary", shop, objectId), + removeGame: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("removeGame", shop, objectId), + deleteGameFolder: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("deleteGameFolder", shop, objectId), getGameByObjectId: (objectId: string) => ipcRenderer.invoke("getGameByObjectId", objectId), - resetGameAchievements: (gameId: number) => - ipcRenderer.invoke("resetGameAchievements", gameId), + resetGameAchievements: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("resetGameAchievements", shop, objectId), onGamesRunning: ( cb: ( gamesRunning: Pick[] diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 2ee60347..d4d79961 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -40,11 +40,11 @@ declare global { interface Electron { /* Torrenting */ startGameDownload: (payload: StartGameDownloadPayload) => Promise; - cancelGameDownload: (gameId: number) => Promise; - pauseGameDownload: (gameId: number) => Promise; - resumeGameDownload: (gameId: number) => Promise; - pauseGameSeed: (gameId: number) => Promise; - resumeGameSeed: (gameId: number) => Promise; + cancelGameDownload: (shop: GameShop, objectId: string) => Promise; + pauseGameDownload: (shop: GameShop, objectId: string) => Promise; + resumeGameDownload: (shop: GameShop, objectId: string) => Promise; + pauseGameSeed: (shop: GameShop, objectId: string) => Promise; + resumeGameSeed: (shop: GameShop, objectId: string) => Promise; onDownloadProgress: ( cb: (value: DownloadProgress) => void ) => () => Electron.IpcRenderer; @@ -82,45 +82,55 @@ declare global { /* Library */ addGameToLibrary: ( + shop: GameShop, objectId: string, - title: string, - shop: GameShop + title: string ) => Promise; - createGameShortcut: (id: number) => Promise; + createGameShortcut: (shop: GameShop, objectId: string) => Promise; updateExecutablePath: ( - id: number, + shop: GameShop, + objectId: string, executablePath: string | null ) => Promise; updateLaunchOptions: ( - id: number, + shop: GameShop, + objectId: string, launchOptions: string | null ) => Promise; selectGameWinePrefix: ( - id: number, + shop: GameShop, + objectId: string, winePrefixPath: string | null ) => Promise; verifyExecutablePathInUse: (executablePath: string) => Promise; getLibrary: () => Promise; - openGameInstaller: (gameId: number) => Promise; - openGameInstallerPath: (gameId: number) => Promise; - openGameExecutablePath: (gameId: number) => Promise; + openGameInstaller: (shop: GameShop, objectId: string) => Promise; + openGameInstallerPath: ( + shop: GameShop, + objectId: string + ) => Promise; + openGameExecutablePath: (shop: GameShop, objectId: string) => Promise; openGame: ( - gameId: number, + shop: GameShop, + objectId: string, executablePath: string, launchOptions: string | null ) => Promise; - closeGame: (gameId: number) => Promise; - removeGameFromLibrary: (gameId: number) => Promise; - removeGame: (gameId: number) => Promise; - deleteGameFolder: (gameId: number) => Promise; - getGameByObjectId: (objectId: string) => Promise; + closeGame: (shop: GameShop, objectId: string) => Promise; + removeGameFromLibrary: (shop: GameShop, objectId: string) => Promise; + removeGame: (shop: GameShop, objectId: string) => Promise; + deleteGameFolder: (shop: GameShop, objectId: string) => Promise; + getGameByObjectId: ( + shop: GameShop, + objectId: string + ) => Promise; onGamesRunning: ( cb: ( gamesRunning: Pick[] ) => void ) => () => Electron.IpcRenderer; onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer; - resetGameAchievements: (gameId: number) => Promise; + resetGameAchievements: (shop: GameShop, objectId: string) => Promise; /* User preferences */ getUserPreferences: () => Promise; updateUserPreferences: ( diff --git a/src/types/download.types.ts b/src/types/download.types.ts index e6186e37..33fc5073 100644 --- a/src/types/download.types.ts +++ b/src/types/download.types.ts @@ -1,4 +1,5 @@ -import type { Game, GameStatus } from "./game.types"; +import type { GameStatus } from "./game.types"; +import { Game } from "./level.types"; export interface DownloadProgress { downloadSpeed: number; diff --git a/src/types/game.types.ts b/src/types/game.types.ts index acadf7ad..18e3cabb 100644 --- a/src/types/game.types.ts +++ b/src/types/game.types.ts @@ -1,5 +1,3 @@ -import type { Downloader } from "@shared"; - export type GameStatus = | "active" | "waiting" @@ -11,33 +9,6 @@ export type GameStatus = export type GameShop = "steam" | "epic"; -export interface Game { - // TODO: To be depreacted - id: number; - title: string; - iconUrl: string; - status: GameStatus | null; - folderName: string; - downloadPath: string | null; - progress: number; - bytesDownloaded: number; - playTimeInMilliseconds: number; - downloader: Downloader; - winePrefixPath: string | null; - executablePath: string | null; - launchOptions: string | null; - lastTimePlayed: Date | null; - uri: string | null; - fileSize: number; - objectID: string; - shop: GameShop; - // downloadQueue: DownloadQueue | null; - downloadQueue: any | null; - shouldSeed: boolean; - createdAt: Date; - updatedAt: Date; -} - export interface UnlockedAchievement { name: string; unlockTime: number; diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 3a9c0c14..8820820b 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -1,4 +1,10 @@ -import type { SteamAchievement, UnlockedAchievement } from "./game.types"; +import type { Downloader } from "@shared"; +import type { + GameShop, + GameStatus, + SteamAchievement, + UnlockedAchievement, +} from "./game.types"; export type SubscriptionStatus = "active" | "pending" | "cancelled"; @@ -24,6 +30,37 @@ export interface User { subscription: Subscription | null; } +export interface Game { + title: string; + iconUrl: string | null; + status: GameStatus | null; + playTimeInMilliseconds: number; + lastTimePlayed: Date | null; + objectId: string; + shop: GameShop; + remoteId: string | null; + isDeleted: boolean; + winePrefixPath?: string | null; + executablePath?: string | null; + launchOptions?: string | null; +} + +export interface Download { + shop: GameShop; + objectId: string; + uri: string; + folderName: string; + downloadPath: string; + progress: number; + downloader: Downloader; + bytesDownloaded: number; + playTimeInMilliseconds: number; + lastTimePlayed: Date | null; + fileSize: number; + shouldSeed: boolean; + timestamp: number; +} + export interface GameAchievement { achievements: SteamAchievement[]; unlockedAchievements: UnlockedAchievement[];