feat: updating play label on hero panel

This commit is contained in:
Hydra
2024-04-18 22:26:17 +01:00
parent 91b1341271
commit 96e11e6be9
40 changed files with 2049 additions and 745 deletions

View File

@@ -7,6 +7,7 @@ import {
RepackerFriendlyName,
UserPreferences,
MigrationScript,
SteamGame,
} from "@main/entity";
import type { SqliteConnectionOptions } from "typeorm/driver/sqlite/SqliteConnectionOptions";
@@ -24,6 +25,7 @@ export const createDataSource = (options: Partial<SqliteConnectionOptions>) =>
UserPreferences,
GameShopCache,
MigrationScript,
SteamGame,
],
...options,
});

View File

@@ -5,3 +5,4 @@ export * from "./repacker-friendly-name.entity";
export * from "./user-preferences.entity";
export * from "./game-shop-cache.entity";
export * from "./migration-script.entity";
export * from "./steam-game.entity";

View File

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

View File

@@ -1,25 +1,22 @@
import { formatName, repackerFormatter } from "@main/helpers";
import { getTrendingGames } from "@main/services";
import type { CatalogueCategory, CatalogueEntry } from "@types";
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
import type { CatalogueCategory, CatalogueEntry, GameShop } from "@types";
import { stateManager } from "@main/state-manager";
import { searchGames } from "../helpers/search-games";
import { searchGames, searchRepacks } from "../helpers/search-games";
import { registerEvent } from "../register-event";
import { requestSteam250 } from "@main/services";
const repacks = stateManager.getValue("repacks");
interface GetStringForLookup {
(index: number): string;
}
const getCatalogue = async (
_event: Electron.IpcMainInvokeEvent,
category: CatalogueCategory
) => {
const trendingGames = await getTrendingGames();
let i = 0;
const results: CatalogueEntry[] = [];
const getStringForLookup = (index: number) => {
if (category === "trending") return trendingGames[index];
const getStringForLookup = (index: number): string => {
const repack = repacks[index];
const formatter =
repackerFormatter[repack.repacker as keyof typeof repackerFormatter];
@@ -30,10 +27,56 @@ const getCatalogue = async (
if (!repacks.length) return [];
const resultSize = 12;
const requestSize = resultSize * 2;
let lookupRequest = [];
while (results.length < resultSize) {
if (category === "trending") {
return getTrendingCatalogue(resultSize);
} else {
return getRecentlyAddedCatalogue(
resultSize,
resultSize,
getStringForLookup
);
}
};
const getTrendingCatalogue = async (
resultSize: number
): Promise<CatalogueEntry[]> => {
const results: CatalogueEntry[] = [];
const trendingGames = await requestSteam250("/30day");
for (
let i = 0;
i < trendingGames.length && results.length < resultSize;
i++
) {
if (!trendingGames[i]) continue;
const { title, objectID } = trendingGames[i];
const repacks = searchRepacks(title);
if (title && repacks.length) {
const catalogueEntry = {
objectID,
title,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", objectID),
};
results.push({ ...catalogueEntry, repacks });
}
}
return results;
};
const getRecentlyAddedCatalogue = async (
resultSize: number,
requestSize: number,
getStringForLookup: GetStringForLookup
): Promise<CatalogueEntry[]> => {
let lookupRequest = [];
const results: CatalogueEntry[] = [];
for (let i = 0; results.length < resultSize; i++) {
const stringForLookup = getStringForLookup(i);
if (!stringForLookup) {
@@ -41,9 +84,7 @@ const getCatalogue = async (
continue;
}
lookupRequest.push(searchGames(stringForLookup));
i++;
lookupRequest.push(searchGames({ query: stringForLookup }));
if (lookupRequest.length < requestSize) {
continue;

View File

@@ -0,0 +1,32 @@
import type { CatalogueEntry } from "@types";
import { registerEvent } from "../register-event";
import { searchGames } from "../helpers/search-games";
import slice from "lodash/slice";
const getGames = async (
_event: Electron.IpcMainInvokeEvent,
take?: number,
prevCursor = 0
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
let results: CatalogueEntry[] = [];
let i = 0;
const batchSize = 100;
while (results.length < take) {
const games = await searchGames({
take: batchSize,
skip: (i + prevCursor) * batchSize,
});
results = [...results, ...games.filter((game) => game.repacks.length)];
i++;
}
return { results: slice(results, 0, take), cursor: prevCursor + i };
};
registerEvent(getGames, {
name: "getGames",
memoize: true,
});

View File

@@ -11,10 +11,10 @@ const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
const shuffledList = shuffle(games);
for (const game of shuffledList) {
const repacks = searchRepacks(formatName(game));
const repacks = searchRepacks(formatName(game.title));
if (repacks.length) {
const results = await searchGames(game);
const results = await searchGames({ query: game.title });
if (results.length) {
return results[0].objectID;

View File

@@ -2,7 +2,8 @@ import { registerEvent } from "../register-event";
import { searchGames } from "../helpers/search-games";
registerEvent(
(_event: Electron.IpcMainInvokeEvent, query: string) => searchGames(query),
(_event: Electron.IpcMainInvokeEvent, query: string) =>
searchGames({ query, take: 12 }),
{
name: "searchGames",
memoize: true,

View File

@@ -4,8 +4,10 @@ import orderBy from "lodash/orderBy";
import type { GameRepack, GameShop, CatalogueEntry } from "@types";
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
import { searchSteamGame } from "@main/services";
import { stateManager } from "@main/state-manager";
import { steamGameRepository } from "@main/repository";
import { FindManyOptions, Like } from "typeorm";
import { SteamGame } from "@main/entity";
const { Index } = flexSearch;
const repacksIndex = new Index();
@@ -32,33 +34,41 @@ export const searchRepacks = (title: string): GameRepack[] => {
);
};
export const searchGames = async (query: string): Promise<CatalogueEntry[]> => {
const formattedName = formatName(query);
export interface SearchGamesArgs {
query?: string;
take?: number;
skip?: number;
}
const steamResults = await searchSteamGame(formattedName);
export const searchGames = async ({
query,
take,
skip,
}: SearchGamesArgs): Promise<CatalogueEntry[]> => {
const options: FindManyOptions<SteamGame> = {};
const results = steamResults.map((result) => ({
objectID: result.objectID,
title: result.name,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", result.objectID),
}));
const gamesIndex = new Index({
tokenize: "full",
});
for (let i = 0; i < results.length; i++) {
const game = results[i];
gamesIndex.add(i, game.title);
if (query) {
options.where = {
name: query ? Like(`%${formatName(query)}%`) : undefined,
};
}
const filteredResults = gamesIndex
.search(query)
.map((index) => results[index as number]);
const steamResults = await steamGameRepository.find({
...options,
take,
skip,
order: { name: "ASC" },
});
const results = steamResults.map((result) => ({
objectID: String(result.id),
title: result.name,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", String(result.id)),
}));
return Promise.all(
filteredResults.map(async (result) => ({
results.map(async (result) => ({
...result,
repacks: searchRepacks(result.title),
}))

View File

@@ -24,6 +24,7 @@ import "./library/remove-game";
import "./library/delete-game-folder";
import "./catalogue/get-random-game";
import "./catalogue/get-how-long-to-beat";
import "./catalogue/get-games";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());

View File

@@ -7,6 +7,7 @@ import {
RepackerFriendlyName,
UserPreferences,
MigrationScript,
SteamGame,
} from "@main/entity";
export const gameRepository = dataSource.getRepository(Game);
@@ -25,3 +26,5 @@ export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
export const migrationScriptRepository =
dataSource.getRepository(MigrationScript);
export const steamGameRepository = dataSource.getRepository(SteamGame);

View File

@@ -46,7 +46,9 @@ export const startProcessWatcher = async () => {
const zero = gamesPlaytime.get(game.id);
const delta = performance.now() - zero;
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-playtime", game.id);
}
await gameRepository.update(game.id, {
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
@@ -68,7 +70,9 @@ export const startProcessWatcher = async () => {
if (gamesPlaytime.has(game.id)) {
gamesPlaytime.delete(game.id);
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-game-close", game.id);
}
}
await sleep(sleepTime);

View File

@@ -1,26 +1,24 @@
import axios from "axios";
import { JSDOM } from "jsdom";
import shuffle from "lodash/shuffle";
import { logger } from "./logger";
const requestSteam250 = async (path: string) => {
return axios
.get(`https://steam250.com${path}`)
.then((response) => response.data);
};
export const requestSteam250 = async (path: string) => {
return axios.get(`https://steam250.com${path}`).then((response) => {
const { window } = new JSDOM(response.data);
const { document } = window;
export const getTrendingGames = async () => {
const response = await requestSteam250("/365day").catch((err) => {
logger.error(err.response, { method: "getTrendingGames" });
throw new Error(err);
return Array.from(document.querySelectorAll(".appline .title a")).map(
($title: HTMLAnchorElement) => {
const steamGameUrl = $title.href;
if (!steamGameUrl) return null;
return {
title: $title.textContent,
objectID: steamGameUrl.split("/").pop(),
};
}
);
});
const { window } = new JSDOM(response);
const { document } = window;
return Array.from(document.querySelectorAll(".appline .title a")).map(
($title) => $title.textContent!
);
};
const steam250Paths = [
@@ -32,15 +30,5 @@ const steam250Paths = [
export const getRandomSteam250List = async () => {
const [path] = shuffle(steam250Paths);
const response = await requestSteam250(path).catch((err) => {
logger.error(err.response, { method: "getRandomSteam250List" });
throw new Error(err);
});
const { window } = new JSDOM(response);
const { document } = window;
return Array.from(document.querySelectorAll(".appline .title a")).map(
($title) => $title.textContent!
);
return requestSteam250(path);
};

View File

@@ -1,5 +1,4 @@
import axios from "axios";
import { JSDOM } from "jsdom";
import type { SteamAppDetails } from "@types";
@@ -34,45 +33,3 @@ export const getSteamAppDetails = async (
throw new Error(err);
});
};
export const searchSteamGame = async (term: string) => {
const searchParams = new URLSearchParams({
start: "0",
count: "12",
sort_by: "_ASC",
/* Games only */
category1: "998",
term: term,
});
const response = await axios.get(
`https://store.steampowered.com/search/results/?${searchParams.toString()}`
);
const { window } = new JSDOM(response.data);
const { document } = window;
const $anchors = Array.from(
document.querySelectorAll("#search_resultsRows a")
);
return $anchors.reduce((prev, $a) => {
const $title = $a.querySelector(".title");
const objectIDs = $a.getAttribute("data-ds-appid");
if (!objectIDs) return prev;
const [objectID] = objectIDs.split(",");
if (!objectID || prev.some((game) => game.objectID === objectID))
return prev;
return [
...prev,
{
name: $title.textContent,
objectID,
},
];
}, []);
};

View File

@@ -4,11 +4,12 @@ import { app } from "electron";
import chunk from "lodash/chunk";
import { createDataSource, dataSource } from "@main/data-source";
import { Repack, RepackerFriendlyName } from "@main/entity";
import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
import {
migrationScriptRepository,
repackRepository,
repackerFriendlyNameRepository,
steamGameRepository,
} from "@main/repository";
import { MigrationScript } from "@main/entity/migration-script.entity";
import { Like } from "typeorm";
@@ -115,11 +116,14 @@ export const resolveDatabaseUpdates = async () => {
const updateRepackRepository = updateDataSource.getRepository(Repack);
const updateRepackerFriendlyNameRepository =
updateDataSource.getRepository(RepackerFriendlyName);
const updateSteamGameRepository = updateDataSource.getRepository(SteamGame);
const [updateRepacks, updateRepackerFriendlyNames] = await Promise.all([
updateRepackRepository.find(),
updateRepackerFriendlyNameRepository.find(),
]);
const [updateRepacks, updateSteamGames, updateRepackerFriendlyNames] =
await Promise.all([
updateRepackRepository.find(),
updateSteamGameRepository.find(),
updateRepackerFriendlyNameRepository.find(),
]);
await runMigrationScripts(updateRepacks);
@@ -140,5 +144,16 @@ export const resolveDatabaseUpdates = async () => {
.orIgnore()
.execute();
}
const steamGamesChunks = chunk(updateSteamGames, 800);
for (const chunk of steamGamesChunks) {
await steamGameRepository
.createQueryBuilder()
.insert()
.values(chunk)
.orIgnore()
.execute();
}
});
};