feat: adding steam client icon cache

This commit is contained in:
Chubby Granny Chaser 2024-05-18 21:55:12 +01:00
parent e5fec91062
commit 19f022e0f6
No known key found for this signature in database
23 changed files with 184 additions and 145 deletions

View File

@ -6,6 +6,7 @@ extraResources:
- hydra-download-manager - hydra-download-manager
- hydra.db - hydra.db
- fastlist.exe - fastlist.exe
- seeds
files: files:
- "!**/.vscode/*" - "!**/.vscode/*"
- "!src/*" - "!src/*"

BIN
hydra.db

Binary file not shown.

1
seeds/steam-games.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -18,21 +18,6 @@ export const repackers = [
"onlinefix", "onlinefix",
] as const; ] as const;
export const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const defaultDownloadsPath = app.getPath("downloads"); export const defaultDownloadsPath = app.getPath("downloads");
export const databasePath = path.join( export const databasePath = path.join(
@ -41,5 +26,6 @@ export const databasePath = path.join(
"hydra.db" "hydra.db"
); );
export const INSTALLATION_ID_LENGTH = 6; export const seedsPath = app.isPackaged
export const ACTIVATION_KEY_MULTIPLIER = 7; ? path.join(process.resourcesPath, "seeds")
: path.join(__dirname, "..", "..", "seeds");

View File

@ -1,25 +1,21 @@
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { import { Game, GameShopCache, Repack, UserPreferences } from "@main/entity";
Game, import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
GameShopCache,
Repack,
UserPreferences,
SteamGame,
} from "@main/entity";
import type { SqliteConnectionOptions } from "typeorm/driver/sqlite/SqliteConnectionOptions";
import { databasePath } from "./constants"; import { databasePath } from "./constants";
import migrations from "./migrations"; import migrations from "./migrations";
export const createDataSource = (options: Partial<SqliteConnectionOptions>) => export const createDataSource = (
options: Partial<BetterSqlite3ConnectionOptions>
) =>
new DataSource({ new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
database: databasePath, entities: [Game, Repack, UserPreferences, GameShopCache],
entities: [Game, Repack, UserPreferences, GameShopCache, SteamGame],
synchronize: true, synchronize: true,
database: databasePath,
...options, ...options,
}); });
export const dataSource = createDataSource({ export const dataSource = createDataSource({
migrations: migrations, migrations,
}); });

View File

@ -23,8 +23,8 @@ export class Game {
@Column("text") @Column("text")
title: string; title: string;
@Column("text") @Column("text", { nullable: true })
iconUrl: string; iconUrl: string | null;
@Column("text", { nullable: true }) @Column("text", { nullable: true })
folderName: string | null; folderName: string | null;

View File

@ -2,4 +2,3 @@ export * from "./game.entity";
export * from "./repack.entity"; export * from "./repack.entity";
export * from "./user-preferences.entity"; export * from "./user-preferences.entity";
export * from "./game-shop-cache.entity"; export * from "./game-shop-cache.entity";
export * from "./steam-game.entity";

View File

@ -1,10 +0,0 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity("steam_game")
export class SteamGame {
@PrimaryColumn()
id: number;
@Column()
name: string;
}

View File

@ -1,9 +1,10 @@
import { gameShopCacheRepository, steamGameRepository } from "@main/repository"; import { gameShopCacheRepository } from "@main/repository";
import { getSteamAppDetails } from "@main/services"; import { getSteamAppDetails } from "@main/services";
import type { ShopDetails, GameShop, SteamAppDetails } from "@types"; import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { stateManager } from "@main/state-manager";
const getLocalizedSteamAppDetails = ( const getLocalizedSteamAppDetails = (
objectID: string, objectID: string,
@ -13,10 +14,11 @@ const getLocalizedSteamAppDetails = (
return getSteamAppDetails(objectID, language); return getSteamAppDetails(objectID, language);
} }
return Promise.all([ return getSteamAppDetails(objectID, language).then((localizedAppDetails) => {
steamGameRepository.findOne({ where: { id: Number(objectID) } }), const steamGame = stateManager
getSteamAppDetails(objectID, language), .getValue("steamGames")
]).then(([steamGame, localizedAppDetails]) => { .find((game) => game.id === Number(objectID));
if (steamGame && localizedAppDetails) { if (steamGame && localizedAppDetails) {
return { return {
...localizedAppDetails, ...localizedAppDetails,

View File

@ -3,8 +3,8 @@ import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
import { getFileBase64 } from "@main/helpers"; import { getFileBase64, getSteamAppAsset } from "@main/helpers";
import { getSteamGameIconUrl } from "@main/services"; import { stateManager } from "@main/state-manager";
const addGameToLibrary = async ( const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -27,16 +27,28 @@ const addGameToLibrary = async (
) )
.then(async ({ affected }) => { .then(async ({ affected }) => {
if (!affected) { if (!affected) {
const iconUrl = await getFileBase64( const steamGame = stateManager
await getSteamGameIconUrl(objectID) .getValue("steamGames")
); .find((game) => game.id === Number(objectID));
await gameRepository.insert({ const iconUrl = steamGame?.clientIcon
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
: null;
await gameRepository
.insert({
title, title,
iconUrl, iconUrl,
objectID, objectID,
shop: gameShop, shop: gameShop,
executablePath, executablePath,
})
.then(() => {
if (iconUrl) {
getFileBase64(iconUrl).then((base64) =>
gameRepository.update({ objectID }, { iconUrl: base64 })
);
}
}); });
} }
}); });

View File

@ -1,4 +1,3 @@
import { getSteamGameIconUrl } from "@main/services";
import { import {
gameRepository, gameRepository,
repackRepository, repackRepository,
@ -8,10 +7,11 @@ import {
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { GameShop } from "@types"; import type { GameShop } from "@types";
import { getFileBase64 } from "@main/helpers"; import { getFileBase64, getSteamAppAsset } from "@main/helpers";
import { In } from "typeorm"; import { In } from "typeorm";
import { DownloadManager } from "@main/services"; import { DownloadManager } from "@main/services";
import { Downloader, GameStatus } from "@shared"; import { Downloader, GameStatus } from "@shared";
import { stateManager } from "@main/state-manager";
const startGameDownload = async ( const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -76,9 +76,16 @@ const startGameDownload = async (
return game; return game;
} else { } else {
const iconUrl = await getFileBase64(await getSteamGameIconUrl(objectID)); const steamGame = stateManager
.getValue("steamGames")
.find((game) => game.id === Number(objectID));
const createdGame = await gameRepository.save({ const iconUrl = steamGame?.clientIcon
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
: null;
const createdGame = await gameRepository
.save({
title, title,
iconUrl, iconUrl,
objectID, objectID,
@ -87,6 +94,15 @@ const startGameDownload = async (
status: GameStatus.Downloading, status: GameStatus.Downloading,
downloadPath, downloadPath,
repack: { id: repackId }, repack: { id: repackId },
})
.then((result) => {
if (iconUrl) {
getFileBase64(iconUrl).then((base64) =>
gameRepository.update({ objectID }, { iconUrl: base64 })
);
}
return result;
}); });
DownloadManager.downloadGame(createdGame.id); DownloadManager.downloadGame(createdGame.id);

View File

@ -13,7 +13,7 @@ import {
gogFormatter, gogFormatter,
onlinefixFormatter, onlinefixFormatter,
} from "./formatters"; } from "./formatters";
import { months, repackers } from "../constants"; import { repackers } from "../constants";
export const pipe = export const pipe =
<T>(...fns: ((arg: T) => any)[]) => <T>(...fns: ((arg: T) => any)[]) =>
@ -44,19 +44,6 @@ export const repackerFormatter: Record<
onlinefix: onlinefixFormatter, onlinefix: onlinefixFormatter,
}; };
export const formatUploadDate = (str: string) => {
const date = new Date();
const [month, day, year] = str.split(" ");
date.setMonth(months.indexOf(month.replace(".", "")));
date.setDate(Number(day.substring(0, 2)));
date.setFullYear(Number("20" + year.replace("'", "")));
date.setHours(0, 0, 0, 0);
return date;
};
export const getSteamAppAsset = ( export const getSteamAppAsset = (
category: "library" | "hero" | "logo" | "icon", category: "library" | "hero" | "logo" | "icon",
objectID: string, objectID: string,

View File

@ -19,8 +19,6 @@ autoUpdater.setFeedURL({
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit(); if (!gotTheLock) app.quit();
app.disableHardwareAcceleration();
i18n.init({ i18n.init({
resources, resources,
lng: "en", lng: "en",

View File

@ -1,5 +1,5 @@
import { stateManager } from "./state-manager"; import { stateManager } from "./state-manager";
import { repackersOn1337x } from "./constants"; import { repackersOn1337x, seedsPath } from "./constants";
import { import {
getNewGOGGames, getNewGOGGames,
getNewRepacksFromUser, getNewRepacksFromUser,
@ -11,7 +11,6 @@ import {
import { import {
gameRepository, gameRepository,
repackRepository, repackRepository,
steamGameRepository,
userPreferencesRepository, userPreferencesRepository,
} from "./repository"; } from "./repository";
import { TorrentDownloader } from "./services"; import { TorrentDownloader } from "./services";
@ -20,6 +19,8 @@ import { Notification } from "electron";
import { t } from "i18next"; import { t } from "i18next";
import { GameStatus } from "@shared"; import { GameStatus } from "@shared";
import { In } from "typeorm"; import { In } from "typeorm";
import fs from "node:fs";
import path from "node:path";
import { RealDebridClient } from "./services/real-debrid"; import { RealDebridClient } from "./services/real-debrid";
startProcessWatcher(); startProcessWatcher();
@ -69,18 +70,15 @@ const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
}; };
const loadState = async (userPreferences: UserPreferences | null) => { const loadState = async (userPreferences: UserPreferences | null) => {
const [repacks, steamGames] = await Promise.all([ const repacks = await repackRepository.find({
repackRepository.find({
order: { order: {
createdAt: "desc", createdAt: "desc",
}, },
}), });
steamGameRepository.find({
order: { const steamGames = JSON.parse(
name: "asc", fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
}, );
}),
]);
stateManager.setValue("repacks", repacks); stateManager.setValue("repacks", repacks);
stateManager.setValue("steamGames", steamGames); stateManager.setValue("steamGames", steamGames);

View File

@ -1,11 +1,5 @@
import { dataSource } from "./data-source"; import { dataSource } from "./data-source";
import { import { Game, GameShopCache, Repack, UserPreferences } from "@main/entity";
Game,
GameShopCache,
Repack,
UserPreferences,
SteamGame,
} from "@main/entity";
export const gameRepository = dataSource.getRepository(Game); export const gameRepository = dataSource.getRepository(Game);
@ -15,5 +9,3 @@ export const userPreferencesRepository =
dataSource.getRepository(UserPreferences); dataSource.getRepository(UserPreferences);
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache); export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
export const steamGameRepository = dataSource.getRepository(SteamGame);

View File

@ -0,0 +1,40 @@
import path from "node:path";
import fs from "node:fs";
import { getSteamGameClientIcon, logger } from "@main/services";
import { chunk } from "lodash-es";
import { seedsPath } from "@main/constants";
import type { SteamGame } from "@types";
const steamGamesPath = path.join(seedsPath, "steam-games.json");
const steamGames = JSON.parse(
fs.readFileSync(steamGamesPath, "utf-8")
) as SteamGame[];
const chunks = chunk(steamGames, 1500);
for (const chunk of chunks) {
await Promise.all(
chunk.map(async (steamGame) => {
if (steamGame.clientIcon) return;
const index = steamGames.findIndex((game) => game.id === steamGame.id);
try {
const clientIcon = await getSteamGameClientIcon(String(steamGame.id));
steamGames[index].clientIcon = clientIcon;
logger.log("info", `Set ${steamGame.name} client icon`);
} catch (err) {
steamGames[index].clientIcon = null;
logger.log("info", `Could not set icon for ${steamGame.name}`);
}
})
);
fs.writeFileSync(steamGamesPath, JSON.stringify(steamGames));
logger.log("info", "Updated steam games");
}

View File

@ -1,13 +1,39 @@
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import { formatUploadDate } from "@main/helpers";
import { Repack } from "@main/entity"; import { Repack } from "@main/entity";
import { requestWebPage, savePage } from "./helpers"; import { requestWebPage, savePage } from "./helpers";
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
export const request1337x = async (path: string) => export const request1337x = async (path: string) =>
requestWebPage(`https://1337xx.to${path}`); requestWebPage(`https://1337xx.to${path}`);
const formatUploadDate = (str: string) => {
const date = new Date();
const [month, day, year] = str.split(" ");
date.setMonth(months.indexOf(month.replace(".", "")));
date.setDate(Number(day.substring(0, 2)));
date.setFullYear(Number("20" + year.replace("'", "")));
date.setHours(0, 0, 0, 0);
return date;
};
/* TODO: $a will often be null */ /* TODO: $a will often be null */
const getTorrentDetails = async (path: string) => { const getTorrentDetails = async (path: string) => {
const response = await request1337x(path); const response = await request1337x(path);

View File

@ -1,5 +1,4 @@
import axios from "axios"; import axios from "axios";
import { getSteamAppAsset } from "@main/helpers";
export interface SteamGridResponse { export interface SteamGridResponse {
success: boolean; success: boolean;
@ -59,16 +58,11 @@ export const getSteamGridGameById = async (
return response.data; return response.data;
}; };
export const getSteamGameIconUrl = async (objectID: string) => { export const getSteamGameClientIcon = async (objectID: string) => {
const { const {
data: { id: steamGridGameId }, data: { id: steamGridGameId },
} = await getSteamGridData(objectID, "games", "steam"); } = await getSteamGridData(objectID, "games", "steam");
const steamGridGame = await getSteamGridGameById(steamGridGameId); const steamGridGame = await getSteamGridGameById(steamGridGameId);
return steamGridGame.data.platforms.steam.metadata.clienticon;
return getSteamAppAsset(
"icon",
objectID,
steamGridGame.data.platforms.steam.metadata.clienticon
);
}; };

View File

@ -4,8 +4,8 @@ import { app } from "electron";
import { chunk } from "lodash-es"; import { chunk } from "lodash-es";
import { createDataSource } from "@main/data-source"; import { createDataSource } from "@main/data-source";
import { Repack, SteamGame } from "@main/entity"; import { Repack } from "@main/entity";
import { repackRepository, steamGameRepository } from "@main/repository"; import { repackRepository } from "@main/repository";
export const resolveDatabaseUpdates = async () => { export const resolveDatabaseUpdates = async () => {
const updateDataSource = createDataSource({ const updateDataSource = createDataSource({
@ -16,12 +16,8 @@ export const resolveDatabaseUpdates = async () => {
return updateDataSource.initialize().then(async () => { return updateDataSource.initialize().then(async () => {
const updateRepackRepository = updateDataSource.getRepository(Repack); const updateRepackRepository = updateDataSource.getRepository(Repack);
const updateSteamGameRepository = updateDataSource.getRepository(SteamGame);
const [updateRepacks, updateSteamGames] = await Promise.all([ const updateRepacks = await updateRepackRepository.find();
updateRepackRepository.find(),
updateSteamGameRepository.find(),
]);
const updateRepacksChunks = chunk(updateRepacks, 800); const updateRepacksChunks = chunk(updateRepacks, 800);
@ -33,16 +29,5 @@ export const resolveDatabaseUpdates = async () => {
.orIgnore() .orIgnore()
.execute(); .execute();
} }
const steamGamesChunks = chunk(updateSteamGames, 800);
for (const chunk of steamGamesChunks) {
await steamGameRepository
.createQueryBuilder()
.insert()
.values(chunk)
.orIgnore()
.execute();
}
}); });
}; };

View File

@ -1,4 +1,5 @@
import type { Repack, SteamGame } from "@main/entity"; import type { Repack } from "@main/entity";
import type { SteamGame } from "@types";
interface State { interface State {
repacks: Repack[]; repacks: Repack[];

View File

@ -106,6 +106,8 @@ export const menuItemButtonLabel = style({
export const gameIcon = style({ export const gameIcon = style({
width: "20px", width: "20px",
height: "20px", height: "20px",
minWidth: "20px",
minHeight: "20px",
borderRadius: "4px", borderRadius: "4px",
backgroundSize: "cover", backgroundSize: "cover",
}); });

View File

@ -13,6 +13,8 @@ import * as styles from "./sidebar.css";
import { GameStatus, GameStatusHelper } from "@shared"; import { GameStatus, GameStatusHelper } from "@shared";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 450; const SIDEBAR_MAX_WIDTH = 450;
@ -191,11 +193,16 @@ export function Sidebar() {
handleSidebarItemClick(buildGameDetailsPath(game)) handleSidebarItemClick(buildGameDetailsPath(game))
} }
> >
{game.iconUrl ? (
<img <img
className={styles.gameIcon} className={styles.gameIcon}
src={game.iconUrl} src={game.iconUrl}
alt={game.title} alt={game.title}
/> />
) : (
<SteamLogo className={styles.gameIcon} />
)}
<span className={styles.menuItemButtonLabel}> <span className={styles.menuItemButtonLabel}>
{getGameTitle(game)} {getGameTitle(game)}
</span> </span>

View File

@ -137,3 +137,9 @@ export interface Steam250Game {
title: string; title: string;
objectID: string; objectID: string;
} }
export interface SteamGame {
id: number;
name: string;
clientIcon: string | null;
}