mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: migrating repacks to a worker
This commit is contained in:
parent
eb3eb88f23
commit
5a85033486
19 changed files with 204 additions and 147 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"]
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@ const getLibrary = async () =>
|
|||
isDeleted: false,
|
||||
},
|
||||
order: {
|
||||
createdAt: "desc",
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
|
31
src/main/workers/repacks.worker.ts
Normal file
31
src/main/workers/repacks.worker.ts
Normal 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 };
|
||||
});
|
||||
};
|
|
@ -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"]);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue