From 9bada771df3a0027946de57f1d59c869485c00bc Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Sat, 11 Oct 2025 11:26:05 -0300
Subject: [PATCH 1/3] feat: separate game assets from game stats
---
src/main/events/catalogue/get-game-assets.ts | 51 +++++++++++++++++++
.../events/catalogue/save-game-shop-assets.ts | 25 ---------
.../library/add-custom-game-to-library.ts | 1 +
.../events/library/create-steam-shortcut.ts | 10 ++--
src/main/level/sublevels/game-shop-assets.ts | 12 ++---
.../library-sync/merge-with-remote-games.ts | 4 ++
src/main/services/notifications/index.ts | 7 ++-
.../services/ws/events/friend-game-session.ts | 11 ++--
src/preload/index.ts | 5 +-
.../game-details/game-details.context.tsx | 40 +++++++--------
src/renderer/src/declaration.d.ts | 10 ++--
.../pages/game-details/sidebar/sidebar.tsx | 13 ++++-
.../user-library-game-card.tsx | 2 +-
src/types/index.ts | 4 +-
14 files changed, 111 insertions(+), 84 deletions(-)
create mode 100644 src/main/events/catalogue/get-game-assets.ts
delete mode 100644 src/main/events/catalogue/save-game-shop-assets.ts
diff --git a/src/main/events/catalogue/get-game-assets.ts b/src/main/events/catalogue/get-game-assets.ts
new file mode 100644
index 00000000..04a03808
--- /dev/null
+++ b/src/main/events/catalogue/get-game-assets.ts
@@ -0,0 +1,51 @@
+import type { GameShop, ShopAssets } from "@types";
+import { registerEvent } from "../register-event";
+import { HydraApi } from "@main/services";
+import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
+
+const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes
+
+export const getGameAssets = async (objectId: string, shop: GameShop) => {
+ const cachedAssets = await gamesShopAssetsSublevel.get(
+ levelKeys.game(shop, objectId)
+ );
+
+ if (
+ cachedAssets &&
+ cachedAssets.updatedAt + LOCAL_CACHE_EXPIRATION > Date.now()
+ ) {
+ return cachedAssets;
+ }
+
+ return HydraApi.get(
+ `/games/${shop}/${objectId}/assets`,
+ null,
+ {
+ needsAuth: false,
+ }
+ ).then(async (assets) => {
+ if (!assets) return null;
+
+ // Preserve existing title if it differs from the incoming title (indicating it was customized)
+ const shouldPreserveTitle =
+ cachedAssets?.title && cachedAssets.title !== assets.title;
+
+ await gamesShopAssetsSublevel.put(levelKeys.game(shop, objectId), {
+ ...assets,
+ title: shouldPreserveTitle ? cachedAssets.title : assets.title,
+ updatedAt: Date.now(),
+ });
+
+ return assets;
+ });
+};
+
+const getGameAssetsEvent = async (
+ _event: Electron.IpcMainInvokeEvent,
+ objectId: string,
+ shop: GameShop
+) => {
+ return getGameAssets(objectId, shop);
+};
+
+registerEvent("getGameAssets", getGameAssetsEvent);
diff --git a/src/main/events/catalogue/save-game-shop-assets.ts b/src/main/events/catalogue/save-game-shop-assets.ts
deleted file mode 100644
index bf5f8b81..00000000
--- a/src/main/events/catalogue/save-game-shop-assets.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { GameShop, ShopAssets } from "@types";
-import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
-import { registerEvent } from "../register-event";
-
-const saveGameShopAssets = async (
- _event: Electron.IpcMainInvokeEvent,
- objectId: string,
- shop: GameShop,
- assets: ShopAssets
-): Promise => {
- const key = levelKeys.game(shop, objectId);
- const existingAssets = await gamesShopAssetsSublevel.get(key);
-
- // Preserve existing title if it differs from the incoming title (indicating it was customized)
- const shouldPreserveTitle =
- existingAssets?.title && existingAssets.title !== assets.title;
-
- return gamesShopAssetsSublevel.put(key, {
- ...existingAssets,
- ...assets,
- title: shouldPreserveTitle ? existingAssets.title : assets.title,
- });
-};
-
-registerEvent("saveGameShopAssets", saveGameShopAssets);
diff --git a/src/main/events/library/add-custom-game-to-library.ts b/src/main/events/library/add-custom-game-to-library.ts
index 47fd3436..f85c008b 100644
--- a/src/main/events/library/add-custom-game-to-library.ts
+++ b/src/main/events/library/add-custom-game-to-library.ts
@@ -27,6 +27,7 @@ const addCustomGameToLibrary = async (
}
const assets = {
+ updatedAt: Date.now(),
objectId,
shop,
title,
diff --git a/src/main/events/library/create-steam-shortcut.ts b/src/main/events/library/create-steam-shortcut.ts
index f83dd675..d5434d7f 100644
--- a/src/main/events/library/create-steam-shortcut.ts
+++ b/src/main/events/library/create-steam-shortcut.ts
@@ -1,12 +1,11 @@
import { registerEvent } from "../register-event";
-import type { GameShop, GameStats } from "@types";
+import type { GameShop, ShopAssets } from "@types";
import { gamesSublevel, levelKeys } from "@main/level";
import {
composeSteamShortcut,
getSteamLocation,
getSteamShortcuts,
getSteamUsersIds,
- HydraApi,
logger,
SystemPath,
writeSteamShortcuts,
@@ -15,6 +14,7 @@ import fs from "node:fs";
import axios from "axios";
import path from "node:path";
import { ASSETS_PATH } from "@main/constants";
+import { getGameAssets } from "../catalogue/get-game-assets";
const downloadAsset = async (downloadPath: string, url?: string | null) => {
try {
@@ -41,7 +41,7 @@ const downloadAsset = async (downloadPath: string, url?: string | null) => {
const downloadAssetsFromSteam = async (
shop: GameShop,
objectId: string,
- assets: GameStats["assets"]
+ assets: ShopAssets | null
) => {
const gameAssetsPath = path.join(ASSETS_PATH, `${shop}-${objectId}`);
@@ -86,9 +86,7 @@ const createSteamShortcut = async (
throw new Error("No executable path found for game");
}
- const { assets } = await HydraApi.get(
- `/games/${shop}/${objectId}/stats`
- );
+ const assets = await getGameAssets(objectId, shop);
const steamUserIds = await getSteamUsersIds();
diff --git a/src/main/level/sublevels/game-shop-assets.ts b/src/main/level/sublevels/game-shop-assets.ts
index 561d85df..806e041f 100644
--- a/src/main/level/sublevels/game-shop-assets.ts
+++ b/src/main/level/sublevels/game-shop-assets.ts
@@ -3,9 +3,9 @@ import type { ShopAssets } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
-export const gamesShopAssetsSublevel = db.sublevel(
- levelKeys.gameShopAssets,
- {
- valueEncoding: "json",
- }
-);
+export const gamesShopAssetsSublevel = db.sublevel<
+ string,
+ ShopAssets & { updatedAt: number }
+>(levelKeys.gameShopAssets, {
+ valueEncoding: "json",
+});
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 68b4b835..f7ea2744 100644
--- a/src/main/services/library-sync/merge-with-remote-games.ts
+++ b/src/main/services/library-sync/merge-with-remote-games.ts
@@ -58,7 +58,11 @@ export const mergeWithRemoteGames = async () => {
});
}
+ const localGameShopAsset = await gamesShopAssetsSublevel.get(gameKey);
+
await gamesShopAssetsSublevel.put(gameKey, {
+ updatedAt: Date.now(),
+ ...localGameShopAsset,
shop: game.shop,
objectId: game.objectId,
title: localGame?.title || game.title, // Preserve local title if it exists
diff --git a/src/main/services/notifications/index.ts b/src/main/services/notifications/index.ts
index fa9ac593..d28c3cd7 100644
--- a/src/main/services/notifications/index.ts
+++ b/src/main/services/notifications/index.ts
@@ -10,7 +10,7 @@ import icon from "@resources/icon.png?asset";
import { NotificationOptions, toXmlString } from "./xml";
import { logger } from "../logger";
import { WindowManager } from "../window-manager";
-import type { Game, GameStats, UserPreferences, UserProfile } from "@types";
+import type { Game, UserPreferences, UserProfile } from "@types";
import { db, levelKeys } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
import { SystemPath } from "../system-path";
@@ -108,15 +108,14 @@ export const publishNewFriendRequestNotification = async (
};
export const publishFriendStartedPlayingGameNotification = async (
- friend: UserProfile,
- game: GameStats
+ friend: UserProfile
) => {
new Notification({
title: t("friend_started_playing_game", {
ns: "notifications",
displayName: friend.displayName,
}),
- body: game.assets?.title,
+ body: friend?.currentGame?.title,
icon: friend?.profileImageUrl
? await downloadImage(friend.profileImageUrl)
: trayIcon,
diff --git a/src/main/services/ws/events/friend-game-session.ts b/src/main/services/ws/events/friend-game-session.ts
index 67967b3c..b089c421 100644
--- a/src/main/services/ws/events/friend-game-session.ts
+++ b/src/main/services/ws/events/friend-game-session.ts
@@ -2,7 +2,7 @@ import type { FriendGameSession } from "@main/generated/envelope";
import { db, levelKeys } from "@main/level";
import { HydraApi } from "@main/services/hydra-api";
import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications";
-import type { GameStats, UserPreferences, UserProfile } from "@types";
+import type { UserPreferences, UserProfile } from "@types";
export const friendGameSessionEvent = async (payload: FriendGameSession) => {
const userPreferences = await db.get(
@@ -14,12 +14,9 @@ export const friendGameSessionEvent = async (payload: FriendGameSession) => {
if (userPreferences?.friendStartGameNotificationsEnabled === false) return;
- const [friend, gameStats] = await Promise.all([
- HydraApi.get(`/users/${payload.friendId}`),
- HydraApi.get(`/games/steam/${payload.objectId}/stats`),
- ]).catch(() => [null, null]);
+ const friend = await HydraApi.get(`/users/${payload.friendId}`);
- if (friend && gameStats) {
- publishFriendStartedPlayingGameNotification(friend, gameStats);
+ if (friend) {
+ publishFriendStartedPlayingGameNotification(friend);
}
};
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 813758f0..e26909d4 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -17,7 +17,6 @@ import type {
Theme,
FriendRequestSync,
ShortcutLocation,
- ShopAssets,
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
} from "@types";
@@ -67,8 +66,6 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("searchGames", payload, take, skip),
getCatalogue: (category: CatalogueCategory) =>
ipcRenderer.invoke("getCatalogue", category),
- saveGameShopAssets: (objectId: string, shop: GameShop, assets: ShopAssets) =>
- ipcRenderer.invoke("saveGameShopAssets", objectId, shop, assets),
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
@@ -76,6 +73,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getHowLongToBeat", objectId, shop),
getGameStats: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getGameStats", objectId, shop),
+ getGameAssets: (objectId: string, shop: GameShop) =>
+ ipcRenderer.invoke("getGameAssets", objectId, shop),
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
createGameReview: (
shop: GameShop,
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 5be5cf98..778fa3fe 100644
--- a/src/renderer/src/context/game-details/game-details.context.tsx
+++ b/src/renderer/src/context/game-details/game-details.context.tsx
@@ -142,29 +142,23 @@ export function GameDetailsContextProvider({
}
});
- const statsPromise = window.electron
- .getGameStats(objectId, shop)
- .then((result) => {
- if (abortController.signal.aborted) return null;
- setStats(result);
- return result;
- });
+ window.electron.getGameStats(objectId, shop).then((result) => {
+ if (abortController.signal.aborted) return;
+ setStats(result);
+ });
- Promise.all([shopDetailsPromise, statsPromise])
- .then(([_, stats]) => {
- if (stats) {
- const assets = stats.assets;
- if (assets) {
- window.electron.saveGameShopAssets(objectId, shop, assets);
+ const assetsPromise = window.electron.getGameAssets(objectId, shop);
- setShopDetails((prev) => {
- if (!prev) return null;
- return {
- ...prev,
- assets,
- };
- });
- }
+ Promise.all([shopDetailsPromise, assetsPromise])
+ .then(([_, assets]) => {
+ if (assets) {
+ setShopDetails((prev) => {
+ if (!prev) return null;
+ return {
+ ...prev,
+ assets,
+ };
+ });
}
})
.finally(() => {
@@ -207,8 +201,8 @@ export function GameDetailsContextProvider({
setShowRepacksModal(true);
try {
window.history.replaceState({}, document.title, location.pathname);
- } catch (_e) {
- void _e;
+ } catch (e) {
+ console.error(e);
}
}
}, [location]);
diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts
index 82bbeb28..9f9e4177 100644
--- a/src/renderer/src/declaration.d.ts
+++ b/src/renderer/src/declaration.d.ts
@@ -39,6 +39,7 @@ import type {
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
UserLibraryResponse,
+ Game,
} from "@types";
import type { AxiosProgressEvent } from "axios";
import type disk from "diskusage";
@@ -77,11 +78,6 @@ declare global {
skip: number
) => Promise<{ edges: CatalogueSearchResult[]; count: number }>;
getCatalogue: (category: CatalogueCategory) => Promise;
- saveGameShopAssets: (
- objectId: string,
- shop: GameShop,
- assets: ShopAssets
- ) => Promise;
getGameShopDetails: (
objectId: string,
shop: GameShop,
@@ -93,6 +89,10 @@ declare global {
shop: GameShop
) => Promise;
getGameStats: (objectId: string, shop: GameShop) => Promise;
+ getGameAssets: (
+ objectId: string,
+ shop: GameShop
+ ) => Promise;
getTrendingGames: () => Promise;
createGameReview: (
shop: GameShop,
diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
index 33009508..df1429ec 100755
--- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
+++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
@@ -233,9 +233,18 @@ export function Sidebar() {
{t("rating_count")}
diff --git a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
index a3d24958..72b48a8c 100644
--- a/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
+++ b/src/renderer/src/pages/profile/profile-content/user-library-game-card.tsx
@@ -234,7 +234,7 @@ export function UserLibraryGameCard({
diff --git a/src/types/index.ts b/src/types/index.ts
index 0c6af89b..6a864f3a 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -45,7 +45,7 @@ export interface ShopAssets {
libraryImageUrl: string;
logoImageUrl: string;
logoPosition: string | null;
- coverImageUrl: string;
+ coverImageUrl: string | null;
}
export type ShopDetails = SteamAppDetails & {
@@ -235,8 +235,8 @@ export interface DownloadSourceValidationResult {
export interface GameStats {
downloadCount: number;
playerCount: number;
- assets: ShopAssets | null;
averageScore: number | null;
+ reviewCount: number;
}
export interface GameReview {
From b21c97ea66d6e68bb6b87cdc2ba0ae32f0364972 Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Sat, 11 Oct 2025 11:59:54 -0300
Subject: [PATCH 2/3] feat: remove import
---
src/main/events/index.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 1d537db3..ecea6463 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -3,7 +3,6 @@ import { ipcMain } from "electron";
import "./catalogue/get-catalogue";
import "./catalogue/get-game-shop-details";
-import "./catalogue/save-game-shop-assets";
import "./catalogue/get-how-long-to-beat";
import "./catalogue/get-random-game";
import "./catalogue/search-games";
From df6d9df31d453c08e8b83d96c74a9d5d7fe8a26d Mon Sep 17 00:00:00 2001
From: Zamitto <167933696+zamitto@users.noreply.github.com>
Date: Sun, 12 Oct 2025 12:18:43 -0300
Subject: [PATCH 3/3] feat: update cache time
---
src/main/events/catalogue/get-game-assets.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/events/catalogue/get-game-assets.ts b/src/main/events/catalogue/get-game-assets.ts
index 04a03808..de1d2b1f 100644
--- a/src/main/events/catalogue/get-game-assets.ts
+++ b/src/main/events/catalogue/get-game-assets.ts
@@ -3,7 +3,7 @@ import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
-const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes
+const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 8; // 8 hours
export const getGameAssets = async (objectId: string, shop: GameShop) => {
const cachedAssets = await gamesShopAssetsSublevel.get(