From 2c5fb8a0379c1515a3f2b4874ea5ec23c7bc8344 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 16:58:59 +0000 Subject: [PATCH 01/23] feat: adding initial leveldb configuration --- electron.vite.config.ts | 7 ++ package.json | 1 + src/main/constants.ts | 7 +- src/main/data-source.ts | 4 -- src/main/entity/index.ts | 2 - src/main/entity/user-auth.entity.ts | 45 ------------ src/main/entity/user-subscription.entity.ts | 42 ------------ src/main/events/auth/get-session-hash.ts | 13 +++- src/main/events/auth/sign-out.ts | 21 +++--- src/main/events/misc/open-checkout.ts | 14 ++-- src/main/events/user/get-user-friends.ts | 11 +-- src/main/repository.ts | 7 -- src/main/services/hydra-api.ts | 71 ++++++++++++------- src/main/services/index.ts | 1 + src/main/services/user/get-user-data.ts | 64 +++++++---------- src/renderer/src/components/modal/modal.tsx | 1 + src/types/index.ts | 11 +-- yarn.lock | 76 +++++++++++++++++++++ 18 files changed, 202 insertions(+), 196 deletions(-) delete mode 100644 src/main/entity/user-auth.entity.ts delete mode 100644 src/main/entity/user-subscription.entity.ts diff --git a/electron.vite.config.ts b/electron.vite.config.ts index cd08b6d4..2b7048c4 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -38,6 +38,13 @@ export default defineConfig(({ mode }) => { build: { sourcemap: true, }, + css: { + preprocessorOptions: { + scss: { + api: "modern", + }, + }, + }, resolve: { alias: { "@renderer": resolve("src/renderer/src"), diff --git a/package.json b/package.json index 2895f20c..4630ad41 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "jsdom": "^24.0.0", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", + "level": "^9.0.0", "lodash-es": "^4.17.21", "parse-torrent": "^11.0.17", "piscina": "^4.7.0", diff --git a/src/main/constants.ts b/src/main/constants.ts index b98b5935..f9d9c3e2 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -7,13 +7,18 @@ export const defaultDownloadsPath = app.getPath("downloads"); export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging"); +export const levelDatabasePath = path.join( + app.getPath("userData"), + `hydra-db${isStaging ? "-staging" : ""}` +); + export const databaseDirectory = path.join(app.getPath("appData"), "hydra"); export const databasePath = path.join( databaseDirectory, isStaging ? "hydra_test.db" : "hydra.db" ); -export const logsPath = path.join(app.getPath("appData"), "hydra", "logs"); +export const logsPath = path.join(app.getPath("userData"), "hydra", "logs"); export const seedsPath = app.isPackaged ? path.join(process.resourcesPath, "seeds") diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 51c8522e..05fdb04d 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -4,9 +4,7 @@ import { Game, GameShopCache, UserPreferences, - UserAuth, GameAchievement, - UserSubscription, } from "@main/entity"; import { databasePath } from "./constants"; @@ -15,9 +13,7 @@ export const dataSource = new DataSource({ type: "better-sqlite3", entities: [ Game, - UserAuth, UserPreferences, - UserSubscription, GameShopCache, DownloadQueue, GameAchievement, diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 1625ac8a..ab0ebff9 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,7 +1,5 @@ export * from "./game.entity"; -export * from "./user-auth.entity"; export * from "./user-preferences.entity"; -export * from "./user-subscription.entity"; export * from "./game-shop-cache.entity"; export * from "./game.entity"; export * from "./game-achievements.entity"; diff --git a/src/main/entity/user-auth.entity.ts b/src/main/entity/user-auth.entity.ts deleted file mode 100644 index f34e23ec..00000000 --- a/src/main/entity/user-auth.entity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToOne, -} from "typeorm"; -import { UserSubscription } from "./user-subscription.entity"; - -@Entity("user_auth") -export class UserAuth { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { default: "" }) - userId: string; - - @Column("text", { default: "" }) - displayName: string; - - @Column("text", { nullable: true }) - profileImageUrl: string | null; - - @Column("text", { nullable: true }) - backgroundImageUrl: string | null; - - @Column("text", { default: "" }) - accessToken: string; - - @Column("text", { default: "" }) - refreshToken: string; - - @Column("int", { default: 0 }) - tokenExpirationTimestamp: number; - - @OneToOne("UserSubscription", "user") - subscription: UserSubscription | null; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/entity/user-subscription.entity.ts b/src/main/entity/user-subscription.entity.ts deleted file mode 100644 index e74ada48..00000000 --- a/src/main/entity/user-subscription.entity.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { SubscriptionStatus } from "@types"; -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToOne, - JoinColumn, -} from "typeorm"; -import { UserAuth } from "./user-auth.entity"; - -@Entity("user_subscription") -export class UserSubscription { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { default: "" }) - subscriptionId: string; - - @OneToOne("UserAuth", "subscription") - @JoinColumn() - user: UserAuth; - - @Column("text", { default: "" }) - status: SubscriptionStatus; - - @Column("text", { default: "" }) - planId: string; - - @Column("text", { default: "" }) - planName: string; - - @Column("datetime", { nullable: true }) - expiresAt: Date | null; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/events/auth/get-session-hash.ts b/src/main/events/auth/get-session-hash.ts index c9dd39cc..293fb62e 100644 --- a/src/main/events/auth/get-session-hash.ts +++ b/src/main/events/auth/get-session-hash.ts @@ -1,13 +1,20 @@ import jwt from "jsonwebtoken"; -import { userAuthRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { db } from "@main/level"; +import type { Auth } from "@types"; +import { levelKeys } from "@main/level/sublevels/keys"; +import { Crypto } from "@main/services"; const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => { - const auth = await userAuthRepository.findOne({ where: { id: 1 } }); + const auth = await db.get(levelKeys.auth, { + valueEncoding: "json", + }); if (!auth) return null; - const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload; + const payload = jwt.decode( + Crypto.decrypt(auth.accessToken) + ) as jwt.JwtPayload; if (!payload) return null; diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 6b720015..1fb3a054 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -1,8 +1,10 @@ import { registerEvent } from "../register-event"; import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services"; import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity"; +import { DownloadQueue, Game } from "@main/entity"; import { PythonRPC } from "@main/services/python-rpc"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level/sublevels/keys"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const databaseOperations = dataSource @@ -11,13 +13,16 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => { await transactionalEntityManager.getRepository(Game).delete({}); - await transactionalEntityManager - .getRepository(UserAuth) - .delete({ id: 1 }); - - await transactionalEntityManager - .getRepository(UserSubscription) - .delete({ id: 1 }); + await db.batch([ + { + type: "del", + key: levelKeys.auth, + }, + { + type: "del", + key: levelKeys.user, + }, + ]); }) .then(() => { /* Removes all games being played */ diff --git a/src/main/events/misc/open-checkout.ts b/src/main/events/misc/open-checkout.ts index ba48f03b..76e7fe09 100644 --- a/src/main/events/misc/open-checkout.ts +++ b/src/main/events/misc/open-checkout.ts @@ -1,17 +1,21 @@ import { shell } from "electron"; import { registerEvent } from "../register-event"; -import { userAuthRepository } from "@main/repository"; -import { HydraApi } from "@main/services"; +import { Crypto, HydraApi } from "@main/services"; +import { db } from "@main/level"; +import type { Auth } from "@types"; +import { levelKeys } from "@main/level/sublevels/keys"; const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { - const userAuth = await userAuthRepository.findOne({ where: { id: 1 } }); + const auth = await db.get(levelKeys.auth, { + valueEncoding: "json", + }); - if (!userAuth) { + if (!auth) { return; } const paymentToken = await HydraApi.post("/auth/payment", { - refreshToken: userAuth.refreshToken, + refreshToken: Crypto.decrypt(auth.refreshToken), }).then((response) => response.accessToken); const params = new URLSearchParams({ diff --git a/src/main/events/user/get-user-friends.ts b/src/main/events/user/get-user-friends.ts index 9a6f156c..7c308506 100644 --- a/src/main/events/user/get-user-friends.ts +++ b/src/main/events/user/get-user-friends.ts @@ -1,16 +1,19 @@ -import { userAuthRepository } from "@main/repository"; +import { db } from "@main/level"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import type { UserFriends } from "@types"; +import type { User, UserFriends } from "@types"; +import { levelKeys } from "@main/level/sublevels/keys"; export const getUserFriends = async ( userId: string, take: number, skip: number ): Promise => { - const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } }); + const user = await db.get(levelKeys.user, { + valueEncoding: "json", + }); - if (loggedUser?.userId === userId) { + if (user?.id === userId) { return HydraApi.get(`/profile/friends`, { take, skip }); } diff --git a/src/main/repository.ts b/src/main/repository.ts index e0c4204e..ef120f7e 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -4,9 +4,7 @@ import { Game, GameShopCache, UserPreferences, - UserAuth, GameAchievement, - UserSubscription, } from "@main/entity"; export const gameRepository = dataSource.getRepository(Game); @@ -18,10 +16,5 @@ export const gameShopCacheRepository = dataSource.getRepository(GameShopCache); export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); -export const userAuthRepository = dataSource.getRepository(UserAuth); - -export const userSubscriptionRepository = - dataSource.getRepository(UserSubscription); - export const gameAchievementRepository = dataSource.getRepository(GameAchievement); diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 63dd9b16..6cf9a8af 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -1,7 +1,3 @@ -import { - userAuthRepository, - userSubscriptionRepository, -} from "@main/repository"; import axios, { AxiosError, AxiosInstance } from "axios"; import { WindowManager } from "./window-manager"; import url from "url"; @@ -13,6 +9,10 @@ import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; import { getUserData } from "./user/get-user-data"; import { isFuture, isToday } from "date-fns"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level/sublevels/keys"; +import type { Auth, User } from "@types"; +import { Crypto } from "./crypto"; interface HydraApiOptions { needsAuth?: boolean; @@ -77,14 +77,14 @@ export class HydraApi { tokenExpirationTimestamp ); - await userAuthRepository.upsert( + db.put( + levelKeys.auth, { - id: 1, - accessToken, + accessToken: Crypto.encrypt(accessToken), + refreshToken: Crypto.encrypt(refreshToken), tokenExpirationTimestamp, - refreshToken, }, - ["id"] + { valueEncoding: "json" } ); await getUserData().then((userDetails) => { @@ -186,17 +186,23 @@ export class HydraApi { ); } - const userAuth = await userAuthRepository.findOne({ - where: { id: 1 }, - relations: { subscription: true }, + const result = await db.getMany([levelKeys.auth, levelKeys.user], { + valueEncoding: "json", }); + const userAuth = result.at(0) as Auth | undefined; + const user = result.at(1) as User | undefined; + this.userAuth = { - authToken: userAuth?.accessToken ?? "", - refreshToken: userAuth?.refreshToken ?? "", + authToken: userAuth?.accessToken + ? Crypto.decrypt(userAuth.accessToken) + : "", + refreshToken: userAuth?.refreshToken + ? Crypto.decrypt(userAuth.refreshToken) + : "", expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0, - subscription: userAuth?.subscription - ? { expiresAt: userAuth.subscription?.expiresAt } + subscription: user?.subscription + ? { expiresAt: user.subscription?.expiresAt } : null, }; @@ -239,14 +245,19 @@ export class HydraApi { this.userAuth.expirationTimestamp ); - userAuthRepository.upsert( - { - id: 1, - accessToken, - tokenExpirationTimestamp, - }, - ["id"] - ); + await db + .get(levelKeys.auth, { valueEncoding: "json" }) + .then((auth) => { + return db.put( + levelKeys.auth, + { + ...auth, + accessToken: Crypto.encrypt(accessToken), + tokenExpirationTimestamp, + }, + { valueEncoding: "json" } + ); + }); } catch (err) { this.handleUnauthorizedError(err); } @@ -276,8 +287,16 @@ export class HydraApi { subscription: null, }; - userAuthRepository.delete({ id: 1 }); - userSubscriptionRepository.delete({ id: 1 }); + db.batch([ + { + type: "del", + key: levelKeys.auth, + }, + { + type: "del", + key: levelKeys.user, + }, + ]); this.sendSignOutEvent(); } diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 5aaf5322..d2034f15 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -1,3 +1,4 @@ +export * from "./crypto"; export * from "./logger"; export * from "./steam"; export * from "./steam-250"; diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index 7e924454..e6cf1c71 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -1,43 +1,30 @@ -import type { ProfileVisibility, UserDetails } from "@types"; +import { User, type ProfileVisibility, type UserDetails } from "@types"; import { HydraApi } from "../hydra-api"; -import { - userAuthRepository, - userSubscriptionRepository, -} from "@main/repository"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level/sublevels/keys"; -export const getUserData = () => { +export const getUserData = async () => { return HydraApi.get(`/profile/me`) .then(async (me) => { - userAuthRepository.upsert( - { - id: 1, - displayName: me.displayName, - profileImageUrl: me.profileImageUrl, - backgroundImageUrl: me.backgroundImageUrl, - userId: me.id, - }, - ["id"] + db.get(levelKeys.user, { valueEncoding: "json" }).then( + (user) => { + return db.put( + levelKeys.user, + { + ...user, + id: me.id, + displayName: me.displayName, + profileImageUrl: me.profileImageUrl, + backgroundImageUrl: me.backgroundImageUrl, + subscription: me.subscription, + }, + { valueEncoding: "json" } + ); + } ); - if (me.subscription) { - await userSubscriptionRepository.upsert( - { - id: 1, - subscriptionId: me.subscription?.id || "", - status: me.subscription?.status || "", - planId: me.subscription?.plan.id || "", - planName: me.subscription?.plan.name || "", - expiresAt: me.subscription?.expiresAt || null, - user: { id: 1 }, - }, - ["id"] - ); - } else { - await userSubscriptionRepository.delete({ id: 1 }); - } - return me; }) .catch(async (err) => { @@ -46,15 +33,14 @@ export const getUserData = () => { return null; } logger.error("Failed to get logged user"); - const loggedUser = await userAuthRepository.findOne({ - where: { id: 1 }, - relations: { subscription: true }, + + const loggedUser = await db.get(levelKeys.user, { + valueEncoding: "json", }); if (loggedUser) { return { ...loggedUser, - id: loggedUser.userId, username: "", bio: "", email: null, @@ -64,11 +50,11 @@ export const getUserData = () => { }, subscription: loggedUser.subscription ? { - id: loggedUser.subscription.subscriptionId, + id: loggedUser.subscription.id, status: loggedUser.subscription.status, plan: { - id: loggedUser.subscription.planId, - name: loggedUser.subscription.planName, + id: loggedUser.subscription.plan.id, + name: loggedUser.subscription.plan.name, }, expiresAt: loggedUser.subscription.expiresAt, } diff --git a/src/renderer/src/components/modal/modal.tsx b/src/renderer/src/components/modal/modal.tsx index d8d0554d..af09ef38 100644 --- a/src/renderer/src/components/modal/modal.tsx +++ b/src/renderer/src/components/modal/modal.tsx @@ -52,6 +52,7 @@ export function Modal({ ) ) return false; + const openModals = document.querySelectorAll("[role=dialog]"); return ( diff --git a/src/types/index.ts b/src/types/index.ts index 345893a5..bae42702 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -248,16 +248,6 @@ export interface UserProfileCurrentGame extends Omit { export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS"; -export type SubscriptionStatus = "active" | "pending" | "cancelled"; - -export interface Subscription { - id: string; - status: SubscriptionStatus; - plan: { id: string; name: string }; - expiresAt: string | null; - paymentMethod: "pix" | "paypal"; -} - export interface UserDetails { id: string; username: string; @@ -421,3 +411,4 @@ export * from "./real-debrid.types"; export * from "./ludusavi.types"; export * from "./how-long-to-beat.types"; export * from "./torbox.types"; +export * from "./level.types"; diff --git a/yarn.lock b/yarn.lock index 69ee75d8..4e58584e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3699,6 +3699,18 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abstract-level@^2.0.0, abstract-level@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-2.0.2.tgz#8d965e731afb42a72f163874410c1687fb2e4bdb" + integrity sha512-pPJixmXk/kTKLB2sSue7o4Uj6TlLD2XfaP2gWZomHVCC6cuUGX/VslQqKG1yZHfXwBb/3lS6oSTMPGzh1P1iig== + dependencies: + buffer "^6.0.3" + is-buffer "^2.0.5" + level-supports "^6.0.0" + level-transcoder "^1.0.1" + maybe-combine-errors "^1.0.0" + module-error "^1.0.1" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -4169,6 +4181,13 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" +browser-level@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browser-level/-/browser-level-2.0.0.tgz#cc63eb1322e67c44489d7fbdda5c30a2db7b59da" + integrity sha512-RuYSCHG/jwFCrK+KWA3dLSUNLKHEgIYhO5ORPjJMjCt7T3e+RzpIDmYKWRHxq2pfKGXjlRuEff7y7RESAAgzew== + dependencies: + abstract-level "^2.0.1" + browserslist@^4.22.2, browserslist@^4.23.1: version "4.24.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" @@ -4421,6 +4440,16 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +classic-level@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/classic-level/-/classic-level-2.0.0.tgz#6fd9ca686bbcd645e35caaf403c3f3a56495d11b" + integrity sha512-ftiMvKgCQK+OppXcvMieDoYlYLYWhScK6yZRFBrrlHQRbm4k6Gr+yDgu/wt3V0k1/jtNbuiXAsRmuAFcD0Tx5Q== + dependencies: + abstract-level "^2.0.0" + module-error "^1.0.1" + napi-macros "^2.2.2" + node-gyp-build "^4.3.0" + classnames@^2.2.1, classnames@^2.2.6, classnames@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" @@ -6459,6 +6488,11 @@ is-boolean-object@^1.2.0: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -6958,6 +6992,28 @@ lazy-val@^1.0.5: resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d" integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q== +level-supports@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-6.2.0.tgz#e78b228973a24acdc5199c5f51e244e70f26c611" + integrity sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w== + +level-transcoder@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/level-transcoder/-/level-transcoder-1.0.1.tgz#f8cef5990c4f1283d4c86d949e73631b0bc8ba9c" + integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w== + dependencies: + buffer "^6.0.3" + module-error "^1.0.1" + +level@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/level/-/level-9.0.0.tgz#880aa9d341a5411e36bed77f4fa233f425b492a8" + integrity sha512-n+mVuf63mUEkd8NUx7gwxY+QF5vtkibv6fXTGUgtHWLPDaA5/XZjLcI/Q1nQ8k6OttHT6Ezt+7nSEXsRUfHtOQ== + dependencies: + abstract-level "^2.0.1" + browser-level "^2.0.0" + classic-level "^2.0.0" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -7198,6 +7254,11 @@ math-intrinsics@^1.0.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.0.0.tgz#4e04bf87c85aa51e90d078dac2252b4eb5260817" integrity sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA== +maybe-combine-errors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz#e9592832e61fc47643a92cff3c1f33e27211e5be" + integrity sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A== + media-query-parser@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29" @@ -7414,6 +7475,11 @@ modern-ahocorasick@^1.0.0: resolved "https://registry.yarnpkg.com/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz#dec373444f51b5458ac05216a8ec376e126dd283" integrity sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA== +module-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" + integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -7448,6 +7514,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-macros@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.2.2.tgz#817fef20c3e0e40a963fbf7b37d1600bd0201044" + integrity sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -7506,6 +7577,11 @@ node-fetch@^3.3.0: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-gyp-build@^4.3.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + node-gyp@^9.0.0: version "9.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" From 08bcf096411e802919acb607f7eebe56d492a90b Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:00:27 +0000 Subject: [PATCH 02/23] feat: adding initial leveldb configuration --- src/main/services/hosters/datanodes.ts | 3 ++- src/types/index.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/services/hosters/datanodes.ts b/src/main/services/hosters/datanodes.ts index d77e7d51..ae144418 100644 --- a/src/main/services/hosters/datanodes.ts +++ b/src/main/services/hosters/datanodes.ts @@ -33,7 +33,8 @@ export class DatanodesApi { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", }, - maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400, + maxRedirects: 0, + validateStatus: (status: number) => status === 302 || status < 400, } ); diff --git a/src/types/index.ts b/src/types/index.ts index bae42702..dd631ccb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ import type { Cracker, DownloadSourceStatus, Downloader } from "@shared"; import type { SteamAppDetails } from "./steam.types"; +import type { Subscription } from "./level.types"; export type GameStatus = | "active" From c59b039eb4cb5a0eae558cb77cc8cedeb57ed441 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:02:40 +0000 Subject: [PATCH 03/23] fix: removing unused navigate --- src/renderer/src/pages/achievements/achievements.tsx | 2 +- .../src/pages/profile/profile-content/profile-content.tsx | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx index 605300ef..f467cf89 100644 --- a/src/renderer/src/pages/achievements/achievements.tsx +++ b/src/renderer/src/pages/achievements/achievements.tsx @@ -44,7 +44,7 @@ export default function Achievements() { .getComparedUnlockedAchievements(objectId, shop as GameShop, userId) .then(setComparedAchievements); } - }, [objectId, shop, userId]); + }, [objectId, shop, userDetails?.id, userId]); const otherUserId = userDetails?.id === userId ? null : userId; diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 951eb41b..71788a32 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -7,7 +7,6 @@ import { SPACING_UNIT } from "@renderer/theme.css"; import * as styles from "./profile-content.css"; import { TelescopeIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; import { FriendsBox } from "./friends-box"; @@ -66,8 +65,6 @@ export function ProfileContent() { const { numberFormatter } = useFormat(); - const navigate = useNavigate(); - const usersAreFriends = useMemo(() => { return userProfile?.relation?.status === "ACCEPTED"; }, [userProfile]); @@ -148,7 +145,6 @@ export function ProfileContent() { userStats, numberFormatter, t, - navigate, statsIndex, ]); From 8b47082047b6e89adb59a39cafadcc45ea525078 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:08:22 +0000 Subject: [PATCH 04/23] fix: removing unused navigate --- src/main/level/index.ts | 3 +++ src/main/level/level.ts | 4 ++++ src/main/level/sublevels/games.ts | 7 +++++++ src/main/level/sublevels/index.ts | 1 + src/main/level/sublevels/keys.ts | 8 ++++++++ src/main/services/crypto.ts | 28 ++++++++++++++++++++++++++++ src/types/level.types.ts | 23 +++++++++++++++++++++++ 7 files changed, 74 insertions(+) create mode 100644 src/main/level/index.ts create mode 100644 src/main/level/level.ts create mode 100644 src/main/level/sublevels/games.ts create mode 100644 src/main/level/sublevels/index.ts create mode 100644 src/main/level/sublevels/keys.ts create mode 100644 src/main/services/crypto.ts create mode 100644 src/types/level.types.ts diff --git a/src/main/level/index.ts b/src/main/level/index.ts new file mode 100644 index 00000000..90a34be3 --- /dev/null +++ b/src/main/level/index.ts @@ -0,0 +1,3 @@ +export { db } from "./level"; + +export * from "./sublevels"; diff --git a/src/main/level/level.ts b/src/main/level/level.ts new file mode 100644 index 00000000..382c61a5 --- /dev/null +++ b/src/main/level/level.ts @@ -0,0 +1,4 @@ +import { levelDatabasePath } from "@main/constants"; +import { Level } from "level"; + +export const db = new Level(levelDatabasePath, { valueEncoding: "json" }); diff --git a/src/main/level/sublevels/games.ts b/src/main/level/sublevels/games.ts new file mode 100644 index 00000000..bc0cad30 --- /dev/null +++ b/src/main/level/sublevels/games.ts @@ -0,0 +1,7 @@ +import { Game } from "@types"; +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const gamesSublevel = db.sublevel(levelKeys.games, { + valueEncoding: "json", +}); diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts new file mode 100644 index 00000000..9d316e1a --- /dev/null +++ b/src/main/level/sublevels/index.ts @@ -0,0 +1 @@ +export * from "./games"; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts new file mode 100644 index 00000000..6bb54c4a --- /dev/null +++ b/src/main/level/sublevels/keys.ts @@ -0,0 +1,8 @@ +import type { GameShop } from "@types"; + +export const levelKeys = { + games: "games", + game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`, + user: "user", + auth: "auth", +}; diff --git a/src/main/services/crypto.ts b/src/main/services/crypto.ts new file mode 100644 index 00000000..63a50668 --- /dev/null +++ b/src/main/services/crypto.ts @@ -0,0 +1,28 @@ +import { safeStorage } from "electron"; +import { logger } from "./logger"; + +export class Crypto { + public static encrypt(str: string) { + if (safeStorage.isEncryptionAvailable()) { + return safeStorage.encryptString(str).toString("base64"); + } else { + logger.warn( + "Encrypt method returned raw string because encryption is not available" + ); + + return str; + } + } + + public static decrypt(b64: string) { + if (safeStorage.isEncryptionAvailable()) { + return safeStorage.decryptString(Buffer.from(b64, "base64")); + } else { + logger.warn( + "Decrypt method returned raw string because encryption is not available" + ); + + return b64; + } + } +} diff --git a/src/types/level.types.ts b/src/types/level.types.ts new file mode 100644 index 00000000..490ab060 --- /dev/null +++ b/src/types/level.types.ts @@ -0,0 +1,23 @@ +export type SubscriptionStatus = "active" | "pending" | "cancelled"; + +export interface Subscription { + id: string; + status: SubscriptionStatus; + plan: { id: string; name: string }; + expiresAt: string | null; + paymentMethod: "pix" | "paypal"; +} + +export interface Auth { + accessToken: string; + refreshToken: string; + tokenExpirationTimestamp: number; +} + +export interface User { + id: string; + displayName: string; + profileImageUrl: string | null; + backgroundImageUrl: string | null; + subscription: Subscription | null; +} From 2c881a61002390a48438f6118b2ce4197c852072 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:15:57 +0000 Subject: [PATCH 05/23] fix: fixing duplicate export --- src/main/entity/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index ab0ebff9..06b543d4 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,6 +1,5 @@ export * from "./game.entity"; export * from "./user-preferences.entity"; export * from "./game-shop-cache.entity"; -export * from "./game.entity"; export * from "./game-achievements.entity"; export * from "./download-queue.entity"; From a23106b0b1d62b257b7aef1a9dd365d7ef92a967 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Thu, 16 Jan 2025 02:30:09 +0000 Subject: [PATCH 06/23] feat: migrating achievements to level --- src/main/data-source.ts | 16 +- src/main/entity/game-achievements.entity.ts | 19 -- src/main/entity/game-shop-cache.entity.ts | 35 ---- src/main/entity/index.ts | 2 - src/main/events/auth/get-session-hash.ts | 2 +- src/main/events/auth/sign-out.ts | 2 +- .../events/catalogue/get-game-shop-details.ts | 34 ++-- .../events/library/reset-game-achievements.ts | 24 ++- src/main/events/misc/open-checkout.ts | 2 +- .../events/user/get-unlocked-achievements.ts | 20 +-- src/main/events/user/get-user-friends.ts | 2 +- src/main/level/sublevels/game-achievements.ts | 11 ++ src/main/level/sublevels/game-shop-cache.ts | 11 ++ src/main/level/sublevels/games.ts | 3 +- src/main/level/sublevels/index.ts | 4 + src/main/level/sublevels/keys.ts | 4 + src/main/repository.ts | 13 +- .../achievements/get-game-achievement-data.ts | 48 ++--- .../achievements/merge-achievements.ts | 66 +++---- src/main/services/hydra-api.ts | 2 +- src/main/services/user/get-user-data.ts | 2 +- .../src/components/sidebar/sidebar.tsx | 11 +- src/renderer/src/declaration.d.ts | 6 +- src/renderer/src/features/library-slice.ts | 4 +- .../pages/achievements/achievement-list.tsx | 9 +- .../src/pages/downloads/download-group.tsx | 10 +- .../src/pages/downloads/downloads.tsx | 6 +- .../pages/game-details/sidebar/sidebar.tsx | 4 +- src/types/download.types.ts | 167 ++++++++++++++++++ src/types/game.types.ts | 59 +++++++ src/types/index.ts | 115 +----------- src/types/level.types.ts | 7 + src/types/real-debrid.types.ts | 66 ------- src/types/torbox.types.ts | 77 -------- 34 files changed, 388 insertions(+), 475 deletions(-) delete mode 100644 src/main/entity/game-achievements.entity.ts delete mode 100644 src/main/entity/game-shop-cache.entity.ts create mode 100644 src/main/level/sublevels/game-achievements.ts create mode 100644 src/main/level/sublevels/game-shop-cache.ts create mode 100644 src/types/download.types.ts create mode 100644 src/types/game.types.ts delete mode 100644 src/types/real-debrid.types.ts delete mode 100644 src/types/torbox.types.ts diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 05fdb04d..7414a758 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -1,23 +1,11 @@ import { DataSource } from "typeorm"; -import { - DownloadQueue, - Game, - GameShopCache, - UserPreferences, - GameAchievement, -} from "@main/entity"; +import { DownloadQueue, Game, UserPreferences } from "@main/entity"; import { databasePath } from "./constants"; export const dataSource = new DataSource({ type: "better-sqlite3", - entities: [ - Game, - UserPreferences, - GameShopCache, - DownloadQueue, - GameAchievement, - ], + entities: [Game, UserPreferences, DownloadQueue], synchronize: false, database: databasePath, }); diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts deleted file mode 100644 index 0cb15f6e..00000000 --- a/src/main/entity/game-achievements.entity.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; - -@Entity("game_achievement") -export class GameAchievement { - @PrimaryGeneratedColumn() - id: number; - - @Column("text") - objectId: string; - - @Column("text") - shop: string; - - @Column("text", { nullable: true }) - unlockedAchievements: string | null; - - @Column("text", { nullable: true }) - achievements: string | null; -} diff --git a/src/main/entity/game-shop-cache.entity.ts b/src/main/entity/game-shop-cache.entity.ts deleted file mode 100644 index 3382da1c..00000000 --- a/src/main/entity/game-shop-cache.entity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - Entity, - PrimaryColumn, - Column, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; -import type { GameShop } from "@types"; - -@Entity("game_shop_cache") -export class GameShopCache { - @PrimaryColumn("text", { unique: true }) - objectID: string; - - @Column("text") - shop: GameShop; - - @Column("text", { nullable: true }) - serializedData: string; - - /** - * @deprecated Use IndexedDB's `howLongToBeatEntries` instead - */ - @Column("text", { nullable: true }) - howLongToBeatSerializedData: string; - - @Column("text", { nullable: true }) - language: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 06b543d4..f35f643d 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,5 +1,3 @@ export * from "./game.entity"; export * from "./user-preferences.entity"; -export * from "./game-shop-cache.entity"; -export * from "./game-achievements.entity"; export * from "./download-queue.entity"; diff --git a/src/main/events/auth/get-session-hash.ts b/src/main/events/auth/get-session-hash.ts index 293fb62e..5848cbd7 100644 --- a/src/main/events/auth/get-session-hash.ts +++ b/src/main/events/auth/get-session-hash.ts @@ -3,7 +3,7 @@ import jwt from "jsonwebtoken"; import { registerEvent } from "../register-event"; import { db } from "@main/level"; import type { Auth } from "@types"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level"; import { Crypto } from "@main/services"; const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => { diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 1fb3a054..866d1ec0 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -4,7 +4,7 @@ import { dataSource } from "@main/data-source"; import { DownloadQueue, Game } from "@main/entity"; import { PythonRPC } from "@main/services/python-rpc"; import { db } from "@main/level"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const databaseOperations = dataSource diff --git a/src/main/events/catalogue/get-game-shop-details.ts b/src/main/events/catalogue/get-game-shop-details.ts index 08366abc..39f8425b 100644 --- a/src/main/events/catalogue/get-game-shop-details.ts +++ b/src/main/events/catalogue/get-game-shop-details.ts @@ -1,10 +1,10 @@ -import { gameShopCacheRepository } from "@main/repository"; -import { getSteamAppDetails } from "@main/services"; +import { getSteamAppDetails, logger } from "@main/services"; -import type { ShopDetails, GameShop, SteamAppDetails } from "@types"; +import type { ShopDetails, GameShop } from "@types"; import { registerEvent } from "../register-event"; import { steamGamesWorker } from "@main/workers"; +import { gamesShopCacheSublevel, levelKeys } from "@main/level"; const getLocalizedSteamAppDetails = async ( objectId: string, @@ -39,35 +39,27 @@ const getGameShopDetails = async ( language: string ): Promise => { if (shop === "steam") { - const cachedData = await gameShopCacheRepository.findOne({ - where: { objectID: objectId, language }, - }); + const cachedData = await gamesShopCacheSublevel.get( + levelKeys.gameShopCacheItem(shop, objectId, language) + ); const appDetails = getLocalizedSteamAppDetails(objectId, language).then( (result) => { if (result) { - gameShopCacheRepository.upsert( - { - objectID: objectId, - shop: "steam", - language, - serializedData: JSON.stringify(result), - }, - ["objectID"] - ); + gamesShopCacheSublevel + .put(levelKeys.gameShopCacheItem(shop, objectId, language), result) + .catch((err) => { + logger.error("Could not cache game details", err); + }); } return result; } ); - const cachedGame = cachedData?.serializedData - ? (JSON.parse(cachedData?.serializedData) as SteamAppDetails) - : null; - - if (cachedGame) { + if (cachedData) { return { - ...cachedGame, + ...cachedData, objectId, } as ShopDetails; } diff --git a/src/main/events/library/reset-game-achievements.ts b/src/main/events/library/reset-game-achievements.ts index 8d52a3a6..0ea26adf 100644 --- a/src/main/events/library/reset-game-achievements.ts +++ b/src/main/events/library/reset-game-achievements.ts @@ -1,9 +1,10 @@ -import { gameAchievementRepository, gameRepository } from "@main/repository"; +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"; const resetGameAchievements = async ( _event: Electron.IpcMainInvokeEvent, @@ -23,12 +24,21 @@ const resetGameAchievements = async ( } } - await gameAchievementRepository.update( - { objectId: game.objectID }, - { - unlockedAchievements: null, - } - ); + 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 HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then( () => diff --git a/src/main/events/misc/open-checkout.ts b/src/main/events/misc/open-checkout.ts index 76e7fe09..95d76d5b 100644 --- a/src/main/events/misc/open-checkout.ts +++ b/src/main/events/misc/open-checkout.ts @@ -3,7 +3,7 @@ import { registerEvent } from "../register-event"; import { Crypto, HydraApi } from "@main/services"; import { db } from "@main/level"; import type { Auth } from "@types"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level"; const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { const auth = await db.get(levelKeys.auth, { diff --git a/src/main/events/user/get-unlocked-achievements.ts b/src/main/events/user/get-unlocked-achievements.ts index ffa25399..78820a94 100644 --- a/src/main/events/user/get-unlocked-achievements.ts +++ b/src/main/events/user/get-unlocked-achievements.ts @@ -1,19 +1,17 @@ -import type { GameShop, UnlockedAchievement, UserAchievement } from "@types"; +import type { GameShop, UserAchievement } from "@types"; import { registerEvent } from "../register-event"; -import { - gameAchievementRepository, - userPreferencesRepository, -} from "@main/repository"; +import { userPreferencesRepository } from "@main/repository"; import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; +import { gameAchievementsSublevel, levelKeys } from "@main/level"; export const getUnlockedAchievements = async ( objectId: string, shop: GameShop, useCachedData: boolean ): Promise => { - const cachedAchievements = await gameAchievementRepository.findOne({ - where: { objectId, shop }, - }); + const cachedAchievements = await gameAchievementsSublevel.get( + levelKeys.game(shop, objectId) + ); const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 }, @@ -25,12 +23,10 @@ export const getUnlockedAchievements = async ( const achievementsData = await getGameAchievementData( objectId, shop, - useCachedData ? cachedAchievements : null + useCachedData ); - const unlockedAchievements = JSON.parse( - cachedAchievements?.unlockedAchievements || "[]" - ) as UnlockedAchievement[]; + const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? []; return achievementsData .map((achievementData) => { diff --git a/src/main/events/user/get-user-friends.ts b/src/main/events/user/get-user-friends.ts index 7c308506..aefc7052 100644 --- a/src/main/events/user/get-user-friends.ts +++ b/src/main/events/user/get-user-friends.ts @@ -2,7 +2,7 @@ import { db } from "@main/level"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import type { User, UserFriends } from "@types"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level/sublevels"; export const getUserFriends = async ( userId: string, diff --git a/src/main/level/sublevels/game-achievements.ts b/src/main/level/sublevels/game-achievements.ts new file mode 100644 index 00000000..4b1fa0c8 --- /dev/null +++ b/src/main/level/sublevels/game-achievements.ts @@ -0,0 +1,11 @@ +import type { GameAchievement } from "@types"; + +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const gameAchievementsSublevel = db.sublevel( + levelKeys.gameAchievements, + { + valueEncoding: "json", + } +); diff --git a/src/main/level/sublevels/game-shop-cache.ts b/src/main/level/sublevels/game-shop-cache.ts new file mode 100644 index 00000000..8187e5c0 --- /dev/null +++ b/src/main/level/sublevels/game-shop-cache.ts @@ -0,0 +1,11 @@ +import type { ShopDetails } from "@types"; + +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const gamesShopCacheSublevel = db.sublevel( + levelKeys.gameShopCache, + { + valueEncoding: "json", + } +); diff --git a/src/main/level/sublevels/games.ts b/src/main/level/sublevels/games.ts index bc0cad30..ce7492f1 100644 --- a/src/main/level/sublevels/games.ts +++ b/src/main/level/sublevels/games.ts @@ -1,4 +1,5 @@ -import { Game } from "@types"; +import type { Game } from "@types"; + import { db } from "../level"; import { levelKeys } from "./keys"; diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts index 9d316e1a..ce61c4e2 100644 --- a/src/main/level/sublevels/index.ts +++ b/src/main/level/sublevels/index.ts @@ -1 +1,5 @@ export * from "./games"; +export * from "./game-shop-cache"; +export * from "./game-achievements"; + +export * from "./keys"; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts index 6bb54c4a..f2bb6f3c 100644 --- a/src/main/level/sublevels/keys.ts +++ b/src/main/level/sublevels/keys.ts @@ -5,4 +5,8 @@ export const levelKeys = { game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`, user: "user", auth: "auth", + gameShopCache: "gameShopCache", + gameShopCacheItem: (shop: GameShop, objectId: string, language: string) => + `${shop}:${objectId}:${language}`, + gameAchievements: "gameAchievements", }; diff --git a/src/main/repository.ts b/src/main/repository.ts index ef120f7e..5bbfaf9f 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -1,20 +1,9 @@ import { dataSource } from "./data-source"; -import { - DownloadQueue, - Game, - GameShopCache, - UserPreferences, - GameAchievement, -} from "@main/entity"; +import { DownloadQueue, Game, UserPreferences } from "@main/entity"; export const gameRepository = dataSource.getRepository(Game); export const userPreferencesRepository = dataSource.getRepository(UserPreferences); -export const gameShopCacheRepository = dataSource.getRepository(GameShopCache); - export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); - -export const gameAchievementRepository = - dataSource.getRepository(GameAchievement); diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index daac7e11..2dc643c1 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -1,40 +1,36 @@ -import { - gameAchievementRepository, - userPreferencesRepository, -} from "@main/repository"; +import { userPreferencesRepository } from "@main/repository"; import { HydraApi } from "../hydra-api"; -import type { AchievementData, GameShop } from "@types"; +import type { GameShop, SteamAchievement } from "@types"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; -import { GameAchievement } from "@main/entity"; +import { gameAchievementsSublevel, levelKeys } from "@main/level"; export const getGameAchievementData = async ( objectId: string, shop: GameShop, - cachedAchievements: GameAchievement | null + useCachedData: boolean ) => { - if (cachedAchievements && cachedAchievements.achievements) { - return JSON.parse(cachedAchievements.achievements) as AchievementData[]; - } + const cachedAchievements = await gameAchievementsSublevel.get( + levelKeys.game(shop, objectId) + ); + + if (cachedAchievements && useCachedData) + return cachedAchievements.achievements; const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 }, }); - return HydraApi.get("/games/achievements", { + return HydraApi.get("/games/achievements", { shop, objectId, language: userPreferences?.language || "en", }) - .then((achievements) => { - gameAchievementRepository.upsert( - { - objectId, - shop, - achievements: JSON.stringify(achievements), - }, - ["objectId", "shop"] - ); + .then(async (achievements) => { + await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), { + unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [], + achievements, + }); return achievements; }) @@ -42,15 +38,9 @@ export const getGameAchievementData = async ( if (err instanceof UserNotLoggedInError) { throw err; } + logger.error("Failed to get game achievements", err); - return gameAchievementRepository - .findOne({ - where: { objectId, shop }, - }) - .then((gameAchievements) => { - return JSON.parse( - gameAchievements?.achievements || "[]" - ) as AchievementData[]; - }); + + return []; }); }; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index dd8c877d..ac2f69d1 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -1,8 +1,5 @@ -import { - gameAchievementRepository, - userPreferencesRepository, -} from "@main/repository"; -import type { AchievementData, GameShop, UnlockedAchievement } from "@types"; +import { userPreferencesRepository } from "@main/repository"; +import type { GameShop, UnlockedAchievement } from "@types"; import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; @@ -10,33 +7,36 @@ import { Game } from "@main/entity"; import { publishNewAchievementNotification } from "../notifications"; import { SubscriptionRequiredError } from "@shared"; import { achievementsLogger } from "../logger"; +import { gameAchievementsSublevel, levelKeys } from "@main/level"; const saveAchievementsOnLocal = async ( objectId: string, shop: GameShop, - achievements: UnlockedAchievement[], + unlockedAchievements: UnlockedAchievement[], sendUpdateEvent: boolean ) => { - return gameAchievementRepository - .upsert( - { - objectId, - shop, - unlockedAchievements: JSON.stringify(achievements), - }, - ["objectId", "shop"] - ) - .then(() => { - if (!sendUpdateEvent) return; + const levelKey = levelKeys.game(shop, objectId); - return getUnlockedAchievements(objectId, shop, true) - .then((achievements) => { - WindowManager.mainWindow?.webContents.send( - `on-update-achievements-${objectId}-${shop}`, - achievements - ); - }) - .catch(() => {}); + return gameAchievementsSublevel + .get(levelKey) + .then(async (gameAchievement) => { + if (gameAchievement) { + await gameAchievementsSublevel.put(levelKey, { + ...gameAchievement, + unlockedAchievements: unlockedAchievements, + }); + + if (!sendUpdateEvent) return; + + return getUnlockedAchievements(objectId, shop, true) + .then((achievements) => { + WindowManager.mainWindow?.webContents.send( + `on-update-achievements-${objectId}-${shop}`, + achievements + ); + }) + .catch(() => {}); + } }); }; @@ -46,22 +46,12 @@ export const mergeAchievements = async ( publishNotification: boolean ) => { const [localGameAchievement, userPreferences] = await Promise.all([ - gameAchievementRepository.findOne({ - where: { - objectId: game.objectID, - shop: game.shop, - }, - }), + gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectID)), userPreferencesRepository.findOne({ where: { id: 1 } }), ]); - const achievementsData = JSON.parse( - localGameAchievement?.achievements || "[]" - ) as AchievementData[]; - - const unlockedAchievements = JSON.parse( - localGameAchievement?.unlockedAchievements || "[]" - ).filter((achievement) => achievement.name) as UnlockedAchievement[]; + const achievementsData = localGameAchievement?.achievements ?? []; + const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? []; const newAchievementsMap = new Map( achievements.reverse().map((achievement) => { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 6cf9a8af..5f7a5034 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -10,7 +10,7 @@ import { appVersion } from "@main/constants"; import { getUserData } from "./user/get-user-data"; import { isFuture, isToday } from "date-fns"; import { db } from "@main/level"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level/sublevels"; import type { Auth, User } from "@types"; import { Crypto } from "./crypto"; diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index e6cf1c71..ed07c61e 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; import { db } from "@main/level"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level/sublevels"; export const getUserData = async () => { return HydraApi.get(`/profile/me`) diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 355d04b2..ae22f552 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; -import type { LibraryGame } from "@types"; +import type { Game } from "@types"; import { TextField } from "@renderer/components"; import { @@ -35,7 +35,7 @@ export function Sidebar() { const { library, updateLibrary } = useLibrary(); const navigate = useNavigate(); - const [filteredLibrary, setFilteredLibrary] = useState([]); + const [filteredLibrary, setFilteredLibrary] = useState([]); const [isResizing, setIsResizing] = useState(false); const [sidebarWidth, setSidebarWidth] = useState( @@ -117,7 +117,7 @@ export function Sidebar() { }; }, [isResizing]); - const getGameTitle = (game: LibraryGame) => { + const getGameTitle = (game: Game) => { if (lastPacket?.game.id === game.id) { return t("downloading", { title: game.title, @@ -140,10 +140,7 @@ export function Sidebar() { } }; - const handleSidebarGameClick = ( - event: React.MouseEvent, - game: LibraryGame - ) => { + const handleSidebarGameClick = (event: React.MouseEvent, game: Game) => { const path = buildGameDetailsPath({ ...game, objectId: game.objectID, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 88f3297f..2ee60347 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -2,7 +2,6 @@ import type { CatalogueCategory } from "@shared"; import type { AppUpdaterEvent, Game, - LibraryGame, GameShop, HowLongToBeatCategory, ShopDetails, @@ -23,7 +22,6 @@ import type { UserStats, UserDetails, FriendRequestSync, - GameAchievement, GameArtifact, LudusaviBackup, UserAchievement, @@ -77,7 +75,7 @@ declare global { onUpdateAchievements: ( objectId: string, shop: GameShop, - cb: (achievements: GameAchievement[]) => void + cb: (achievements: UserAchievement[]) => void ) => () => Electron.IpcRenderer; getPublishers: () => Promise; getDevelopers: () => Promise; @@ -102,7 +100,7 @@ declare global { winePrefixPath: string | null ) => Promise; verifyExecutablePathInUse: (executablePath: string) => Promise; - getLibrary: () => Promise; + getLibrary: () => Promise; openGameInstaller: (gameId: number) => Promise; openGameInstallerPath: (gameId: number) => Promise; openGameExecutablePath: (gameId: number) => Promise; diff --git a/src/renderer/src/features/library-slice.ts b/src/renderer/src/features/library-slice.ts index 6c95aa79..f536ace7 100644 --- a/src/renderer/src/features/library-slice.ts +++ b/src/renderer/src/features/library-slice.ts @@ -1,10 +1,10 @@ import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; -import type { LibraryGame } from "@types"; +import type { Game } from "@types"; export interface LibraryState { - value: LibraryGame[]; + value: Game[]; } const initialState: LibraryState = { diff --git a/src/renderer/src/pages/achievements/achievement-list.tsx b/src/renderer/src/pages/achievements/achievement-list.tsx index ef178b50..6066f241 100644 --- a/src/renderer/src/pages/achievements/achievement-list.tsx +++ b/src/renderer/src/pages/achievements/achievement-list.tsx @@ -47,7 +47,14 @@ export function AchievementList({ achievements }: AchievementListProps) {

{achievement.description}

-
+
{achievement.points != undefined ? (
void; openGameInstaller: (gameId: number) => void; @@ -65,7 +65,7 @@ export function DownloadGroup({ resumeSeeding, } = useDownload(); - const getFinalDownloadSize = (game: LibraryGame) => { + const getFinalDownloadSize = (game: Game) => { const isGameDownloading = lastPacket?.game.id === game.id; if (game.fileSize) return formatBytes(game.fileSize); @@ -86,7 +86,7 @@ export function DownloadGroup({ return map; }, [seedingStatus]); - const getGameInfo = (game: LibraryGame) => { + const getGameInfo = (game: Game) => { const isGameDownloading = lastPacket?.game.id === game.id; const finalDownloadSize = getFinalDownloadSize(game); const seedingStatus = seedingMap.get(game.id); @@ -165,7 +165,7 @@ export function DownloadGroup({ return

{t(game.status as string)}

; }; - const getGameActions = (game: LibraryGame): DropdownMenuItem[] => { + const getGameActions = (game: Game): DropdownMenuItem[] => { const isGameDownloading = lastPacket?.game.id === game.id; const deleting = isGameDeleting(game.id); diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index 5c5a121a..41dbae90 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -7,7 +7,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; import * as styles from "./downloads.css"; import { DeleteGameModal } from "./delete-game-modal"; import { DownloadGroup } from "./download-group"; -import type { LibraryGame, SeedingStatus } from "@types"; +import type { Game, SeedingStatus } from "@types"; import { orderBy } from "lodash-es"; import { ArrowDownIcon } from "@primer/octicons-react"; @@ -49,8 +49,8 @@ export default function Downloads() { setShowDeleteModal(true); }; - const libraryGroup: Record = useMemo(() => { - const initialValue: Record = { + const libraryGroup: Record = useMemo(() => { + const initialValue: Record = { downloading: [], queued: [], complete: [], diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 7787b22a..d3a65ae5 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -23,7 +23,7 @@ import { buildGameAchievementPath } from "@renderer/helpers"; import { SPACING_UNIT } from "@renderer/theme.css"; import { useSubscription } from "@renderer/hooks/use-subscription"; -const fakeAchievements: UserAchievement[] = [ +const achievementsPlaceholder: UserAchievement[] = [ { displayName: "Timber!!", name: "", @@ -140,7 +140,7 @@ export function Sidebar() {

{t("sign_in_to_see_achievements")}

    - {fakeAchievements.map((achievement, index) => ( + {achievementsPlaceholder.map((achievement, index) => (
  • ; - export interface GameRunning { id?: number; title: string; @@ -139,24 +53,6 @@ export interface GameRunning { sessionDurationInMillis: number; } -export interface DownloadProgress { - downloadSpeed: number; - timeRemaining: number; - numPeers: number; - numSeeds: number; - isDownloadingMetadata: boolean; - isCheckingFiles: boolean; - progress: number; - gameId: number; - game: LibraryGame; -} - -export interface SeedingStatus { - gameId: number; - status: GameStatus; - uploadSpeed: number; -} - export interface UserPreferences { downloadsPath: string | null; language: string; @@ -344,11 +240,6 @@ export interface UserStats { unlockedAchievementSum?: number; } -export interface UnlockedAchievement { - name: string; - unlockTime: number; -} - export interface AchievementFile { type: Cracker; filePath: string; @@ -407,9 +298,9 @@ export interface CatalogueSearchPayload { developers: string[]; } +export * from "./game.types"; export * from "./steam.types"; -export * from "./real-debrid.types"; +export * from "./download.types"; export * from "./ludusavi.types"; export * from "./how-long-to-beat.types"; -export * from "./torbox.types"; export * from "./level.types"; diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 490ab060..3a9c0c14 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -1,3 +1,5 @@ +import type { SteamAchievement, UnlockedAchievement } from "./game.types"; + export type SubscriptionStatus = "active" | "pending" | "cancelled"; export interface Subscription { @@ -21,3 +23,8 @@ export interface User { backgroundImageUrl: string | null; subscription: Subscription | null; } + +export interface GameAchievement { + achievements: SteamAchievement[]; + unlockedAchievements: UnlockedAchievement[]; +} diff --git a/src/types/real-debrid.types.ts b/src/types/real-debrid.types.ts deleted file mode 100644 index 6b16ecfd..00000000 --- a/src/types/real-debrid.types.ts +++ /dev/null @@ -1,66 +0,0 @@ -export interface RealDebridUnrestrictLink { - id: string; - filename: string; - mimeType: string; - filesize: number; - link: string; - host: string; - host_icon: string; - chunks: number; - crc: number; - download: string; - streamable: number; -} - -export interface RealDebridAddMagnet { - id: string; - // URL of the created resource - uri: string; -} - -export interface RealDebridTorrentInfo { - id: string; - filename: string; - original_filename: string; - hash: string; - bytes: number; - original_bytes: number; - host: string; - split: number; - progress: number; - status: - | "magnet_error" - | "magnet_conversion" - | "waiting_files_selection" - | "queued" - | "downloading" - | "downloaded" - | "error" - | "virus" - | "compressing" - | "uploading" - | "dead"; - added: string; - files: { - id: number; - path: string; - bytes: number; - selected: number; - }[]; - links: string[]; - ended: string; - speed: number; - seeders: number; -} - -export interface RealDebridUser { - id: number; - username: string; - email: string; - points: number; - locale: string; - avatar: string; - type: string; - premium: number; - expiration: string; -} diff --git a/src/types/torbox.types.ts b/src/types/torbox.types.ts deleted file mode 100644 index a53ccc4c..00000000 --- a/src/types/torbox.types.ts +++ /dev/null @@ -1,77 +0,0 @@ -export interface TorBoxUser { - id: number; - email: string; - plan: string; - expiration: string; -} - -export interface TorBoxUserRequest { - success: boolean; - detail: string; - error: string; - data: TorBoxUser; -} - -export interface TorBoxFile { - id: number; - md5: string; - s3_path: string; - name: string; - size: number; - mimetype: string; - short_name: string; -} - -export interface TorBoxTorrentInfo { - id: number; - hash: string; - created_at: string; - updated_at: string; - magnet: string; - size: number; - active: boolean; - cached: boolean; - auth_id: string; - download_state: - | "downloading" - | "uploading" - | "stalled (no seeds)" - | "paused" - | "completed" - | "cached" - | "metaDL" - | "checkingResumeData"; - seeds: number; - ratio: number; - progress: number; - download_speed: number; - upload_speed: number; - name: string; - eta: number; - files: TorBoxFile[]; -} - -export interface TorBoxTorrentInfoRequest { - success: boolean; - detail: string; - error: string; - data: TorBoxTorrentInfo[]; -} - -export interface TorBoxAddTorrentRequest { - success: boolean; - detail: string; - error: string; - data: { - torrent_id: number; - name: string; - hash: string; - }; -} - -export interface TorBoxRequestLinkRequest { - success: boolean; - detail: string; - error: string; - data: string; -} From c115040e9015a039b80fb7b985955a1a2638af5d Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Thu, 16 Jan 2025 02:37:57 +0000 Subject: [PATCH 07/23] fix: fixing sonar issues --- src/main/events/auth/get-session-hash.ts | 3 +-- src/main/events/auth/sign-out.ts | 3 +-- src/main/events/misc/open-checkout.ts | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/events/auth/get-session-hash.ts b/src/main/events/auth/get-session-hash.ts index 5848cbd7..c81e0965 100644 --- a/src/main/events/auth/get-session-hash.ts +++ b/src/main/events/auth/get-session-hash.ts @@ -1,9 +1,8 @@ import jwt from "jsonwebtoken"; import { registerEvent } from "../register-event"; -import { db } from "@main/level"; +import { db, levelKeys } from "@main/level"; import type { Auth } from "@types"; -import { levelKeys } from "@main/level"; import { Crypto } from "@main/services"; const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => { diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 866d1ec0..50ea0c51 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -3,8 +3,7 @@ import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services"; import { dataSource } from "@main/data-source"; import { DownloadQueue, Game } from "@main/entity"; import { PythonRPC } from "@main/services/python-rpc"; -import { db } from "@main/level"; -import { levelKeys } from "@main/level"; +import { db, levelKeys } from "@main/level"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const databaseOperations = dataSource diff --git a/src/main/events/misc/open-checkout.ts b/src/main/events/misc/open-checkout.ts index 95d76d5b..76316a6e 100644 --- a/src/main/events/misc/open-checkout.ts +++ b/src/main/events/misc/open-checkout.ts @@ -1,9 +1,8 @@ import { shell } from "electron"; import { registerEvent } from "../register-event"; import { Crypto, HydraApi } from "@main/services"; -import { db } from "@main/level"; +import { db, levelKeys } from "@main/level"; import type { Auth } from "@types"; -import { levelKeys } from "@main/level"; const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { const auth = await db.get(levelKeys.auth, { From 1f0e19585447303a8d7fce4a75a9f8fd0b9c03b9 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 19 Jan 2025 17:59:39 +0000 Subject: [PATCH 08/23] feat: migrating games to leveldb --- .../cloud-save/get-game-backup-preview.ts | 9 +- .../events/cloud-save/upload-save-game.ts | 10 +- .../events/library/add-game-to-library.ts | 19 ++-- src/main/events/library/close-game.ts | 10 +- .../events/library/create-game-shortcut.ts | 12 +-- src/main/events/library/delete-game-folder.ts | 37 +++----- .../events/library/get-game-by-object-id.ts | 19 ++-- src/main/events/library/get-library.ts | 23 ++--- .../library/open-game-executable-path.ts | 10 +- .../library/open-game-installer-path.ts | 10 +- .../events/library/open-game-installer.ts | 16 ++-- .../library/remove-game-from-library.ts | 33 +++---- src/main/events/library/remove-game.ts | 20 ++-- .../events/library/reset-game-achievements.ts | 32 ++++--- .../events/library/select-game-wine-prefix.ts | 19 +++- .../events/library/update-executable-path.ts | 24 ++--- .../events/library/update-launch-options.ts | 23 +++-- .../events/library/verify-executable-path.ts | 12 ++- src/main/level/sublevels/index.ts | 1 + src/main/level/sublevels/keys.ts | 1 + src/main/main.ts | 42 ++++----- .../achievement-watcher-manager.ts | 12 +-- .../achievements/find-achivement-files.ts | 2 +- .../services/download/download-manager.ts | 18 ++-- .../library-sync/clear-games-remote-id.ts | 17 +++- src/main/services/library-sync/create-game.ts | 16 ++-- .../library-sync/update-game-playtime.ts | 2 +- .../library-sync/upload-games-batch.ts | 16 ++-- src/main/services/process-watcher.ts | 72 ++++++++------ src/preload/index.ts | 93 ++++++++++++------- src/renderer/src/declaration.d.ts | 52 ++++++----- src/types/download.types.ts | 3 +- src/types/game.types.ts | 29 ------ src/types/level.types.ts | 39 +++++++- 34 files changed, 410 insertions(+), 343 deletions(-) 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[]; From d760d0139de88580d53169ec5ac399a57ceadee4 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 20 Jan 2025 10:09:49 +0000 Subject: [PATCH 09/23] feat: migrating games to level --- src/main/data-source.ts | 4 +- src/main/entity/download-queue.entity.ts | 25 --- src/main/entity/game.entity.ts | 90 --------- src/main/entity/index.ts | 2 - src/main/events/auth/sign-out.ts | 34 ++-- .../events/helpers/generate-lutris-yaml.ts | 44 ----- .../events/library/add-game-to-library.ts | 69 ++++--- src/main/events/library/get-library.ts | 18 +- .../events/library/open-game-installer.ts | 23 +-- src/main/events/library/open-game.ts | 23 ++- .../events/torrenting/cancel-game-download.ts | 26 +-- src/main/events/torrenting/pause-game-seed.ts | 18 +- .../events/torrenting/resume-game-download.ts | 2 - .../events/torrenting/resume-game-seed.ts | 25 +-- src/main/index.ts | 26 --- src/main/knex-client.ts | 53 +----- src/main/level/sublevels/downloads.ts | 11 ++ .../migrations/20240830143811_Hydra_2_0_3.ts | 171 ------------------ .../migrations/20240830143906_RepackUris.ts | 18 -- .../20240913213944_update_user_language.ts | 13 -- .../20240915035339_ensure_repack_uris.ts | 17 -- .../20240918001920_FixMissingColumns.ts | 41 ----- .../20240919030940_create_game_achievement.ts | 20 -- ...add_achievement_notification_preference.ts | 17 -- ...20241015235142_create_user_subscription.ts | 27 --- ...20241016100249_add_background_image_url.ts | 17 -- .../20241019081648_add_wine_prefix_to_game.ts | 17 -- ...241030171454_add_start_minimized_column.ts | 17 -- ...106053733_add_disable_nsfw_alert_column.ts | 17 -- .../20241108200154_add_should_seed_colum.ts | 17 -- .../20241108201806_add_seed_after_download.ts | 20 -- ..._hidden_achievement_description_column .ts | 20 -- ...44022_add_launch_options_column_to_game.ts | 17 -- src/main/migrations/migration.stub | 11 -- src/main/repository.ts | 6 +- .../achievement-watcher-manager.ts | 37 ++-- .../achievements/find-achivement-files.ts | 2 +- .../update-local-unlocked-achivements.ts | 3 +- .../services/download/download-manager.ts | 19 +- src/main/services/download/types.ts | 4 +- .../library-sync/merge-with-remote-games.ts | 32 ++-- src/main/services/notifications/index.ts | 2 +- src/main/services/python-rpc.ts | 2 +- src/main/services/window-manager.ts | 32 ++-- .../game-details/game-details.context.tsx | 4 +- src/renderer/src/hooks/use-download.ts | 44 +++-- src/types/level.types.ts | 3 +- 47 files changed, 219 insertions(+), 941 deletions(-) delete mode 100644 src/main/entity/download-queue.entity.ts delete mode 100644 src/main/entity/game.entity.ts delete mode 100644 src/main/events/helpers/generate-lutris-yaml.ts create mode 100644 src/main/level/sublevels/downloads.ts delete mode 100644 src/main/migrations/20240830143811_Hydra_2_0_3.ts delete mode 100644 src/main/migrations/20240830143906_RepackUris.ts delete mode 100644 src/main/migrations/20240913213944_update_user_language.ts delete mode 100644 src/main/migrations/20240915035339_ensure_repack_uris.ts delete mode 100644 src/main/migrations/20240918001920_FixMissingColumns.ts delete mode 100644 src/main/migrations/20240919030940_create_game_achievement.ts delete mode 100644 src/main/migrations/20241013012900_add_achievement_notification_preference.ts delete mode 100644 src/main/migrations/20241015235142_create_user_subscription.ts delete mode 100644 src/main/migrations/20241016100249_add_background_image_url.ts delete mode 100644 src/main/migrations/20241019081648_add_wine_prefix_to_game.ts delete mode 100644 src/main/migrations/20241030171454_add_start_minimized_column.ts delete mode 100644 src/main/migrations/20241106053733_add_disable_nsfw_alert_column.ts delete mode 100644 src/main/migrations/20241108200154_add_should_seed_colum.ts delete mode 100644 src/main/migrations/20241108201806_add_seed_after_download.ts delete mode 100644 src/main/migrations/20241216140633_add_hidden_achievement_description_column .ts delete mode 100644 src/main/migrations/20241226044022_add_launch_options_column_to_game.ts delete mode 100644 src/main/migrations/migration.stub diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 7414a758..69e54667 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -1,11 +1,11 @@ import { DataSource } from "typeorm"; -import { DownloadQueue, Game, UserPreferences } from "@main/entity"; +import { UserPreferences } from "@main/entity"; import { databasePath } from "./constants"; export const dataSource = new DataSource({ type: "better-sqlite3", - entities: [Game, UserPreferences, DownloadQueue], + entities: [UserPreferences], synchronize: false, database: databasePath, }); diff --git a/src/main/entity/download-queue.entity.ts b/src/main/entity/download-queue.entity.ts deleted file mode 100644 index cf618947..00000000 --- a/src/main/entity/download-queue.entity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - CreateDateColumn, - UpdateDateColumn, - OneToOne, - JoinColumn, -} from "typeorm"; -import type { Game } from "./game.entity"; - -@Entity("download_queue") -export class DownloadQueue { - @PrimaryGeneratedColumn() - id: number; - - @OneToOne("Game", "downloadQueue") - @JoinColumn() - game: Game; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/entity/game.entity.ts b/src/main/entity/game.entity.ts deleted file mode 100644 index 0fcdcc77..00000000 --- a/src/main/entity/game.entity.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToOne, -} from "typeorm"; - -import type { GameShop, GameStatus } from "@types"; -import { Downloader } from "@shared"; -import type { DownloadQueue } from "./download-queue.entity"; - -@Entity("game") -export class Game { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { unique: true }) - objectID: string; - - @Column("text", { unique: true, nullable: true }) - remoteId: string | null; - - @Column("text") - title: string; - - @Column("text", { nullable: true }) - iconUrl: string | null; - - @Column("text", { nullable: true }) - folderName: string | null; - - @Column("text", { nullable: true }) - downloadPath: string | null; - - @Column("text", { nullable: true }) - executablePath: string | null; - - @Column("text", { nullable: true }) - launchOptions: string | null; - - @Column("text", { nullable: true }) - winePrefixPath: string | null; - - @Column("int", { default: 0 }) - playTimeInMilliseconds: number; - - @Column("text") - shop: GameShop; - - @Column("text", { nullable: true }) - status: GameStatus | null; - - @Column("int", { default: Downloader.Torrent }) - downloader: Downloader; - - /** - * Progress is a float between 0 and 1 - */ - @Column("float", { default: 0 }) - progress: number; - - @Column("int", { default: 0 }) - bytesDownloaded: number; - - @Column("datetime", { nullable: true }) - lastTimePlayed: Date | null; - - @Column("float", { default: 0 }) - fileSize: number; - - @Column("text", { nullable: true }) - uri: string | null; - - @OneToOne("DownloadQueue", "game") - downloadQueue: DownloadQueue; - - @Column("boolean", { default: false }) - isDeleted: boolean; - - @Column("boolean", { default: false }) - shouldSeed: boolean; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index f35f643d..ebf29400 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,3 +1 @@ -export * from "./game.entity"; export * from "./user-preferences.entity"; -export * from "./download-queue.entity"; diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 50ea0c51..4de9c285 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -1,31 +1,25 @@ import { registerEvent } from "../register-event"; import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services"; -import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game } from "@main/entity"; import { PythonRPC } from "@main/services/python-rpc"; -import { db, levelKeys } from "@main/level"; +import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { - const databaseOperations = dataSource - .transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.getRepository(DownloadQueue).delete({}); - - await transactionalEntityManager.getRepository(Game).delete({}); - - await db.batch([ - { - type: "del", - key: levelKeys.auth, - }, - { - type: "del", - key: levelKeys.user, - }, - ]); - }) + const databaseOperations = db + .batch([ + { + type: "del", + key: levelKeys.auth, + }, + { + type: "del", + key: levelKeys.user, + }, + ]) .then(() => { /* Removes all games being played */ gamesPlaytime.clear(); + + return Promise.all([gamesSublevel.clear(), downloadsSublevel.clear()]); }); /* Cancels any ongoing downloads */ diff --git a/src/main/events/helpers/generate-lutris-yaml.ts b/src/main/events/helpers/generate-lutris-yaml.ts deleted file mode 100644 index f47a2a68..00000000 --- a/src/main/events/helpers/generate-lutris-yaml.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Document as YMLDocument } from "yaml"; -import { Game } from "@main/entity"; -import path from "node:path"; - -export const generateYML = (game: Game) => { - const slugifiedGameTitle = game.title.replace(/\s/g, "-").toLocaleLowerCase(); - - const doc = new YMLDocument({ - name: game.title, - game_slug: slugifiedGameTitle, - slug: `${slugifiedGameTitle}-installer`, - version: "Installer", - runner: "wine", - script: { - game: { - prefix: "$GAMEDIR", - arch: "win64", - working_dir: "$GAMEDIR", - }, - installer: [ - { - task: { - name: "create_prefix", - arch: "win64", - prefix: "$GAMEDIR", - }, - }, - { - task: { - executable: path.join( - game.downloadPath!, - game.folderName!, - "setup.exe" - ), - name: "wineexec", - prefix: "$GAMEDIR", - }, - }, - ], - }, - }); - - return doc.toString(); -}; diff --git a/src/main/events/library/add-game-to-library.ts b/src/main/events/library/add-game-to-library.ts index dd9a6bd6..e27709e9 100644 --- a/src/main/events/library/add-game-to-library.ts +++ b/src/main/events/library/add-game-to-library.ts @@ -1,5 +1,3 @@ -import { gameRepository } from "@main/repository"; - import { registerEvent } from "../register-event"; import type { Game, GameShop } from "@types"; @@ -8,7 +6,7 @@ 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"; +import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; const addGameToLibrary = async ( _event: Electron.IpcMainInvokeEvent, @@ -16,41 +14,42 @@ const addGameToLibrary = async ( objectId: string, title: string ) => { - return gameRepository - .update( - { - objectID: objectId, - }, - { - shop, - status: null, - isDeleted: false, - } - ) - .then(async ({ affected }) => { - if (!affected) { - const steamGame = await steamGamesWorker.run(Number(objectId), { - name: "getById", - }); + const gameKey = levelKeys.game(shop, objectId); + const game = await gamesSublevel.get(gameKey); - const iconUrl = steamGame?.clientIcon - ? steamUrlBuilder.icon(objectId, steamGame.clientIcon) - : null; + if (game) { + await downloadsSublevel.del(gameKey); - const game: Game = { - title, - iconUrl, - objectId, - shop, - }; - - await gamesSublevel.put(levelKeys.game(shop, objectId), game); - } - - updateLocalUnlockedAchivements(game!); - - createGame(game!).catch(() => {}); + await gamesSublevel.put(gameKey, { + ...game, + isDeleted: false, }); + } else { + const steamGame = await steamGamesWorker.run(Number(objectId), { + name: "getById", + }); + + const iconUrl = steamGame?.clientIcon + ? steamUrlBuilder.icon(objectId, steamGame.clientIcon) + : null; + + const game: Game = { + title, + iconUrl, + objectId, + shop, + remoteId: null, + isDeleted: false, + playTimeInMilliseconds: 0, + lastTimePlayed: null, + }; + + await gamesSublevel.put(levelKeys.game(shop, objectId), game); + + updateLocalUnlockedAchivements(game!); + + createGame(game!).catch(() => {}); + } }; registerEvent("addGameToLibrary", addGameToLibrary); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 73dc2e04..bdaabc87 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -1,13 +1,23 @@ import { registerEvent } from "../register-event"; -import { gamesSublevel } from "@main/level"; +import { downloadsSublevel, gamesSublevel } from "@main/level"; const getLibrary = async () => { - // TODO: Add sorting return gamesSublevel - .values() + .iterator() .all() .then((results) => { - return results.filter((game) => game.isDeleted === false); + return Promise.all( + results + .filter(([_key, game]) => game.isDeleted === false) + .map(async ([key, game]) => { + const download = await downloadsSublevel.get(key); + + return { + ...game, + download, + }; + }) + ); }); }; diff --git a/src/main/events/library/open-game-installer.ts b/src/main/events/library/open-game-installer.ts index 949b4364..955d71fd 100644 --- a/src/main/events/library/open-game-installer.ts +++ b/src/main/events/library/open-game-installer.ts @@ -1,13 +1,11 @@ import { shell } from "electron"; import path from "node:path"; import fs from "node:fs"; -import { writeFile } from "node:fs/promises"; import { spawnSync, exec } from "node:child_process"; -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 { downloadsSublevel, levelKeys } from "@main/level"; import { GameShop } from "@types"; const executeGameInstaller = (filePath: string) => { @@ -29,18 +27,18 @@ const openGameInstaller = async ( shop: GameShop, objectId: string ) => { - const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); + const downloadKey = levelKeys.game(shop, objectId); + const download = await downloadsSublevel.get(downloadKey); - if (!game || game.isDeleted || !game.folderName) return true; + if (!download || !download.folderName) return true; const gamePath = path.join( - game.downloadPath ?? (await getDownloadsPath()), - game.folderName! + download.downloadPath ?? (await getDownloadsPath()), + download.folderName! ); if (!fs.existsSync(gamePath)) { - // TODO: LEVELDB Remove download? - // await gameRepository.update({ id: gameId }, { status: null }); + await downloadsSublevel.del(downloadKey); return true; } @@ -70,13 +68,6 @@ const openGameInstaller = async ( ); } - if (spawnSync("which", ["lutris"]).status === 0) { - const ymlPath = path.join(gamePath, "setup.yml"); - await writeFile(ymlPath, generateYML(game)); - exec(`lutris --install "${ymlPath}"`); - return true; - } - shell.openPath(gamePath); return true; }; diff --git a/src/main/events/library/open-game.ts b/src/main/events/library/open-game.ts index cf73c810..f60cd200 100644 --- a/src/main/events/library/open-game.ts +++ b/src/main/events/library/open-game.ts @@ -1,22 +1,31 @@ -import { gameRepository } from "@main/repository"; - import { registerEvent } from "../register-event"; import { shell } from "electron"; import { parseExecutablePath } from "../helpers/parse-executable-path"; +import { levelKeys } from "@main/level"; +import { gamesSublevel } from "@main/level"; +import { GameShop } from "@types"; const openGame = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number, + shop: GameShop, + objectId: string, executablePath: string, launchOptions: string | null ) => { // TODO: revisit this for launchOptions const parsedPath = parseExecutablePath(executablePath); - await gameRepository.update( - { id: gameId }, - { executablePath: parsedPath, launchOptions } - ); + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + + if (!game) return; + + await gamesSublevel.put(gameKey, { + ...game, + executablePath: parsedPath, + launchOptions, + }); shell.openPath(parsedPath); }; diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index fbdf2761..20ba0820 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -1,31 +1,17 @@ import { registerEvent } from "../register-event"; import { DownloadManager } from "@main/services"; -import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game } from "@main/entity"; +import { GameShop } from "@types"; +import { downloadsSublevel, levelKeys } from "@main/level"; const cancelGameDownload = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - await dataSource.transaction(async (transactionalEntityManager) => { - await DownloadManager.cancelDownload(gameId); + await DownloadManager.cancelDownload(shop, objectId); - await transactionalEntityManager.getRepository(DownloadQueue).delete({ - game: { id: gameId }, - }); - - await transactionalEntityManager.getRepository(Game).update( - { - id: gameId, - }, - { - status: "removed", - bytesDownloaded: 0, - progress: 0, - } - ); - }); + await downloadsSublevel.del(levelKeys.game(shop, objectId)); }; registerEvent("cancelGameDownload", cancelGameDownload); diff --git a/src/main/events/torrenting/pause-game-seed.ts b/src/main/events/torrenting/pause-game-seed.ts index df2af756..62dfca96 100644 --- a/src/main/events/torrenting/pause-game-seed.ts +++ b/src/main/events/torrenting/pause-game-seed.ts @@ -1,17 +1,25 @@ +import { downloadsSublevel } from "@main/level"; +import { levelKeys } from "@main/level"; import { registerEvent } from "../register-event"; import { DownloadManager } from "@main/services"; -import { gameRepository } from "@main/repository"; +import type { GameShop } from "@types"; const pauseGameSeed = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - await gameRepository.update(gameId, { - status: "complete", + const downloadKey = levelKeys.game(shop, objectId); + const download = await downloadsSublevel.get(downloadKey); + + if (!download) return; + + await downloadsSublevel.put(downloadKey, { + ...download, shouldSeed: false, }); - await DownloadManager.pauseSeeding(gameId); + await DownloadManager.pauseSeeding(download); }; registerEvent("pauseGameSeed", pauseGameSeed); diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index c8c75545..2327e929 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -1,11 +1,9 @@ import { Not } from "typeorm"; import { registerEvent } from "../register-event"; -import { gameRepository } from "../../repository"; import { DownloadManager } from "@main/services"; import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game } from "@main/entity"; const resumeGameDownload = async ( _event: Electron.IpcMainInvokeEvent, diff --git a/src/main/events/torrenting/resume-game-seed.ts b/src/main/events/torrenting/resume-game-seed.ts index 9f79e53a..62493717 100644 --- a/src/main/events/torrenting/resume-game-seed.ts +++ b/src/main/events/torrenting/resume-game-seed.ts @@ -1,29 +1,24 @@ +import { downloadsSublevel } from "@main/level"; +import { levelKeys } from "@main/level"; import { registerEvent } from "../register-event"; -import { gameRepository } from "../../repository"; import { DownloadManager } from "@main/services"; -import { Downloader } from "@shared"; +import type { GameShop } from "@types"; const resumeGameSeed = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - const game = await gameRepository.findOne({ - where: { - id: gameId, - isDeleted: false, - downloader: Downloader.Torrent, - progress: 1, - }, - }); + const download = await downloadsSublevel.get(levelKeys.game(shop, objectId)); - if (!game) return; + if (!download) return; - await gameRepository.update(gameId, { - status: "seeding", + await downloadsSublevel.put(levelKeys.game(shop, objectId), { + ...download, shouldSeed: true, }); - await DownloadManager.resumeSeeding(game); + await DownloadManager.resumeSeeding(download); }; registerEvent("resumeGameSeed", resumeGameSeed); diff --git a/src/main/index.ts b/src/main/index.ts index ca49a9fb..a60c4569 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,14 +3,11 @@ import updater from "electron-updater"; import i18n from "i18next"; import path from "node:path"; import url from "node:url"; -import fs from "node:fs"; import { electronApp, optimizer } from "@electron-toolkit/utils"; import { logger, WindowManager } from "@main/services"; import { dataSource } from "@main/data-source"; import resources from "@locales"; import { userPreferencesRepository } from "@main/repository"; -import { knexClient, migrationConfig } from "./knex-client"; -import { databaseDirectory } from "./constants"; import { PythonRPC } from "./services/python-rpc"; import { Aria2 } from "./services/aria2"; @@ -50,21 +47,6 @@ if (process.defaultApp) { app.setAsDefaultProtocolClient(PROTOCOL); } -const runMigrations = async () => { - if (!fs.existsSync(databaseDirectory)) { - fs.mkdirSync(databaseDirectory, { recursive: true }); - } - - await knexClient.migrate.list(migrationConfig).then((result) => { - logger.log( - "Migrations to run:", - result[1].map((migration) => migration.name) - ); - }); - - await knexClient.migrate.latest(migrationConfig); -}; - // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. @@ -76,14 +58,6 @@ app.whenReady().then(async () => { return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString()); }); - await runMigrations() - .then(() => { - logger.log("Migrations executed successfully"); - }) - .catch((err) => { - logger.log("Migrations failed to run:", err); - }); - await dataSource.initialize(); await import("./main"); diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index 821efc80..57982332 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -1,53 +1,6 @@ -import knex, { Knex } from "knex"; +import knex from "knex"; import { databasePath } from "./constants"; -import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3"; -import { RepackUris } from "./migrations/20240830143906_RepackUris"; -import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language"; -import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris"; import { app } from "electron"; -import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns"; -import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement"; -import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference"; -import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription"; -import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url"; -import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game"; -import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column"; -import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disable_nsfw_alert_column"; -import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum"; -import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download"; -import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column "; -import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game"; - -export type HydraMigration = Knex.Migration & { name: string }; - -class MigrationSource implements Knex.MigrationSource { - getMigrations(): Promise { - return Promise.resolve([ - Hydra2_0_3, - RepackUris, - UpdateUserLanguage, - EnsureRepackUris, - FixMissingColumns, - CreateGameAchievement, - AddAchievementNotificationPreference, - CreateUserSubscription, - AddBackgroundImageUrl, - AddWinePrefixToGame, - AddStartMinimizedColumn, - AddDisableNsfwAlertColumn, - AddShouldSeedColumn, - AddSeedAfterDownloadColumn, - AddHiddenAchievementDescriptionColumn, - AddLaunchOptionsColumnToGame, - ]); - } - getMigrationName(migration: HydraMigration): string { - return migration.name; - } - getMigration(migration: HydraMigration): Promise { - return Promise.resolve(migration); - } -} export const knexClient = knex({ debug: !app.isPackaged, @@ -56,7 +9,3 @@ export const knexClient = knex({ filename: databasePath, }, }); - -export const migrationConfig: Knex.MigratorConfig = { - migrationSource: new MigrationSource(), -}; diff --git a/src/main/level/sublevels/downloads.ts b/src/main/level/sublevels/downloads.ts new file mode 100644 index 00000000..23030670 --- /dev/null +++ b/src/main/level/sublevels/downloads.ts @@ -0,0 +1,11 @@ +import type { Download } from "@types"; + +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const downloadsSublevel = db.sublevel( + levelKeys.downloads, + { + valueEncoding: "json", + } +); diff --git a/src/main/migrations/20240830143811_Hydra_2_0_3.ts b/src/main/migrations/20240830143811_Hydra_2_0_3.ts deleted file mode 100644 index 6013f714..00000000 --- a/src/main/migrations/20240830143811_Hydra_2_0_3.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const Hydra2_0_3: HydraMigration = { - name: "Hydra_2_0_3", - up: async (knex: Knex) => { - const timestamp = new Date().getTime(); - - await knex.schema.hasTable("migrations").then(async (exists) => { - if (exists) { - await knex.schema.dropTable("migrations"); - } - }); - - await knex.schema.hasTable("download_source").then(async (exists) => { - if (!exists) { - await knex.schema.createTable("download_source", (table) => { - table.increments("id").primary(); - table - .text("url") - .unique({ indexName: "download_source_url_unique_" + timestamp }); - table.text("name").notNullable(); - table.text("etag"); - table.integer("downloadCount").notNullable().defaultTo(0); - table.text("status").notNullable().defaultTo(0); - table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); - table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); - }); - } - }); - - await knex.schema.hasTable("repack").then(async (exists) => { - if (!exists) { - await knex.schema.createTable("repack", (table) => { - table.increments("id").primary(); - table - .text("title") - .notNullable() - .unique({ indexName: "repack_title_unique_" + timestamp }); - table - .text("magnet") - .notNullable() - .unique({ indexName: "repack_magnet_unique_" + timestamp }); - table.integer("page"); - table.text("repacker").notNullable(); - table.text("fileSize").notNullable(); - table.datetime("uploadDate").notNullable(); - table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); - table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); - table - .integer("downloadSourceId") - .references("download_source.id") - .onDelete("CASCADE"); - }); - } - }); - - await knex.schema.hasTable("game").then(async (exists) => { - if (!exists) { - await knex.schema.createTable("game", (table) => { - table.increments("id").primary(); - table - .text("objectID") - .notNullable() - .unique({ indexName: "game_objectID_unique_" + timestamp }); - table - .text("remoteId") - .unique({ indexName: "game_remoteId_unique_" + timestamp }); - table.text("title").notNullable(); - table.text("iconUrl"); - table.text("folderName"); - table.text("downloadPath"); - table.text("executablePath"); - table.integer("playTimeInMilliseconds").notNullable().defaultTo(0); - table.text("shop").notNullable(); - table.text("status"); - table.integer("downloader").notNullable().defaultTo(1); - table.float("progress").notNullable().defaultTo(0); - table.integer("bytesDownloaded").notNullable().defaultTo(0); - table.datetime("lastTimePlayed"); - table.float("fileSize").notNullable().defaultTo(0); - table.text("uri"); - table.boolean("isDeleted").notNullable().defaultTo(0); - table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); - table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); - table - .integer("repackId") - .references("repack.id") - .unique("repack_repackId_unique_" + timestamp); - }); - } - }); - - await knex.schema.hasTable("user_preferences").then(async (exists) => { - if (!exists) { - await knex.schema.createTable("user_preferences", (table) => { - table.increments("id").primary(); - table.text("downloadsPath"); - table.text("language").notNullable().defaultTo("en"); - table.text("realDebridApiToken"); - table - .boolean("downloadNotificationsEnabled") - .notNullable() - .defaultTo(0); - table - .boolean("repackUpdatesNotificationsEnabled") - .notNullable() - .defaultTo(0); - table.boolean("preferQuitInsteadOfHiding").notNullable().defaultTo(0); - table.boolean("runAtStartup").notNullable().defaultTo(0); - table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); - table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); - }); - } - }); - - await knex.schema.hasTable("game_shop_cache").then(async (exists) => { - if (!exists) { - await knex.schema.createTable("game_shop_cache", (table) => { - table.text("objectID").primary().notNullable(); - table.text("shop").notNullable(); - table.text("serializedData"); - table.text("howLongToBeatSerializedData"); - table.text("language"); - table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); - table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); - }); - } - }); - - await knex.schema.hasTable("download_queue").then(async (exists) => { - if (!exists) { - await knex.schema.createTable("download_queue", (table) => { - table.increments("id").primary(); - table - .integer("gameId") - .references("game.id") - .unique("download_queue_gameId_unique_" + timestamp); - table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); - table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); - }); - } - }); - - await knex.schema.hasTable("user_auth").then(async (exists) => { - if (!exists) { - await knex.schema.createTable("user_auth", (table) => { - table.increments("id").primary(); - table.text("userId").notNullable().defaultTo(""); - table.text("displayName").notNullable().defaultTo(""); - table.text("profileImageUrl"); - table.text("accessToken").notNullable().defaultTo(""); - table.text("refreshToken").notNullable().defaultTo(""); - table.integer("tokenExpirationTimestamp").notNullable().defaultTo(0); - table.datetime("createdAt").notNullable().defaultTo(knex.fn.now()); - table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now()); - }); - } - }); - }, - - down: async (knex: Knex) => { - await knex.schema.dropTableIfExists("game"); - await knex.schema.dropTableIfExists("repack"); - await knex.schema.dropTableIfExists("download_queue"); - await knex.schema.dropTableIfExists("user_auth"); - await knex.schema.dropTableIfExists("game_shop_cache"); - await knex.schema.dropTableIfExists("user_preferences"); - await knex.schema.dropTableIfExists("download_source"); - }, -}; diff --git a/src/main/migrations/20240830143906_RepackUris.ts b/src/main/migrations/20240830143906_RepackUris.ts deleted file mode 100644 index 18bb9a59..00000000 --- a/src/main/migrations/20240830143906_RepackUris.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const RepackUris: HydraMigration = { - name: "RepackUris", - up: async (knex: Knex) => { - await knex.schema.alterTable("repack", (table) => { - table.text("uris").notNullable().defaultTo("[]"); - }); - }, - - down: async (knex: Knex) => { - await knex.schema.alterTable("repack", (table) => { - table.integer("page"); - table.dropColumn("uris"); - }); - }, -}; diff --git a/src/main/migrations/20240913213944_update_user_language.ts b/src/main/migrations/20240913213944_update_user_language.ts deleted file mode 100644 index 3297eb0d..00000000 --- a/src/main/migrations/20240913213944_update_user_language.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const UpdateUserLanguage: HydraMigration = { - name: "UpdateUserLanguage", - up: async (knex: Knex) => { - await knex("user_preferences") - .update("language", "pt-BR") - .where("language", "pt"); - }, - - down: async (_knex: Knex) => {}, -}; diff --git a/src/main/migrations/20240915035339_ensure_repack_uris.ts b/src/main/migrations/20240915035339_ensure_repack_uris.ts deleted file mode 100644 index 64fbcd2e..00000000 --- a/src/main/migrations/20240915035339_ensure_repack_uris.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const EnsureRepackUris: HydraMigration = { - name: "EnsureRepackUris", - up: async (knex: Knex) => { - await knex.schema.hasColumn("repack", "uris").then(async (exists) => { - if (!exists) { - await knex.schema.table("repack", (table) => { - table.text("uris").notNullable().defaultTo("[]"); - }); - } - }); - }, - - down: async (_knex: Knex) => {}, -}; diff --git a/src/main/migrations/20240918001920_FixMissingColumns.ts b/src/main/migrations/20240918001920_FixMissingColumns.ts deleted file mode 100644 index d23662ed..00000000 --- a/src/main/migrations/20240918001920_FixMissingColumns.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const FixMissingColumns: HydraMigration = { - name: "FixMissingColumns", - up: async (knex: Knex) => { - const timestamp = new Date().getTime(); - await knex.schema - .hasColumn("repack", "downloadSourceId") - .then(async (exists) => { - if (!exists) { - await knex.schema.table("repack", (table) => { - table - .integer("downloadSourceId") - .references("download_source.id") - .onDelete("CASCADE"); - }); - } - }); - - await knex.schema.hasColumn("game", "remoteId").then(async (exists) => { - if (!exists) { - await knex.schema.table("game", (table) => { - table - .text("remoteId") - .unique({ indexName: "game_remoteId_unique_" + timestamp }); - }); - } - }); - - await knex.schema.hasColumn("game", "uri").then(async (exists) => { - if (!exists) { - await knex.schema.table("game", (table) => { - table.text("uri"); - }); - } - }); - }, - - down: async (_knex: Knex) => {}, -}; diff --git a/src/main/migrations/20240919030940_create_game_achievement.ts b/src/main/migrations/20240919030940_create_game_achievement.ts deleted file mode 100644 index 791eeb29..00000000 --- a/src/main/migrations/20240919030940_create_game_achievement.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const CreateGameAchievement: HydraMigration = { - name: "CreateGameAchievement", - up: (knex: Knex) => { - return knex.schema.createTable("game_achievement", (table) => { - table.increments("id").primary(); - table.text("objectId").notNullable(); - table.text("shop").notNullable(); - table.text("achievements"); - table.text("unlockedAchievements"); - table.unique(["objectId", "shop"]); - }); - }, - - down: (knex: Knex) => { - return knex.schema.dropTable("game_achievement"); - }, -}; diff --git a/src/main/migrations/20241013012900_add_achievement_notification_preference.ts b/src/main/migrations/20241013012900_add_achievement_notification_preference.ts deleted file mode 100644 index a4f48265..00000000 --- a/src/main/migrations/20241013012900_add_achievement_notification_preference.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddAchievementNotificationPreference: HydraMigration = { - name: "AddAchievementNotificationPreference", - up: (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.boolean("achievementNotificationsEnabled").defaultTo(true); - }); - }, - - down: (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.dropColumn("achievementNotificationsEnabled"); - }); - }, -}; diff --git a/src/main/migrations/20241015235142_create_user_subscription.ts b/src/main/migrations/20241015235142_create_user_subscription.ts deleted file mode 100644 index 5f9ecab1..00000000 --- a/src/main/migrations/20241015235142_create_user_subscription.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const CreateUserSubscription: HydraMigration = { - name: "CreateUserSubscription", - up: async (knex: Knex) => { - return knex.schema.createTable("user_subscription", (table) => { - table.increments("id").primary(); - table.string("subscriptionId").defaultTo(""); - table - .text("userId") - .notNullable() - .references("user_auth.id") - .onDelete("CASCADE"); - table.string("status").defaultTo(""); - table.string("planId").defaultTo(""); - table.string("planName").defaultTo(""); - table.dateTime("expiresAt").nullable(); - table.dateTime("createdAt").defaultTo(knex.fn.now()); - table.dateTime("updatedAt").defaultTo(knex.fn.now()); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.dropTable("user_subscription"); - }, -}; diff --git a/src/main/migrations/20241016100249_add_background_image_url.ts b/src/main/migrations/20241016100249_add_background_image_url.ts deleted file mode 100644 index b377c650..00000000 --- a/src/main/migrations/20241016100249_add_background_image_url.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddBackgroundImageUrl: HydraMigration = { - name: "AddBackgroundImageUrl", - up: (knex: Knex) => { - return knex.schema.alterTable("user_auth", (table) => { - return table.text("backgroundImageUrl").nullable(); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("user_auth", (table) => { - return table.dropColumn("backgroundImageUrl"); - }); - }, -}; diff --git a/src/main/migrations/20241019081648_add_wine_prefix_to_game.ts b/src/main/migrations/20241019081648_add_wine_prefix_to_game.ts deleted file mode 100644 index 517f6fb5..00000000 --- a/src/main/migrations/20241019081648_add_wine_prefix_to_game.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddWinePrefixToGame: HydraMigration = { - name: "AddWinePrefixToGame", - up: (knex: Knex) => { - return knex.schema.alterTable("game", (table) => { - return table.text("winePrefixPath").nullable(); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("game", (table) => { - return table.dropColumn("winePrefixPath"); - }); - }, -}; diff --git a/src/main/migrations/20241030171454_add_start_minimized_column.ts b/src/main/migrations/20241030171454_add_start_minimized_column.ts deleted file mode 100644 index 69ede189..00000000 --- a/src/main/migrations/20241030171454_add_start_minimized_column.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddStartMinimizedColumn: HydraMigration = { - name: "AddStartMinimizedColumn", - up: (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.boolean("startMinimized").notNullable().defaultTo(0); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.dropColumn("startMinimized"); - }); - }, -}; diff --git a/src/main/migrations/20241106053733_add_disable_nsfw_alert_column.ts b/src/main/migrations/20241106053733_add_disable_nsfw_alert_column.ts deleted file mode 100644 index a248dd2b..00000000 --- a/src/main/migrations/20241106053733_add_disable_nsfw_alert_column.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddDisableNsfwAlertColumn: HydraMigration = { - name: "AddDisableNsfwAlertColumn", - up: (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.boolean("disableNsfwAlert").notNullable().defaultTo(0); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.dropColumn("disableNsfwAlert"); - }); - }, -}; diff --git a/src/main/migrations/20241108200154_add_should_seed_colum.ts b/src/main/migrations/20241108200154_add_should_seed_colum.ts deleted file mode 100644 index 7e90a3b1..00000000 --- a/src/main/migrations/20241108200154_add_should_seed_colum.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddShouldSeedColumn: HydraMigration = { - name: "AddShouldSeedColumn", - up: (knex: Knex) => { - return knex.schema.alterTable("game", (table) => { - return table.boolean("shouldSeed").notNullable().defaultTo(true); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("game", (table) => { - return table.dropColumn("shouldSeed"); - }); - }, -}; diff --git a/src/main/migrations/20241108201806_add_seed_after_download.ts b/src/main/migrations/20241108201806_add_seed_after_download.ts deleted file mode 100644 index 75b94577..00000000 --- a/src/main/migrations/20241108201806_add_seed_after_download.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddSeedAfterDownloadColumn: HydraMigration = { - name: "AddSeedAfterDownloadColumn", - up: (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table - .boolean("seedAfterDownloadComplete") - .notNullable() - .defaultTo(true); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.dropColumn("seedAfterDownloadComplete"); - }); - }, -}; diff --git a/src/main/migrations/20241216140633_add_hidden_achievement_description_column .ts b/src/main/migrations/20241216140633_add_hidden_achievement_description_column .ts deleted file mode 100644 index 36771c43..00000000 --- a/src/main/migrations/20241216140633_add_hidden_achievement_description_column .ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddHiddenAchievementDescriptionColumn: HydraMigration = { - name: "AddHiddenAchievementDescriptionColumn", - up: (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table - .boolean("showHiddenAchievementsDescription") - .notNullable() - .defaultTo(0); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("user_preferences", (table) => { - return table.dropColumn("showHiddenAchievementsDescription"); - }); - }, -}; diff --git a/src/main/migrations/20241226044022_add_launch_options_column_to_game.ts b/src/main/migrations/20241226044022_add_launch_options_column_to_game.ts deleted file mode 100644 index 417eeb63..00000000 --- a/src/main/migrations/20241226044022_add_launch_options_column_to_game.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const AddLaunchOptionsColumnToGame: HydraMigration = { - name: "AddLaunchOptionsColumnToGame", - up: (knex: Knex) => { - return knex.schema.alterTable("game", (table) => { - return table.string("launchOptions").nullable(); - }); - }, - - down: async (knex: Knex) => { - return knex.schema.alterTable("game", (table) => { - return table.dropColumn("launchOptions"); - }); - }, -}; diff --git a/src/main/migrations/migration.stub b/src/main/migrations/migration.stub deleted file mode 100644 index 299b3fc2..00000000 --- a/src/main/migrations/migration.stub +++ /dev/null @@ -1,11 +0,0 @@ -import type { HydraMigration } from "@main/knex-client"; -import type { Knex } from "knex"; - -export const MigrationName: HydraMigration = { - name: "MigrationName", - up: (knex: Knex) => { - return knex.schema.createTable("table_name", async (table) => {}); - }, - - down: async (knex: Knex) => {}, -}; diff --git a/src/main/repository.ts b/src/main/repository.ts index 5bbfaf9f..1a6975a2 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -1,9 +1,5 @@ import { dataSource } from "./data-source"; -import { DownloadQueue, Game, UserPreferences } from "@main/entity"; - -export const gameRepository = dataSource.getRepository(Game); +import { UserPreferences } from "@main/entity"; export const userPreferencesRepository = dataSource.getRepository(UserPreferences); - -export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index 754847c9..d1111b0d 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -1,6 +1,4 @@ -import { gameRepository } from "@main/repository"; import { parseAchievementFile } from "./parse-achievement-file"; -import { Game } from "@main/entity"; import { mergeAchievements } from "./merge-achievements"; import fs, { readdirSync } from "node:fs"; import { @@ -9,10 +7,9 @@ import { findAllAchievementFiles, getAlternativeObjectIds, } from "./find-achivement-files"; -import type { AchievementFile, UnlockedAchievement } from "@types"; +import type { AchievementFile, Game, UnlockedAchievement } from "@types"; import { achievementsLogger } from "../logger"; import { Cracker } from "@shared"; -import { IsNull, Not } from "typeorm"; import { publishCombinedNewAchievementNotification } from "../notifications"; import { gamesSublevel } from "@main/level"; @@ -47,12 +44,12 @@ const watchAchievementsWindows = async () => { }; const watchAchievementsWithWine = async () => { - const games = await gameRepository.find({ - where: { - isDeleted: false, - winePrefixPath: Not(IsNull()), - }, - }); + const games = await gamesSublevel + .values() + .all() + .then((games) => + games.filter((game) => !game.isDeleted && game.winePrefixPath) + ); for (const game of games) { const gameAchievementFiles = findAchievementFiles(game); @@ -188,11 +185,10 @@ export class AchievementWatcherManager { }; private static preSearchAchievementsWindows = async () => { - const games = await gameRepository.find({ - where: { - isDeleted: false, - }, - }); + const games = await gamesSublevel + .values() + .all() + .then((games) => games.filter((game) => !game.isDeleted)); const gameAchievementFilesMap = findAllAchievementFiles(); @@ -200,7 +196,7 @@ export class AchievementWatcherManager { games.map((game) => { const gameAchievementFiles: AchievementFile[] = []; - for (const objectId of getAlternativeObjectIds(game.objectID)) { + for (const objectId of getAlternativeObjectIds(game.objectId)) { gameAchievementFiles.push( ...(gameAchievementFilesMap.get(objectId) || []) ); @@ -216,11 +212,10 @@ export class AchievementWatcherManager { }; private static preSearchAchievementsWithWine = async () => { - const games = await gameRepository.find({ - where: { - isDeleted: false, - }, - }); + const games = await gamesSublevel + .values() + .all() + .then((games) => games.filter((game) => !game.isDeleted)); return Promise.all( games.map((game) => { diff --git a/src/main/services/achievements/find-achivement-files.ts b/src/main/services/achievements/find-achivement-files.ts index 0578065c..f9bd9a42 100644 --- a/src/main/services/achievements/find-achivement-files.ts +++ b/src/main/services/achievements/find-achivement-files.ts @@ -254,7 +254,7 @@ export const findAchievementFiles = (game: Game) => { for (const cracker of crackers) { for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) { - for (const objectId of getAlternativeObjectIds(game.objectID)) { + for (const objectId of getAlternativeObjectIds(game.objectId)) { const filePath = path.join( game.winePrefixPath ?? "", folderPath, diff --git a/src/main/services/achievements/update-local-unlocked-achivements.ts b/src/main/services/achievements/update-local-unlocked-achivements.ts index 0393477c..8832a475 100644 --- a/src/main/services/achievements/update-local-unlocked-achivements.ts +++ b/src/main/services/achievements/update-local-unlocked-achivements.ts @@ -4,8 +4,7 @@ import { } from "./find-achivement-files"; import { parseAchievementFile } from "./parse-achievement-file"; import { mergeAchievements } from "./merge-achievements"; -import type { UnlockedAchievement } from "@types"; -import { Game } from "@main/entity"; +import type { Game, UnlockedAchievement } from "@types"; export const updateLocalUnlockedAchivements = async (game: Game) => { const gameAchievementFiles = findAchievementFiles(game); diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index f85b018e..9f726ad9 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -1,11 +1,6 @@ -import { Game } from "@main/entity"; import { Downloader } from "@shared"; import { WindowManager } from "../window-manager"; -import { - downloadQueueRepository, - gameRepository, - userPreferencesRepository, -} from "@main/repository"; +import { userPreferencesRepository } from "@main/repository"; import { publishDownloadCompleteNotification } from "../notifications"; import type { Download, DownloadProgress } from "@types"; import { GofileApi, QiwiApi, DatanodesApi } from "../hosters"; @@ -23,7 +18,7 @@ import { logger } from "../logger"; import { downloadsSublevel, levelKeys } from "@main/level"; export class DownloadManager { - private static downloadingGameId: number | null = null; + private static downloadingGameId: string | null = null; public static async startRPC( download?: Download, @@ -34,13 +29,15 @@ export class DownloadManager { ? await this.getDownloadPayload(download).catch(() => undefined) : undefined, downloadsToSeed?.map((download) => ({ - game_id: game.id, - url: game.uri!, - save_path: game.downloadPath!, + game_id: `${download.shop}-${download.objectId}`, + url: download.uri!, + save_path: download.downloadPath!, })) ); - this.downloadingGameId = game?.id ?? null; + if (download) { + this.downloadingGameId = `${download.shop}-${download.objectId}`; + } } private static async getDownloadStatus() { diff --git a/src/main/services/download/types.ts b/src/main/services/download/types.ts index 8cacdcb7..bbd3efc5 100644 --- a/src/main/services/download/types.ts +++ b/src/main/services/download/types.ts @@ -1,9 +1,9 @@ export interface PauseDownloadPayload { - game_id: number; + game_id: string; } export interface CancelDownloadPayload { - game_id: number; + game_id: string; } export enum LibtorrentStatus { diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index d286ad6c..2b6eebb0 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -1,17 +1,15 @@ -import { gameRepository } from "@main/repository"; import { HydraApi } from "../hydra-api"; import { steamGamesWorker } from "@main/workers"; import { steamUrlBuilder } from "@shared"; +import { gamesSublevel, levelKeys } from "@main/level"; export const mergeWithRemoteGames = async () => { return HydraApi.get("/profile/games") .then(async (response) => { for (const game of response) { - const localGame = await gameRepository.findOne({ - where: { - objectID: game.objectId, - }, - }); + const localGame = await gamesSublevel.get( + levelKeys.game(game.shop, game.objectId) + ); if (localGame) { const updatedLastTimePlayed = @@ -26,17 +24,12 @@ export const mergeWithRemoteGames = async () => { ? game.playTimeInMilliseconds : localGame.playTimeInMilliseconds; - gameRepository.update( - { - objectID: game.objectId, - shop: "steam", - }, - { - remoteId: game.id, - lastTimePlayed: updatedLastTimePlayed, - playTimeInMilliseconds: updatedPlayTime, - } - ); + gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { + ...localGame, + remoteId: game.id, + lastTimePlayed: updatedLastTimePlayed, + playTimeInMilliseconds: updatedPlayTime, + }); } else { const steamGame = await steamGamesWorker.run(Number(game.objectId), { name: "getById", @@ -47,14 +40,15 @@ export const mergeWithRemoteGames = async () => { ? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon) : null; - gameRepository.insert({ - objectID: game.objectId, + gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { + objectId: game.objectId, title: steamGame?.name, remoteId: game.id, shop: game.shop, iconUrl, lastTimePlayed: game.lastTimePlayed, playTimeInMilliseconds: game.playTimeInMilliseconds, + isDeleted: false, }); } } diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index f3e2541b..23230589 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -1,7 +1,6 @@ import { Notification, app } from "electron"; import { t } from "i18next"; import trayIcon from "@resources/tray-icon.png?asset"; -import { Game } from "@main/entity"; import { userPreferencesRepository } from "@main/repository"; import fs from "node:fs"; import axios from "axios"; @@ -11,6 +10,7 @@ import { achievementSoundPath } from "@main/constants"; import icon from "@resources/icon.png?asset"; import { NotificationOptions, toXmlString } from "./xml"; import { logger } from "../logger"; +import type { Game } from "@types"; async function downloadImage(url: string | null) { if (!url) return undefined; diff --git a/src/main/services/python-rpc.ts b/src/main/services/python-rpc.ts index 1384a1be..22e60461 100644 --- a/src/main/services/python-rpc.ts +++ b/src/main/services/python-rpc.ts @@ -10,7 +10,7 @@ import { Readable } from "node:stream"; import { app, dialog } from "electron"; interface GamePayload { - game_id: number; + game_id: string; url: string; save_path: string; } diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index a7cfcee2..e7a6db2b 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -13,10 +13,11 @@ import i18next, { t } from "i18next"; import path from "node:path"; import icon from "@resources/icon.png?asset"; import trayIcon from "@resources/tray-icon.png?asset"; -import { gameRepository, userPreferencesRepository } from "@main/repository"; -import { IsNull, Not } from "typeorm"; +import { userPreferencesRepository } from "@main/repository"; import { HydraApi } from "./hydra-api"; import UserAgent from "user-agents"; +import { gamesSublevel } from "@main/level"; +import { slice, sortBy } from "lodash-es"; export class WindowManager { public static mainWindow: Electron.BrowserWindow | null = null; @@ -207,17 +208,22 @@ export class WindowManager { } const updateSystemTray = async () => { - const games = await gameRepository.find({ - where: { - isDeleted: false, - executablePath: Not(IsNull()), - lastTimePlayed: Not(IsNull()), - }, - take: 5, - order: { - lastTimePlayed: "DESC", - }, - }); + const games = await gamesSublevel + .values() + .all() + .then((games) => + slice( + sortBy( + games.filter( + (game) => + !game.isDeleted && game.executablePath && game.lastTimePlayed + ), + "lastTimePlayed", + "DESC" + ), + 5 + ) + ); const recentlyPlayedGames: Array = games.map(({ title, executablePath }) => ({ diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index 9242d9a6..3ee5f094 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -101,9 +101,9 @@ export function GameDetailsContextProvider({ const updateGame = useCallback(async () => { return window.electron - .getGameByObjectId(objectId!) + .getGameByObjectId(shop, objectId!) .then((result) => setGame(result)); - }, [setGame, objectId]); + }, [setGame, shop, objectId]); const isGameDownloading = lastPacket?.game.id === game?.id; diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts index 4ea79b93..2a21dea2 100644 --- a/src/renderer/src/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -9,7 +9,11 @@ import { setGameDeleting, removeGameFromDeleting, } from "@renderer/features"; -import type { DownloadProgress, StartGameDownloadPayload } from "@types"; +import type { + DownloadProgress, + GameShop, + StartGameDownloadPayload, +} from "@types"; import { useDate } from "./use-date"; import { formatBytes } from "@shared"; @@ -31,48 +35,48 @@ export function useDownload() { return game; }; - const pauseDownload = async (gameId: number) => { - await window.electron.pauseGameDownload(gameId); + const pauseDownload = async (shop: GameShop, objectId: string) => { + await window.electron.pauseGameDownload(shop, objectId); await updateLibrary(); dispatch(clearDownload()); }; - const resumeDownload = async (gameId: number) => { - await window.electron.resumeGameDownload(gameId); + const resumeDownload = async (shop: GameShop, objectId: string) => { + await window.electron.resumeGameDownload(shop, objectId); return updateLibrary(); }; - const removeGameInstaller = async (gameId: number) => { - dispatch(setGameDeleting(gameId)); + const removeGameInstaller = async (shop: GameShop, objectId: string) => { + dispatch(setGameDeleting(objectId)); try { - await window.electron.deleteGameFolder(gameId); + await window.electron.deleteGameFolder(shop, objectId); updateLibrary(); } finally { - dispatch(removeGameFromDeleting(gameId)); + dispatch(removeGameFromDeleting(objectId)); } }; - const cancelDownload = async (gameId: number) => { - await window.electron.cancelGameDownload(gameId); + const cancelDownload = async (shop: GameShop, objectId: string) => { + await window.electron.cancelGameDownload(shop, objectId); dispatch(clearDownload()); updateLibrary(); - removeGameInstaller(gameId); + removeGameInstaller(shop, objectId); }; - const removeGameFromLibrary = (gameId: number) => - window.electron.removeGameFromLibrary(gameId).then(() => { + const removeGameFromLibrary = (shop: GameShop, objectId: string) => + window.electron.removeGameFromLibrary(shop, objectId).then(() => { updateLibrary(); }); - const pauseSeeding = async (gameId: number) => { - await window.electron.pauseGameSeed(gameId); + const pauseSeeding = async (shop: GameShop, objectId: string) => { + await window.electron.pauseGameSeed(shop, objectId); await updateLibrary(); }; - const resumeSeeding = async (gameId: number) => { - await window.electron.resumeGameSeed(gameId); + const resumeSeeding = async (shop: GameShop, objectId: string) => { + await window.electron.resumeGameSeed(shop, objectId); await updateLibrary(); }; @@ -90,8 +94,8 @@ export function useDownload() { } }; - const isGameDeleting = (gameId: number) => { - return gamesWithDeletionInProgress.includes(gameId); + const isGameDeleting = (objectId: string) => { + return gamesWithDeletionInProgress.includes(objectId); }; return { diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 8820820b..8c71364d 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -33,7 +33,6 @@ export interface User { export interface Game { title: string; iconUrl: string | null; - status: GameStatus | null; playTimeInMilliseconds: number; lastTimePlayed: Date | null; objectId: string; @@ -58,6 +57,8 @@ export interface Download { lastTimePlayed: Date | null; fileSize: number; shouldSeed: boolean; + // TODO: Rename to DownloadStatus + status: GameStatus | null; timestamp: number; } From f1e0ba4dd659626ec61a8615d998e9892299a513 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Tue, 21 Jan 2025 03:48:46 +0000 Subject: [PATCH 10/23] feat: migrating user preferences --- package.json | 1 - src/main/data-source.ts | 11 - src/main/entity/index.ts | 1 - src/main/entity/user-preferences.entity.ts | 55 ----- .../events/catalogue/get-trending-games.ts | 13 +- src/main/events/helpers/get-downloads-path.ts | 15 +- .../events/library/get-game-by-object-id.ts | 11 +- src/main/events/library/get-library.ts | 6 +- .../library/open-game-installer-path.ts | 10 +- src/main/events/library/open-game.ts | 2 +- .../publish-new-repacks-notification.ts | 13 +- .../events/torrenting/cancel-game-download.ts | 6 +- .../events/torrenting/pause-game-download.ts | 24 ++- src/main/events/torrenting/pause-game-seed.ts | 2 +- .../events/torrenting/resume-game-download.ts | 50 ++--- .../events/torrenting/start-game-download.ts | 145 +++++++------ .../user-preferences/get-user-preferences.ts | 7 +- .../update-user-preferences.ts | 20 +- .../get-compared-unlocked-achievements.ts | 15 +- .../events/user/get-unlocked-achievements.ts | 14 +- src/main/index.ts | 15 +- src/main/level/sublevels/keys.ts | 3 + src/main/main.ts | 129 +++++++++++- src/main/repository.ts | 5 - .../achievements/get-game-achievement-data.ts | 13 +- .../achievements/merge-achievements.ts | 23 ++- .../services/download/download-manager.ts | 194 +++++++++--------- src/main/services/download/types.ts | 2 +- src/main/services/notifications/index.ts | 14 +- src/main/services/window-manager.ts | 36 ++-- src/preload/index.ts | 6 +- src/renderer/src/app.tsx | 2 +- .../components/bottom-panel/bottom-panel.tsx | 27 +-- .../src/components/sidebar/sidebar.tsx | 33 +-- .../game-details/game-details.context.tsx | 15 +- .../game-details.context.types.ts | 4 +- src/renderer/src/declaration.d.ts | 8 +- src/renderer/src/features/download-slice.ts | 10 +- src/renderer/src/features/library-slice.ts | 4 +- .../src/pages/downloads/download-group.tsx | 98 +++++---- .../src/pages/downloads/downloads.tsx | 41 ++-- .../game-details/hero/hero-panel-actions.tsx | 13 +- .../game-details/hero/hero-panel-playtime.tsx | 11 +- .../pages/game-details/hero/hero-panel.tsx | 12 +- .../modals/game-options-modal.tsx | 64 +++--- .../game-details/modals/repacks-modal.tsx | 4 +- .../profile/profile-hero/profile-hero.tsx | 2 +- .../src/pages/settings/settings-general.tsx | 53 +++-- src/types/download.types.ts | 20 +- src/types/game.types.ts | 9 - src/types/index.ts | 25 +-- src/types/level.types.ts | 26 ++- yarn.lock | 185 +---------------- 53 files changed, 737 insertions(+), 790 deletions(-) delete mode 100644 src/main/data-source.ts delete mode 100644 src/main/entity/index.ts delete mode 100644 src/main/entity/user-preferences.entity.ts delete mode 100644 src/main/repository.ts diff --git a/package.json b/package.json index 4630ad41..d019669f 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "sound-play": "^1.1.0", "sudo-prompt": "^9.2.1", "tar": "^7.4.3", - "typeorm": "^0.3.20", "user-agents": "^1.1.387", "yaml": "^2.6.1", "yup": "^1.5.0", diff --git a/src/main/data-source.ts b/src/main/data-source.ts deleted file mode 100644 index 69e54667..00000000 --- a/src/main/data-source.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { DataSource } from "typeorm"; -import { UserPreferences } from "@main/entity"; - -import { databasePath } from "./constants"; - -export const dataSource = new DataSource({ - type: "better-sqlite3", - entities: [UserPreferences], - synchronize: false, - database: databasePath, -}); diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts deleted file mode 100644 index ebf29400..00000000 --- a/src/main/entity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./user-preferences.entity"; diff --git a/src/main/entity/user-preferences.entity.ts b/src/main/entity/user-preferences.entity.ts deleted file mode 100644 index a850b42f..00000000 --- a/src/main/entity/user-preferences.entity.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; - -@Entity("user_preferences") -export class UserPreferences { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { nullable: true }) - downloadsPath: string | null; - - @Column("text", { default: "en" }) - language: string; - - @Column("text", { nullable: true }) - realDebridApiToken: string | null; - - @Column("boolean", { default: false }) - downloadNotificationsEnabled: boolean; - - @Column("boolean", { default: false }) - repackUpdatesNotificationsEnabled: boolean; - - @Column("boolean", { default: true }) - achievementNotificationsEnabled: boolean; - - @Column("boolean", { default: false }) - preferQuitInsteadOfHiding: boolean; - - @Column("boolean", { default: false }) - runAtStartup: boolean; - - @Column("boolean", { default: false }) - startMinimized: boolean; - - @Column("boolean", { default: false }) - disableNsfwAlert: boolean; - - @Column("boolean", { default: true }) - seedAfterDownloadComplete: boolean; - - @Column("boolean", { default: false }) - showHiddenAchievementsDescription: boolean; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/events/catalogue/get-trending-games.ts b/src/main/events/catalogue/get-trending-games.ts index acfebfd6..32072c10 100644 --- a/src/main/events/catalogue/get-trending-games.ts +++ b/src/main/events/catalogue/get-trending-games.ts @@ -1,14 +1,15 @@ +import { levelKeys } from "@main/level"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import { userPreferencesRepository } from "@main/repository"; import type { TrendingGame } from "@types"; +import { db } from "@main/level"; const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); - - const language = userPreferences?.language || "en"; + const language = await db + .get(levelKeys.language, { + valueEncoding: "utf-8", + }) + .then((language) => language || "en"); const trendingGames = await HydraApi.get( "/games/trending", diff --git a/src/main/events/helpers/get-downloads-path.ts b/src/main/events/helpers/get-downloads-path.ts index c78a0ede..0823109e 100644 --- a/src/main/events/helpers/get-downloads-path.ts +++ b/src/main/events/helpers/get-downloads-path.ts @@ -1,12 +1,15 @@ -import { userPreferencesRepository } from "@main/repository"; import { defaultDownloadsPath } from "@main/constants"; +import { levelKeys } from "@main/level"; +import type { UserPreferences } from "@types"; +import { db } from "@main/level"; export const getDownloadsPath = async () => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { - id: 1, - }, - }); + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); if (userPreferences && userPreferences.downloadsPath) return userPreferences.downloadsPath; 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 57e4a2bd..b835c5db 100644 --- a/src/main/events/library/get-game-by-object-id.ts +++ b/src/main/events/library/get-game-by-object-id.ts @@ -1,5 +1,5 @@ import { registerEvent } from "../register-event"; -import { levelKeys } from "@main/level"; +import { downloadsSublevel, levelKeys } from "@main/level"; import { gamesSublevel } from "@main/level"; import type { GameShop } from "@types"; @@ -9,9 +9,14 @@ const getGameByObjectId = async ( objectId: string ) => { const gameKey = levelKeys.game(shop, objectId); - const game = await gamesSublevel.get(gameKey); + const [game, download] = await Promise.all([ + gamesSublevel.get(gameKey), + downloadsSublevel.get(gameKey), + ]); - return game; + if (!game) return null; + + return { id: gameKey, ...game, download }; }; registerEvent("getGameByObjectId", getGameByObjectId); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index bdaabc87..86c0fd29 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -1,7 +1,8 @@ +import type { LibraryGame } from "@types"; import { registerEvent } from "../register-event"; import { downloadsSublevel, gamesSublevel } from "@main/level"; -const getLibrary = async () => { +const getLibrary = async (): Promise => { return gamesSublevel .iterator() .all() @@ -13,8 +14,9 @@ const getLibrary = async () => { const download = await downloadsSublevel.get(key); return { + id: key, ...game, - download, + download: download ?? null, }; }) ); diff --git a/src/main/events/library/open-game-installer-path.ts b/src/main/events/library/open-game-installer-path.ts index 971bd3ca..b61246fa 100644 --- a/src/main/events/library/open-game-installer-path.ts +++ b/src/main/events/library/open-game-installer-path.ts @@ -3,20 +3,20 @@ import path from "node:path"; import { getDownloadsPath } from "../helpers/get-downloads-path"; import { registerEvent } from "../register-event"; import { GameShop } from "@types"; -import { gamesSublevel, levelKeys } from "@main/level"; +import { downloadsSublevel, levelKeys } from "@main/level"; const openGameInstallerPath = async ( _event: Electron.IpcMainInvokeEvent, shop: GameShop, objectId: string ) => { - const game = await gamesSublevel.get(levelKeys.game(shop, objectId)); + const download = await downloadsSublevel.get(levelKeys.game(shop, objectId)); - if (!game || !game.folderName || !game.downloadPath) return true; + if (!download || !download.folderName || !download.downloadPath) return true; const gamePath = path.join( - game.downloadPath ?? (await getDownloadsPath()), - game.folderName! + download.downloadPath ?? (await getDownloadsPath()), + download.folderName! ); shell.showItemInFolder(gamePath); diff --git a/src/main/events/library/open-game.ts b/src/main/events/library/open-game.ts index f60cd200..2a74c5d4 100644 --- a/src/main/events/library/open-game.ts +++ b/src/main/events/library/open-game.ts @@ -10,7 +10,7 @@ const openGame = async ( shop: GameShop, objectId: string, executablePath: string, - launchOptions: string | null + launchOptions?: string | null ) => { // TODO: revisit this for launchOptions const parsedPath = parseExecutablePath(executablePath); diff --git a/src/main/events/notifications/publish-new-repacks-notification.ts b/src/main/events/notifications/publish-new-repacks-notification.ts index 5230c209..cb5c1f5f 100644 --- a/src/main/events/notifications/publish-new-repacks-notification.ts +++ b/src/main/events/notifications/publish-new-repacks-notification.ts @@ -1,7 +1,9 @@ import { Notification } from "electron"; import { registerEvent } from "../register-event"; -import { userPreferencesRepository } from "@main/repository"; import { t } from "i18next"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level"; +import type { UserPreferences } from "@types"; const publishNewRepacksNotification = async ( _event: Electron.IpcMainInvokeEvent, @@ -9,9 +11,12 @@ const publishNewRepacksNotification = async ( ) => { if (newRepacksCount < 1) return; - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); if (userPreferences?.repackUpdatesNotificationsEnabled) { new Notification({ diff --git a/src/main/events/torrenting/cancel-game-download.ts b/src/main/events/torrenting/cancel-game-download.ts index 20ba0820..5d80337f 100644 --- a/src/main/events/torrenting/cancel-game-download.ts +++ b/src/main/events/torrenting/cancel-game-download.ts @@ -9,9 +9,11 @@ const cancelGameDownload = async ( shop: GameShop, objectId: string ) => { - await DownloadManager.cancelDownload(shop, objectId); + const downloadKey = levelKeys.game(shop, objectId); - await downloadsSublevel.del(levelKeys.game(shop, objectId)); + await DownloadManager.cancelDownload(downloadKey); + + await downloadsSublevel.del(downloadKey); }; registerEvent("cancelGameDownload", cancelGameDownload); diff --git a/src/main/events/torrenting/pause-game-download.ts b/src/main/events/torrenting/pause-game-download.ts index 03bb2781..27b21943 100644 --- a/src/main/events/torrenting/pause-game-download.ts +++ b/src/main/events/torrenting/pause-game-download.ts @@ -1,24 +1,26 @@ import { registerEvent } from "../register-event"; import { DownloadManager } from "@main/services"; -import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game } from "@main/entity"; +import { GameShop } from "@types"; +import { downloadsSublevel, levelKeys } from "@main/level"; const pauseGameDownload = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - await dataSource.transaction(async (transactionalEntityManager) => { + const gameKey = levelKeys.game(shop, objectId); + + const download = await downloadsSublevel.get(gameKey); + + if (download) { await DownloadManager.pauseDownload(); - await transactionalEntityManager.getRepository(DownloadQueue).delete({ - game: { id: gameId }, + await downloadsSublevel.put(gameKey, { + ...download, + status: "paused", }); - - await transactionalEntityManager - .getRepository(Game) - .update({ id: gameId }, { status: "paused" }); - }); + } }; registerEvent("pauseGameDownload", pauseGameDownload); diff --git a/src/main/events/torrenting/pause-game-seed.ts b/src/main/events/torrenting/pause-game-seed.ts index 62dfca96..49aefc66 100644 --- a/src/main/events/torrenting/pause-game-seed.ts +++ b/src/main/events/torrenting/pause-game-seed.ts @@ -19,7 +19,7 @@ const pauseGameSeed = async ( shouldSeed: false, }); - await DownloadManager.pauseSeeding(download); + await DownloadManager.pauseSeeding(downloadKey); }; registerEvent("pauseGameSeed", pauseGameSeed); diff --git a/src/main/events/torrenting/resume-game-download.ts b/src/main/events/torrenting/resume-game-download.ts index 2327e929..c27aea84 100644 --- a/src/main/events/torrenting/resume-game-download.ts +++ b/src/main/events/torrenting/resume-game-download.ts @@ -1,44 +1,36 @@ -import { Not } from "typeorm"; - import { registerEvent } from "../register-event"; import { DownloadManager } from "@main/services"; -import { dataSource } from "@main/data-source"; +import { downloadsSublevel, levelKeys } from "@main/level"; +import { GameShop } from "@types"; const resumeGameDownload = async ( _event: Electron.IpcMainInvokeEvent, - gameId: number + shop: GameShop, + objectId: string ) => { - const game = await gameRepository.findOne({ - where: { - id: gameId, - isDeleted: false, - }, - }); + const gameKey = levelKeys.game(shop, objectId); - if (!game) return; + const download = await downloadsSublevel.get(gameKey); - if (game.status === "paused") { - await dataSource.transaction(async (transactionalEntityManager) => { - await DownloadManager.pauseDownload(); + if (download?.status === "paused") { + await DownloadManager.pauseDownload(); - await transactionalEntityManager - .getRepository(Game) - .update({ status: "active", progress: Not(1) }, { status: "paused" }); + for await (const [key, value] of downloadsSublevel.iterator()) { + if (value.status === "active" && value.progress !== 1) { + await downloadsSublevel.put(key, { + ...value, + status: "paused", + }); + } + } - await DownloadManager.resumeDownload(game); + await DownloadManager.resumeDownload(download); - await transactionalEntityManager - .getRepository(DownloadQueue) - .delete({ game: { id: gameId } }); - - await transactionalEntityManager - .getRepository(DownloadQueue) - .insert({ game: { id: gameId } }); - - await transactionalEntityManager - .getRepository(Game) - .update({ id: gameId }, { status: "active" }); + await downloadsSublevel.put(gameKey, { + ...download, + status: "active", + timestamp: Date.now(), }); } }; diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index de10b07d..6d818d55 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -1,13 +1,11 @@ import { registerEvent } from "../register-event"; -import type { StartGameDownloadPayload } from "@types"; +import type { Download, StartGameDownloadPayload } from "@types"; import { DownloadManager, HydraApi } from "@main/services"; -import { Not } from "typeorm"; import { steamGamesWorker } from "@main/workers"; import { createGame } from "@main/services/library-sync"; import { steamUrlBuilder } from "@shared"; -import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game } from "@main/entity"; +import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; const startGameDownload = async ( _event: Electron.IpcMainInvokeEvent, @@ -15,85 +13,84 @@ const startGameDownload = async ( ) => { const { objectId, title, shop, downloadPath, downloader, uri } = payload; - return dataSource.transaction(async (transactionalEntityManager) => { - const gameRepository = transactionalEntityManager.getRepository(Game); - const downloadQueueRepository = - transactionalEntityManager.getRepository(DownloadQueue); + const gameKey = levelKeys.game(shop, objectId); - const game = await gameRepository.findOne({ - where: { - objectID: objectId, - shop, - }, - }); + await DownloadManager.pauseDownload(); - await DownloadManager.pauseDownload(); - - await gameRepository.update( - { status: "active", progress: Not(1) }, - { status: "paused" } - ); - - if (game) { - await gameRepository.update( - { - id: game.id, - }, - { - status: "active", - progress: 0, - bytesDownloaded: 0, - downloadPath, - downloader, - uri, - isDeleted: false, - } - ); - } else { - const steamGame = await steamGamesWorker.run(Number(objectId), { - name: "getById", - }); - - const iconUrl = steamGame?.clientIcon - ? steamUrlBuilder.icon(objectId, steamGame.clientIcon) - : null; - - await gameRepository.insert({ - title, - iconUrl, - objectID: objectId, - downloader, - shop, - status: "active", - downloadPath, - uri, + for await (const [key, value] of downloadsSublevel.iterator()) { + if (value.status === "active" && value.progress !== 1) { + await downloadsSublevel.put(key, { + ...value, + status: "paused", }); } + } - const updatedGame = await gameRepository.findOne({ - where: { - objectID: objectId, - }, + const game = await gamesSublevel.get(gameKey); + + /* Delete any previous download */ + await downloadsSublevel.del(gameKey); + + if (game?.isDeleted) { + await gamesSublevel.put(gameKey, { + ...game, + isDeleted: false, + }); + } else { + const steamGame = await steamGamesWorker.run(Number(objectId), { + name: "getById", }); - await DownloadManager.cancelDownload(updatedGame!.id); - await DownloadManager.startDownload(updatedGame!); + const iconUrl = steamGame?.clientIcon + ? steamUrlBuilder.icon(objectId, steamGame.clientIcon) + : null; - await downloadQueueRepository.delete({ game: { id: updatedGame!.id } }); - await downloadQueueRepository.insert({ game: { id: updatedGame!.id } }); + await gamesSublevel.put(gameKey, { + title, + iconUrl, + objectId, + shop, + remoteId: null, + playTimeInMilliseconds: 0, + lastTimePlayed: null, + isDeleted: false, + }); + } - await Promise.all([ - createGame(updatedGame!).catch(() => {}), - HydraApi.post( - "/games/download", - { - objectId: updatedGame!.objectID, - shop: updatedGame!.shop, - }, - { needsAuth: false } - ).catch(() => {}), - ]); - }); + await DownloadManager.cancelDownload(gameKey); + + const download: Download = { + shop, + objectId, + status: "active", + progress: 0, + bytesDownloaded: 0, + downloadPath, + downloader, + uri, + folderName: null, + fileSize: null, + shouldSeed: false, + timestamp: Date.now(), + }; + + await downloadsSublevel.put(gameKey, download); + + await DownloadManager.startDownload(download); + + const updatedGame = await gamesSublevel.get(gameKey); + + await Promise.all([ + createGame(updatedGame!).catch(() => {}), + HydraApi.post( + "/games/download", + { + objectId, + shop, + }, + { needsAuth: false } + ).catch(() => {}), + ]); }; registerEvent("startGameDownload", startGameDownload); diff --git a/src/main/events/user-preferences/get-user-preferences.ts b/src/main/events/user-preferences/get-user-preferences.ts index 2a2df254..5a20b2b7 100644 --- a/src/main/events/user-preferences/get-user-preferences.ts +++ b/src/main/events/user-preferences/get-user-preferences.ts @@ -1,9 +1,10 @@ -import { userPreferencesRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { db, levelKeys } from "@main/level"; +import type { UserPreferences } from "@types"; const getUserPreferences = async () => - userPreferencesRepository.findOne({ - where: { id: 1 }, + db.get(levelKeys.userPreferences, { + valueEncoding: "json", }); registerEvent("getUserPreferences", getUserPreferences); diff --git a/src/main/events/user-preferences/update-user-preferences.ts b/src/main/events/user-preferences/update-user-preferences.ts index f45af519..6001a85c 100644 --- a/src/main/events/user-preferences/update-user-preferences.ts +++ b/src/main/events/user-preferences/update-user-preferences.ts @@ -1,23 +1,35 @@ -import { userPreferencesRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import type { UserPreferences } from "@types"; import i18next from "i18next"; +import { db, levelKeys } from "@main/level"; const updateUserPreferences = async ( _event: Electron.IpcMainInvokeEvent, preferences: Partial ) => { + const userPreferences = await db.get( + levelKeys.userPreferences, + { valueEncoding: "json" } + ); + if (preferences.language) { + await db.put(levelKeys.language, preferences.language, { + valueEncoding: "utf-8", + }); + i18next.changeLanguage(preferences.language); } - return userPreferencesRepository.upsert( + await db.put( + levelKeys.userPreferences, { - id: 1, + ...userPreferences, ...preferences, }, - ["id"] + { + valueEncoding: "json", + } ); }; diff --git a/src/main/events/user/get-compared-unlocked-achievements.ts b/src/main/events/user/get-compared-unlocked-achievements.ts index 0b665212..b2358199 100644 --- a/src/main/events/user/get-compared-unlocked-achievements.ts +++ b/src/main/events/user/get-compared-unlocked-achievements.ts @@ -1,7 +1,9 @@ -import type { ComparedAchievements, GameShop } from "@types"; +import type { ComparedAchievements, GameShop, UserPreferences } from "@types"; import { registerEvent } from "../register-event"; -import { userPreferencesRepository } from "@main/repository"; + import { HydraApi } from "@main/services"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level"; const getComparedUnlockedAchievements = async ( _event: Electron.IpcMainInvokeEvent, @@ -9,9 +11,12 @@ const getComparedUnlockedAchievements = async ( shop: GameShop, userId: string ) => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); const showHiddenAchievementsDescription = userPreferences?.showHiddenAchievementsDescription || false; diff --git a/src/main/events/user/get-unlocked-achievements.ts b/src/main/events/user/get-unlocked-achievements.ts index 78820a94..9cb44423 100644 --- a/src/main/events/user/get-unlocked-achievements.ts +++ b/src/main/events/user/get-unlocked-achievements.ts @@ -1,8 +1,7 @@ -import type { GameShop, UserAchievement } from "@types"; +import type { GameShop, UserAchievement, UserPreferences } from "@types"; import { registerEvent } from "../register-event"; -import { userPreferencesRepository } from "@main/repository"; import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; -import { gameAchievementsSublevel, levelKeys } from "@main/level"; +import { db, gameAchievementsSublevel, levelKeys } from "@main/level"; export const getUnlockedAchievements = async ( objectId: string, @@ -13,9 +12,12 @@ export const getUnlockedAchievements = async ( levelKeys.game(shop, objectId) ); - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); const showHiddenAchievementsDescription = userPreferences?.showHiddenAchievementsDescription || false; diff --git a/src/main/index.ts b/src/main/index.ts index a60c4569..0f7c0297 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,11 +5,10 @@ import path from "node:path"; import url from "node:url"; import { electronApp, optimizer } from "@electron-toolkit/utils"; import { logger, WindowManager } from "@main/services"; -import { dataSource } from "@main/data-source"; import resources from "@locales"; -import { userPreferencesRepository } from "@main/repository"; import { PythonRPC } from "./services/python-rpc"; import { Aria2 } from "./services/aria2"; +import { db, levelKeys } from "./level"; const { autoUpdater } = updater; @@ -58,23 +57,19 @@ app.whenReady().then(async () => { return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString()); }); - await dataSource.initialize(); - await import("./main"); - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, + const language = await db.get(levelKeys.language, { + valueEncoding: "utf-8", }); - if (userPreferences?.language) { - i18n.changeLanguage(userPreferences.language); - } + if (language) i18n.changeLanguage(language); if (!process.argv.includes("--hidden")) { WindowManager.createMainWindow(); } - WindowManager.createSystemTray(userPreferences?.language || "en"); + WindowManager.createSystemTray(language || "en"); }); app.on("browser-window-created", (_, window) => { diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts index 5a787f13..53eae44b 100644 --- a/src/main/level/sublevels/keys.ts +++ b/src/main/level/sublevels/keys.ts @@ -10,4 +10,7 @@ export const levelKeys = { `${shop}:${objectId}:${language}`, gameAchievements: "gameAchievements", downloads: "downloads", + userPreferences: "userPreferences", + language: "language", + sqliteMigrationDone: "sqliteMigrationDone", }; diff --git a/src/main/main.ts b/src/main/main.ts index 5f5ec768..4e7bb5ab 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,6 +1,4 @@ -import { DownloadManager, Ludusavi, startMainLoop } from "./services"; -import { userPreferencesRepository } from "./repository"; -import { UserPreferences } from "./entity"; +import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services"; import { RealDebridClient } from "./services/download/real-debrid"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; @@ -8,6 +6,10 @@ import { Aria2 } from "./services/aria2"; import { downloadsSublevel } from "./level/sublevels/downloads"; import { sortBy } from "lodash-es"; import { Downloader } from "@shared"; +import { gameAchievementsSublevel, gamesSublevel, levelKeys } from "./level"; +import { Auth, User, type UserPreferences } from "@types"; +import { db } from "./level"; +import { knexClient } from "./knex-client"; const loadState = async (userPreferences: UserPreferences | null) => { import("./events"); @@ -46,10 +48,121 @@ const loadState = async (userPreferences: UserPreferences | null) => { startMainLoop(); }; -userPreferencesRepository - .findOne({ - where: { id: 1 }, - }) - .then((userPreferences) => { +const migrateFromSqlite = async () => { + const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone); + + if (sqliteMigrationDone) { + return; + } + + const migrateGames = knexClient("game") + .select("*") + .then((games) => { + return gamesSublevel.batch( + games.map((game) => ({ + type: "put", + key: levelKeys.game(game.shop, game.objectID), + value: { + objectId: game.objectID, + shop: game.shop, + title: game.title, + iconUrl: game.iconUrl, + playTimeInMilliseconds: game.playTimeInMilliseconds, + lastTimePlayed: game.lastTimePlayed, + remoteId: game.remoteId, + isDeleted: game.isDeleted, + }, + })) + ); + }) + .then(() => { + logger.info("Games migrated successfully"); + }); + + const migrateUserPreferences = knexClient("user_preferences") + .select("*") + .then(async (userPreferences) => { + if (userPreferences.length > 0) { + await db.put(levelKeys.userPreferences, userPreferences[0]); + + if (userPreferences[0].language) { + await db.put(levelKeys.language, userPreferences[0].language); + } + } + }) + .then(() => { + logger.info("User preferences migrated successfully"); + }); + + const migrateAchievements = knexClient("game_achievement") + .select("*") + .then((achievements) => { + return gameAchievementsSublevel.batch( + achievements.map((achievement) => ({ + type: "put", + key: levelKeys.game(achievement.shop, achievement.objectId), + value: { + achievements: JSON.parse(achievement.achievements), + unlockedAchievements: JSON.parse(achievement.unlockedAchievements), + }, + })) + ); + }) + .then(() => { + logger.info("Achievements migrated successfully"); + }); + + const migrateUser = knexClient("user_auth") + .select("*") + .then(async (users) => { + if (users.length > 0) { + await db.put( + levelKeys.user, + { + id: users[0].userId, + displayName: users[0].displayName, + profileImageUrl: users[0].profileImageUrl, + backgroundImageUrl: users[0].backgroundImageUrl, + subscription: users[0].subscription, + }, + { + valueEncoding: "json", + } + ); + + await db.put( + levelKeys.auth, + { + accessToken: users[0].accessToken, + refreshToken: users[0].refreshToken, + tokenExpirationTimestamp: users[0].tokenExpirationTimestamp, + }, + { + valueEncoding: "json", + } + ); + } + }) + .then(() => { + logger.info("User data migrated successfully"); + }); + + return Promise.all([ + migrateGames, + migrateUserPreferences, + migrateAchievements, + migrateUser, + ]); +}; + +migrateFromSqlite().then(async () => { + await db.put(levelKeys.sqliteMigrationDone, true, { + valueEncoding: "json", + }); + + db.get(levelKeys.userPreferences, { + valueEncoding: "json", + }).then((userPreferences) => { loadState(userPreferences); }); +}); diff --git a/src/main/repository.ts b/src/main/repository.ts deleted file mode 100644 index 1a6975a2..00000000 --- a/src/main/repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { dataSource } from "./data-source"; -import { UserPreferences } from "@main/entity"; - -export const userPreferencesRepository = - dataSource.getRepository(UserPreferences); diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index 2dc643c1..4c03c7e1 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -1,9 +1,8 @@ -import { userPreferencesRepository } from "@main/repository"; import { HydraApi } from "../hydra-api"; import type { GameShop, SteamAchievement } from "@types"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; -import { gameAchievementsSublevel, levelKeys } from "@main/level"; +import { db, gameAchievementsSublevel, levelKeys } from "@main/level"; export const getGameAchievementData = async ( objectId: string, @@ -17,14 +16,16 @@ export const getGameAchievementData = async ( if (cachedAchievements && useCachedData) return cachedAchievements.achievements; - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); + const language = await db + .get(levelKeys.language, { + valueEncoding: "utf-8", + }) + .then((language) => language || "en"); return HydraApi.get("/games/achievements", { shop, objectId, - language: userPreferences?.language || "en", + language, }) .then(async (achievements) => { await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), { diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index ac2f69d1..a2541d5e 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -1,13 +1,16 @@ -import { userPreferencesRepository } from "@main/repository"; -import type { GameShop, UnlockedAchievement } from "@types"; +import type { + Game, + GameShop, + UnlockedAchievement, + UserPreferences, +} from "@types"; import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; -import { Game } from "@main/entity"; import { publishNewAchievementNotification } from "../notifications"; import { SubscriptionRequiredError } from "@shared"; import { achievementsLogger } from "../logger"; -import { gameAchievementsSublevel, levelKeys } from "@main/level"; +import { db, gameAchievementsSublevel, levelKeys } from "@main/level"; const saveAchievementsOnLocal = async ( objectId: string, @@ -46,8 +49,10 @@ export const mergeAchievements = async ( publishNotification: boolean ) => { const [localGameAchievement, userPreferences] = await Promise.all([ - gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectID)), - userPreferencesRepository.findOne({ where: { id: 1 } }), + gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectId)), + db.get(levelKeys.userPreferences, { + valueEncoding: "json", + }), ]); const achievementsData = localGameAchievement?.achievements ?? []; @@ -131,13 +136,13 @@ export const mergeAchievements = async ( if (err! instanceof SubscriptionRequiredError) { achievementsLogger.log( "Achievements not synchronized on API due to lack of subscription", - game.objectID, + game.objectId, game.title ); } return saveAchievementsOnLocal( - game.objectID, + game.objectId, game.shop, mergedLocalAchievements, publishNotification @@ -145,7 +150,7 @@ export const mergeAchievements = async ( }); } else { await saveAchievementsOnLocal( - game.objectID, + game.objectId, game.shop, mergedLocalAchievements, publishNotification diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 9f726ad9..45e4bab5 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -1,8 +1,7 @@ import { Downloader } from "@shared"; import { WindowManager } from "../window-manager"; -import { userPreferencesRepository } from "@main/repository"; import { publishDownloadCompleteNotification } from "../notifications"; -import type { Download, DownloadProgress } from "@types"; +import type { Download, DownloadProgress, UserPreferences } from "@types"; import { GofileApi, QiwiApi, DatanodesApi } from "../hosters"; import { PythonRPC } from "../python-rpc"; import { @@ -11,11 +10,11 @@ import { PauseDownloadPayload, } from "./types"; import { calculateETA, getDirSize } from "./helpers"; -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"; +import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; +import { sortBy } from "lodash-es"; export class DownloadManager { private static downloadingGameId: string | null = null; @@ -44,10 +43,8 @@ export class DownloadManager { const response = await PythonRPC.rpc.get( "/status" ); - if (response.data === null || !this.downloadingGameId) return null; - - const gameId = this.downloadingGameId; + const downloadId = this.downloadingGameId; try { const { @@ -63,24 +60,21 @@ export class DownloadManager { const isDownloadingMetadata = status === LibtorrentStatus.DownloadingMetadata; - const isCheckingFiles = status === LibtorrentStatus.CheckingFiles; + const download = await downloadsSublevel.get(downloadId); + if (!isDownloadingMetadata && !isCheckingFiles) { - const update: QueryDeepPartialEntity = { + if (!download) return null; + + await downloadsSublevel.put(downloadId, { + ...download, bytesDownloaded, fileSize, progress, + folderName, status: "active", - }; - - await gameRepository.update( - { id: gameId }, - { - ...update, - folderName, - } - ); + }); } return { @@ -91,7 +85,8 @@ export class DownloadManager { isDownloadingMetadata, isCheckingFiles, progress, - gameId, + gameId: downloadId, + download, } as DownloadProgress; } catch (err) { return null; @@ -103,15 +98,22 @@ export class DownloadManager { if (status) { const { gameId, progress } = status; - const game = await gameRepository.findOne({ - where: { id: gameId, isDeleted: false }, - }); - const userPreferences = await userPreferencesRepository.findOneBy({ - id: 1, - }); + const [download, game] = await Promise.all([ + downloadsSublevel.get(gameId), + gamesSublevel.get(gameId), + ]); - if (WindowManager.mainWindow && game) { + if (!download || !game) return; + + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); + + if (WindowManager.mainWindow && download) { WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress); WindowManager.mainWindow.webContents.send( "on-download-progress", @@ -123,40 +125,42 @@ export class DownloadManager { ) ); } - if (progress === 1 && game) { + + if (progress === 1 && download) { publishDownloadCompleteNotification(game); if ( userPreferences?.seedAfterDownloadComplete && - game.downloader === Downloader.Torrent + download.downloader === Downloader.Torrent ) { - gameRepository.update( - { id: gameId }, - { status: "seeding", shouldSeed: true } - ); + downloadsSublevel.put(gameId, { + ...download, + status: "seeding", + shouldSeed: true, + }); } else { - gameRepository.update( - { id: gameId }, - { status: "complete", shouldSeed: false } - ); + downloadsSublevel.put(gameId, { + ...download, + status: "complete", + shouldSeed: false, + }); this.cancelDownload(gameId); } - await downloadsSublevel.del(levelKeys.game(game.shop, game.objectId)); + const downloads = await downloadsSublevel + .values() + .all() + .then((games) => { + return sortBy(games, "timestamp", "DESC"); + }); - const [nextQueueItem] = await downloadQueueRepository.find({ - order: { - id: "DESC", - }, - relations: { - game: true, - }, - }); - if (nextQueueItem) { - this.resumeDownload(nextQueueItem.game); + const [nextItemOnQueue] = downloads; + + if (nextItemOnQueue) { + this.resumeDownload(nextItemOnQueue); } else { - this.downloadingGameId = -1; + this.downloadingGameId = null; } } } @@ -172,20 +176,19 @@ export class DownloadManager { logger.log(seedStatus); seedStatus.forEach(async (status) => { - const game = await gameRepository.findOne({ - where: { id: status.gameId }, - }); + const download = await downloadsSublevel.get(status.gameId); - if (!game) return; + if (!download) return; const totalSize = await getDirSize( - path.join(game.downloadPath!, status.folderName) + path.join(download.downloadPath!, status.folderName) ); if (totalSize < status.fileSize) { - await this.cancelDownload(game.id); + await this.cancelDownload(status.gameId); - await gameRepository.update(game.id, { + await downloadsSublevel.put(status.gameId, { + ...download, status: "paused", shouldSeed: false, progress: totalSize / status.fileSize, @@ -207,114 +210,109 @@ export class DownloadManager { .catch(() => {}); WindowManager.mainWindow?.setProgressBar(-1); - this.downloadingGameId = null; } - static async resumeDownload(game: Game) { - return this.startDownload(game); + static async resumeDownload(download: Download) { + return this.startDownload(download); } - static async cancelDownload(gameId = this.downloadingGameId!) { + static async cancelDownload(downloadKey = this.downloadingGameId!) { await PythonRPC.rpc.post("/action", { action: "cancel", - game_id: gameId, + game_id: downloadKey, }); WindowManager.mainWindow?.setProgressBar(-1); - - if (gameId === this.downloadingGameId) { + if (downloadKey === this.downloadingGameId) { this.downloadingGameId = null; } } - static async resumeSeeding(game: Game) { + static async resumeSeeding(download: Download) { await PythonRPC.rpc.post("/action", { action: "resume_seeding", - game_id: game.id, - url: game.uri, - save_path: game.downloadPath, + game_id: levelKeys.game(download.shop, download.objectId), + url: download.uri, + save_path: download.downloadPath, }); } - static async pauseSeeding(gameId: number) { + static async pauseSeeding(downloadKey: string) { await PythonRPC.rpc.post("/action", { action: "pause_seeding", - game_id: gameId, + game_id: downloadKey, }); } - private static async getDownloadPayload(game: Game) { - switch (game.downloader) { - case Downloader.Gofile: { - const id = game.uri!.split("/").pop(); + private static async getDownloadPayload(download: Download) { + const downloadId = levelKeys.game(download.shop, download.objectId); + switch (download.downloader) { + case Downloader.Gofile: { + const id = download.uri!.split("/").pop(); const token = await GofileApi.authorize(); const downloadLink = await GofileApi.getDownloadLink(id!); return { action: "start", - game_id: game.id, + game_id: downloadId, url: downloadLink, - save_path: game.downloadPath!, + save_path: download.downloadPath!, header: `Cookie: accountToken=${token}`, }; } case Downloader.PixelDrain: { - const id = game.uri!.split("/").pop(); - + const id = download.uri!.split("/").pop(); return { action: "start", - game_id: game.id, + game_id: downloadId, url: `https://pixeldrain.com/api/file/${id}?download`, - save_path: game.downloadPath!, + save_path: download.downloadPath!, }; } case Downloader.Qiwi: { - const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!); - + const downloadUrl = await QiwiApi.getDownloadUrl(download.uri!); return { action: "start", - game_id: game.id, + game_id: downloadId, url: downloadUrl, - save_path: game.downloadPath!, + save_path: download.downloadPath!, }; } case Downloader.Datanodes: { - const downloadUrl = await DatanodesApi.getDownloadUrl(game.uri!); - + const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri!); return { action: "start", - game_id: game.id, + game_id: downloadId, url: downloadUrl, - save_path: game.downloadPath!, + save_path: download.downloadPath!, }; } case Downloader.Torrent: return { action: "start", - game_id: game.id, - url: game.uri!, - save_path: game.downloadPath!, + game_id: downloadId, + url: download.uri!, + save_path: download.downloadPath!, }; case Downloader.RealDebrid: { - const downloadUrl = await RealDebridClient.getDownloadUrl(game.uri!); - + const downloadUrl = await RealDebridClient.getDownloadUrl( + download.uri! + ); return { action: "start", - game_id: game.id, + game_id: downloadId, url: downloadUrl!, - save_path: game.downloadPath!, + save_path: download.downloadPath!, }; } } } - static async startDownload(game: Game) { - const payload = await this.getDownloadPayload(game); - + static async startDownload(download: Download) { + const payload = await this.getDownloadPayload(download); await PythonRPC.rpc.post("/action", payload); - - this.downloadingGameId = game.id; + this.downloadingGameId = levelKeys.game(download.shop, download.objectId); } } diff --git a/src/main/services/download/types.ts b/src/main/services/download/types.ts index bbd3efc5..0e868318 100644 --- a/src/main/services/download/types.ts +++ b/src/main/services/download/types.ts @@ -24,7 +24,7 @@ export interface LibtorrentPayload { fileSize: number; folderName: string; status: LibtorrentStatus; - gameId: number; + gameId: string; } export interface ProcessPayload { diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts index 23230589..c0098ddd 100644 --- a/src/main/services/notifications/index.ts +++ b/src/main/services/notifications/index.ts @@ -1,7 +1,6 @@ import { Notification, app } from "electron"; import { t } from "i18next"; import trayIcon from "@resources/tray-icon.png?asset"; -import { userPreferencesRepository } from "@main/repository"; import fs from "node:fs"; import axios from "axios"; import path from "node:path"; @@ -10,7 +9,9 @@ import { achievementSoundPath } from "@main/constants"; import icon from "@resources/icon.png?asset"; import { NotificationOptions, toXmlString } from "./xml"; import { logger } from "../logger"; -import type { Game } from "@types"; +import type { Game, UserPreferences } from "@types"; +import { levelKeys } from "@main/level"; +import { db } from "@main/level"; async function downloadImage(url: string | null) { if (!url) return undefined; @@ -38,9 +39,12 @@ async function downloadImage(url: string | null) { } export const publishDownloadCompleteNotification = async (game: Game) => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); if (userPreferences?.downloadNotificationsEnabled) { new Notification({ diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index e7a6db2b..df8b08a3 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -13,11 +13,11 @@ import i18next, { t } from "i18next"; import path from "node:path"; import icon from "@resources/icon.png?asset"; import trayIcon from "@resources/tray-icon.png?asset"; -import { userPreferencesRepository } from "@main/repository"; import { HydraApi } from "./hydra-api"; import UserAgent from "user-agents"; -import { gamesSublevel } from "@main/level"; +import { db, gamesSublevel, levelKeys } from "@main/level"; import { slice, sortBy } from "lodash-es"; +import type { UserPreferences } from "@types"; export class WindowManager { public static mainWindow: Electron.BrowserWindow | null = null; @@ -131,9 +131,12 @@ export class WindowManager { }); this.mainWindow.on("close", async () => { - const userPreferences = await userPreferencesRepository.findOne({ - where: { id: 1 }, - }); + const userPreferences = await db.get( + levelKeys.userPreferences, + { + valueEncoding: "json", + } + ); if (userPreferences?.preferQuitInsteadOfHiding) { app.quit(); @@ -211,19 +214,16 @@ export class WindowManager { const games = await gamesSublevel .values() .all() - .then((games) => - slice( - sortBy( - games.filter( - (game) => - !game.isDeleted && game.executablePath && game.lastTimePlayed - ), - "lastTimePlayed", - "DESC" - ), - 5 - ) - ); + .then((games) => { + const filteredGames = games.filter( + (game) => + !game.isDeleted && game.executablePath && game.lastTimePlayed + ); + + const sortedGames = sortBy(filteredGames, "lastTimePlayed", "DESC"); + + return slice(sortedGames, 5); + }); const recentlyPlayedGames: Array = games.map(({ title, executablePath }) => ({ diff --git a/src/preload/index.ts b/src/preload/index.ts index cda910b3..3c1e9e83 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -132,7 +132,7 @@ contextBridge.exposeInMainWorld("electron", { shop: GameShop, objectId: string, executablePath: string, - launchOptions: string | null + launchOptions?: string | null ) => ipcRenderer.invoke( "openGame", @@ -149,8 +149,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("removeGame", shop, objectId), deleteGameFolder: (shop: GameShop, objectId: string) => ipcRenderer.invoke("deleteGameFolder", shop, objectId), - getGameByObjectId: (objectId: string) => - ipcRenderer.invoke("getGameByObjectId", objectId), + getGameByObjectId: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("getGameByObjectId", shop, objectId), resetGameAchievements: (shop: GameShop, objectId: string) => ipcRenderer.invoke("resetGameAchievements", shop, objectId), onGamesRunning: ( diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 5fefe90c..b03fb540 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -84,7 +84,7 @@ export function App() { useEffect(() => { const unsubscribe = window.electron.onDownloadProgress( (downloadProgress) => { - if (downloadProgress.game.progress === 1) { + if (downloadProgress.progress === 1) { clearDownload(); updateLibrary(); return; diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index bf8f71d5..9f9f32c1 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useDownload, useUserDetails } from "@renderer/hooks"; +import { useDownload, useLibrary, useUserDetails } from "@renderer/hooks"; import "./bottom-panel.scss"; @@ -15,9 +15,11 @@ export function BottomPanel() { const { userDetails } = useUserDetails(); + const { library } = useLibrary(); + const { lastPacket, progress, downloadSpeed, eta } = useDownload(); - const isGameDownloading = !!lastPacket?.game; + const isGameDownloading = !!lastPacket; const [version, setVersion] = useState(""); const [sessionHash, setSessionHash] = useState(""); @@ -32,27 +34,29 @@ export function BottomPanel() { const status = useMemo(() => { if (isGameDownloading) { + const game = library.find((game) => game.id === lastPacket?.gameId)!; + if (lastPacket?.isCheckingFiles) return t("checking_files", { - title: lastPacket?.game.title, + title: game.title, percentage: progress, }); if (lastPacket?.isDownloadingMetadata) return t("downloading_metadata", { - title: lastPacket?.game.title, + title: game.title, percentage: progress, }); if (!eta) { return t("calculating_eta", { - title: lastPacket?.game.title, + title: game.title, percentage: progress, }); } return t("downloading", { - title: lastPacket?.game.title, + title: game.title, percentage: progress, eta, speed: downloadSpeed, @@ -60,16 +64,7 @@ export function BottomPanel() { } return t("no_downloads_in_progress"); - }, [ - t, - isGameDownloading, - lastPacket?.game, - lastPacket?.isDownloadingMetadata, - lastPacket?.isCheckingFiles, - progress, - eta, - downloadSpeed, - ]); + }, [t, isGameDownloading, library, lastPacket, progress, eta, downloadSpeed]); return (