feat: migrating repacks to a worker

This commit is contained in:
Chubby Granny Chaser 2024-06-03 14:34:02 +01:00
parent eb3eb88f23
commit 5a85033486
No known key found for this signature in database
19 changed files with 204 additions and 147 deletions

View file

@ -2,7 +2,9 @@ import { getSteamAppAsset } from "@main/helpers";
import type { CatalogueEntry, GameShop } from "@types";
import { registerEvent } from "../register-event";
import { SearchEngine, requestSteam250 } from "@main/services";
import { requestSteam250 } from "@main/services";
import { repacksWorker } from "@main/workers";
import { formatName } from "@shared";
const resultSize = 12;
@ -17,7 +19,10 @@ const getCatalogue = async (_event: Electron.IpcMainInvokeEvent) => {
}
const { title, objectID } = trendingGames[i]!;
const repacks = SearchEngine.searchRepacks(title);
const repacks = await repacksWorker.run(
{ query: formatName(title) },
{ name: "search" }
);
const catalogueEntry = {
objectID,

View file

@ -1,22 +1,29 @@
import type { CatalogueEntry } from "@types";
import { registerEvent } from "../register-event";
import { repacksWorker, steamGamesWorker } from "@main/workers";
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
import { steamGamesWorker } from "@main/workers";
const getGames = async (
_event: Electron.IpcMainInvokeEvent,
take = 12,
cursor = 0
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
const results = await steamGamesWorker.run(
const steamGames = await steamGamesWorker.run(
{ limit: take, offset: cursor },
{ name: "list" }
);
const entries = await repacksWorker.run(
steamGames.map((game) => convertSteamGameToCatalogueEntry(game)),
{
name: "findRepacksForCatalogueEntries",
}
);
return {
results: results.map((result) => convertSteamGameToCatalogueEntry(result)),
cursor: cursor + results.length,
results: entries,
cursor: cursor + entries.length,
};
};

View file

@ -1,10 +1,12 @@
import { shuffle } from "lodash-es";
import { SearchEngine, getSteam250List } from "@main/services";
import { getSteam250List } from "@main/services";
import { registerEvent } from "../register-event";
import { searchSteamGames } from "../helpers/search-games";
import type { Steam250Game } from "@types";
import { repacksWorker } from "@main/workers";
import { formatName } from "@shared";
const state = { games: Array<Steam250Game>(), index: 0 };
@ -15,7 +17,10 @@ const filterGames = async (games: Steam250Game[]) => {
const catalogue = await searchSteamGames({ query: game.title });
if (catalogue.length) {
const repacks = SearchEngine.searchRepacks(catalogue[0].title);
const repacks = await repacksWorker.run(
{ query: formatName(catalogue[0].title) },
{ name: "search" }
);
if (repacks.length) {
results.push(game);

View file

@ -1,9 +1,9 @@
import { SearchEngine } from "@main/services";
import { registerEvent } from "../register-event";
import { repacksWorker } from "@main/workers";
const searchGameRepacks = (
_event: Electron.IpcMainInvokeEvent,
query: string
) => SearchEngine.searchRepacks(query);
) => repacksWorker.run({ query }, { name: "search" });
registerEvent("searchGameRepacks", searchGameRepacks);

View file

@ -5,7 +5,8 @@ import { DownloadSource, Repack } from "@main/entity";
import axios from "axios";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
import { downloadSourceSchema } from "../helpers/validators";
import { SearchEngine } from "@main/services";
import { repackRepository } from "@main/repository";
import { repacksWorker } from "@main/workers";
const addDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
@ -49,7 +50,15 @@ const addDownloadSource = async (
}
);
await SearchEngine.updateRepacks();
repackRepository
.find({
order: {
createdAt: "DESC",
},
})
.then((repacks) => {
repacksWorker.run(repacks, { name: "setRepacks" });
});
return downloadSource;
};

View file

@ -1,13 +1,22 @@
import { downloadSourceRepository } from "@main/repository";
import { downloadSourceRepository, repackRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { SearchEngine } from "@main/services";
import { repacksWorker } from "@main/workers";
const removeDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
id: number
) => {
await downloadSourceRepository.delete(id);
await SearchEngine.updateRepacks();
repackRepository
.find({
order: {
createdAt: "DESC",
},
})
.then((repacks) => {
repacksWorker.run(repacks, { name: "setRepacks" });
});
};
registerEvent("removeDownloadSource", removeDownloadSource);

View file

@ -1,22 +1,9 @@
import { z } from "zod";
import { registerEvent } from "../register-event";
import axios from "axios";
import { downloadSourceRepository } from "@main/repository";
const downloadSourceSchema = z.object({
name: z.string().max(255),
downloads: z.array(
z.object({
title: z.string().max(255),
objectId: z.string().max(255).nullable(),
shop: z.enum(["steam"]).nullable(),
downloaders: z.array(z.enum(["real_debrid", "torrent"])),
uris: z.array(z.string()),
uploadDate: z.string().max(255),
fileSize: z.string().max(255),
})
),
});
import { downloadSourceSchema } from "../helpers/validators";
import { repacksWorker } from "@main/workers";
import { GameRepack } from "@types";
const validateDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
@ -27,13 +14,26 @@ const validateDownloadSource = async (
const source = downloadSourceSchema.parse(response.data);
const existingSource = await downloadSourceRepository.findOne({
where: [{ url }, { name: source.name }],
where: { url },
});
if (existingSource?.url === url)
if (existingSource)
throw new Error("Source with the same url already exists");
return { name: source.name, downloadCount: source.downloads.length };
const repacks = (await repacksWorker.run(undefined, {
name: "list",
})) as GameRepack[];
console.log(repacks);
const existingUris = source.downloads
.flatMap((download) => download.uris)
.filter((uri) => repacks.some((repack) => repack.magnet === uri));
return {
name: source.name,
downloadCount: source.downloads.length - existingUris.length,
};
};
registerEvent("validateDownloadSource", validateDownloadSource);

View file

@ -4,8 +4,7 @@ import flexSearch from "flexsearch";
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
import { getSteamAppAsset } from "@main/helpers";
import { SearchEngine } from "@main/services";
import { steamGamesWorker } from "@main/workers";
import { repacksWorker, steamGamesWorker } from "@main/workers";
export interface SearchGamesArgs {
query?: string;
@ -14,24 +13,31 @@ export interface SearchGamesArgs {
}
export const convertSteamGameToCatalogueEntry = (
result: SteamGame
): CatalogueEntry => {
return {
objectID: String(result.id),
title: result.name,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", String(result.id)),
repacks: SearchEngine.searchRepacks(result.name),
};
};
game: SteamGame
): CatalogueEntry => ({
objectID: String(game.id),
title: game.name,
shop: "steam" as GameShop,
cover: getSteamAppAsset("library", String(game.id)),
repacks: [],
});
export const searchSteamGames = async (
options: flexSearch.SearchOptions
): Promise<CatalogueEntry[]> => {
const steamGames = await steamGamesWorker.run(options, { name: "search" });
const steamGames = (await steamGamesWorker.run(options, {
name: "search",
})) as SteamGame[];
const result = await repacksWorker.run(
steamGames.map((game) => convertSteamGameToCatalogueEntry(game)),
{
name: "findRepacksForCatalogueEntries",
}
);
return orderBy(
steamGames.map((result) => convertSteamGameToCatalogueEntry(result)),
result,
[({ repacks }) => repacks.length, "repacks"],
["desc"]
);

View file

@ -7,7 +7,7 @@ const getLibrary = async () =>
isDeleted: false,
},
order: {
createdAt: "desc",
updatedAt: "desc",
},
});

View file

@ -1,8 +1,13 @@
import { DownloadManager, SearchEngine, startMainLoop } from "./services";
import { gameRepository, userPreferencesRepository } from "./repository";
import { DownloadManager, startMainLoop } from "./services";
import {
gameRepository,
repackRepository,
userPreferencesRepository,
} from "./repository";
import { UserPreferences } from "./entity";
import { RealDebridClient } from "./services/real-debrid";
import { Not } from "typeorm";
import { repacksWorker } from "./workers";
startMainLoop();
@ -21,7 +26,16 @@ const loadState = async (userPreferences: UserPreferences | null) => {
});
if (game) DownloadManager.startDownload(game);
await SearchEngine.updateRepacks();
repackRepository
.find({
order: {
createdAt: "DESC",
},
})
.then((repacks) => {
repacksWorker.run(repacks, { name: "setRepacks" });
});
};
userPreferencesRepository

View file

@ -7,4 +7,3 @@ export * from "./download-manager";
export * from "./how-long-to-beat";
export * from "./process-watcher";
export * from "./main-loop";
export * from "./search-engine";

View file

@ -1,36 +0,0 @@
import flexSearch from "flexsearch";
import { repackRepository } from "@main/repository";
import { formatName } from "@shared";
import type { GameRepack } from "@types";
export class SearchEngine {
public static repacks: GameRepack[] = [];
private static repacksIndex = new flexSearch.Index();
public static searchRepacks(query: string): GameRepack[] {
return this.repacksIndex
.search(formatName(query))
.map((index) => this.repacks[index]);
}
public static async updateRepacks() {
this.repacks = [];
const repacks = await repackRepository.find({
order: {
createdAt: "desc",
},
});
for (let i = 0; i < repacks.length; i++) {
const repack = repacks[i];
const formattedTitle = formatName(repack.title);
this.repacks = [...this.repacks, { ...repack, title: formattedTitle }];
this.repacksIndex.add(i, formattedTitle);
}
}
}

View file

@ -1,5 +1,6 @@
import path from "node:path";
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
import repacksWorkerPath from "./repacks.worker?modulePath";
import Piscina from "piscina";
@ -11,3 +12,7 @@ export const steamGamesWorker = new Piscina({
steamGamesPath: path.join(seedsPath, "steam-games.json"),
},
});
export const repacksWorker = new Piscina({
filename: repacksWorkerPath,
});

View file

@ -0,0 +1,31 @@
import { formatName } from "@shared";
import { CatalogueEntry, GameRepack } from "@types";
import flexSearch from "flexsearch";
const repacksIndex = new flexSearch.Index();
const state: { repacks: GameRepack[] } = { repacks: [] };
export const setRepacks = (repacks: GameRepack[]) => {
state.repacks = repacks;
for (let i = 0; i < repacks.length; i++) {
const repack = repacks[i];
const formattedTitle = formatName(repack.title);
repacksIndex.add(i, formattedTitle);
}
};
export const search = (options: flexSearch.SearchOptions) =>
repacksIndex.search(options).map((index) => state.repacks[index]);
export const list = () => state.repacks;
export const findRepacksForCatalogueEntries = (entries: CatalogueEntry[]) => {
return entries.map((entry) => {
const repacks = search({ query: formatName(entry.title) });
return { ...entry, repacks };
});
};

View file

@ -25,7 +25,7 @@ for (let i = 0; i < steamGames.length; i++) {
}
export const search = (options: flexSearch.SearchOptions) => {
const results = steamGamesIndex.search(options.query ?? "", options);
const results = steamGamesIndex.search(options);
const games = results.map((index) => steamGames[index]);
return orderBy(games, ["name"], ["asc"]);

View file

@ -40,7 +40,3 @@ export const buildGameDetailsPath = (
const searchParams = new URLSearchParams({ title: game.title, ...params });
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
};
export const numberFormatter = new Intl.NumberFormat("en-US", {
maximumSignificantDigits: 3,
});

View file

@ -224,47 +224,51 @@ export function HeroPanelActions() {
);
}
return (
<>
{game?.progress === 1 ? (
<>
<BinaryNotFoundModal
visible={showBinaryNotFoundModal}
onClose={() => setShowBinaryNotFoundModal(false)}
/>
if (game) {
return (
<>
{game?.progress === 1 ? (
<>
<BinaryNotFoundModal
visible={showBinaryNotFoundModal}
onClose={() => setShowBinaryNotFoundModal(false)}
/>
<Button
onClick={openGameInstaller}
theme="outline"
disabled={deleting || isGameRunning}
className={styles.heroPanelAction}
>
{t("install")}
</Button>
</>
) : (
toggleGameOnLibraryButton
)}
{isGameRunning ? (
<Button
onClick={openGameInstaller}
onClick={closeGame}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
{t("close")}
</Button>
) : (
<Button
onClick={openGame}
theme="outline"
disabled={deleting || isGameRunning}
className={styles.heroPanelAction}
>
{t("install")}
{t("play")}
</Button>
</>
) : (
toggleGameOnLibraryButton
)}
)}
</>
);
}
{isGameRunning ? (
<Button
onClick={closeGame}
theme="outline"
disabled={deleting}
className={styles.heroPanelAction}
>
{t("close")}
</Button>
) : (
<Button
onClick={openGame}
theme="outline"
disabled={deleting || isGameRunning}
className={styles.heroPanelAction}
>
{t("play")}
</Button>
)}
</>
);
return toggleGameOnLibraryButton;
}

View file

@ -1,10 +1,9 @@
import { Button, Modal, TextField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import * as styles from "./settings-download-sources.css";
import { numberFormatter } from "@renderer/helpers";
interface AddDownloadSourceModalProps {
visible: boolean;
@ -25,6 +24,12 @@ export function AddDownloadSourceModal({
downloadCount: number;
} | null>(null);
useEffect(() => {
setValue("");
setIsLoading(false);
setValidationResult(null);
}, [visible]);
const { t } = useTranslation("settings");
const handleValidateDownloadSource = async () => {
@ -32,8 +37,8 @@ export function AddDownloadSourceModal({
try {
const result = await window.electron.validateDownloadSource(value);
setValidationResult(result);
console.log(result);
setValidationResult(result);
} finally {
setIsLoading(false);
}
@ -96,7 +101,8 @@ export function AddDownloadSourceModal({
>
<h4>{validationResult?.name}</h4>
<small>
Found {numberFormatter.format(validationResult?.downloadCount)}{" "}
Found{" "}
{validationResult?.downloadCount.toLocaleString(undefined)}{" "}
download options
</small>
</div>

View file

@ -8,7 +8,6 @@ import type { DownloadSource } from "@types";
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
import { AddDownloadSourceModal } from "./add-download-source-modal";
import { useToast } from "@renderer/hooks";
import { numberFormatter } from "@renderer/helpers";
export function SettingsDownloadSources() {
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
@ -53,6 +52,16 @@ export function SettingsDownloadSources() {
{t("download_sources_description")}
</p>
<Button
type="button"
theme="outline"
style={{ alignSelf: "flex-start" }}
onClick={() => setShowAddDownloadSourceModal(true)}
>
<PlusCircleIcon />
{t("add_download_source")}
</Button>
{downloadSources.map((downloadSource) => (
<div key={downloadSource.id} className={styles.downloadSourceItem}>
<div className={styles.downloadSourceItemHeader}>
@ -60,9 +69,7 @@ export function SettingsDownloadSources() {
<small>
{t("download_options", {
count: downloadSource.repackCount,
countFormatted: numberFormatter.format(
downloadSource.repackCount
),
countFormatted: downloadSource.repackCount.toLocaleString(),
})}
</small>
</div>
@ -87,16 +94,6 @@ export function SettingsDownloadSources() {
</div>
</div>
))}
<Button
type="button"
theme="outline"
style={{ alignSelf: "flex-start" }}
onClick={() => setShowAddDownloadSourceModal(true)}
>
<PlusCircleIcon />
{t("add_download_source")}
</Button>
</>
);
}