diff --git a/src/main/events/catalogue/get-catalogue.ts b/src/main/events/catalogue/get-catalogue.ts index 24654bdc..b4ce4531 100644 --- a/src/main/events/catalogue/get-catalogue.ts +++ b/src/main/events/catalogue/get-catalogue.ts @@ -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, diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts index 21e7bc03..8a538178 100644 --- a/src/main/events/catalogue/get-games.ts +++ b/src/main/events/catalogue/get-games.ts @@ -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, }; }; diff --git a/src/main/events/catalogue/get-random-game.ts b/src/main/events/catalogue/get-random-game.ts index 7a1a641b..77ac48a6 100644 --- a/src/main/events/catalogue/get-random-game.ts +++ b/src/main/events/catalogue/get-random-game.ts @@ -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(), 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); diff --git a/src/main/events/catalogue/search-game-repacks.ts b/src/main/events/catalogue/search-game-repacks.ts index a03fe5f0..37bf74f1 100644 --- a/src/main/events/catalogue/search-game-repacks.ts +++ b/src/main/events/catalogue/search-game-repacks.ts @@ -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); diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts index a58f0cc6..cb311184 100644 --- a/src/main/events/download-sources/add-download-source.ts +++ b/src/main/events/download-sources/add-download-source.ts @@ -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; }; diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts index 0f52c58e..a50c5ec9 100644 --- a/src/main/events/download-sources/remove-download-source.ts +++ b/src/main/events/download-sources/remove-download-source.ts @@ -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); diff --git a/src/main/events/download-sources/validate-download-source.ts b/src/main/events/download-sources/validate-download-source.ts index 622e2748..20a3fca7 100644 --- a/src/main/events/download-sources/validate-download-source.ts +++ b/src/main/events/download-sources/validate-download-source.ts @@ -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); diff --git a/src/main/events/helpers/search-games.ts b/src/main/events/helpers/search-games.ts index 4d075409..5434c08a 100644 --- a/src/main/events/helpers/search-games.ts +++ b/src/main/events/helpers/search-games.ts @@ -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 => { - 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"] ); diff --git a/src/main/events/library/get-library.ts b/src/main/events/library/get-library.ts index 429ab4ea..b17807d5 100644 --- a/src/main/events/library/get-library.ts +++ b/src/main/events/library/get-library.ts @@ -7,7 +7,7 @@ const getLibrary = async () => isDeleted: false, }, order: { - createdAt: "desc", + updatedAt: "desc", }, }); diff --git a/src/main/main.ts b/src/main/main.ts index e12b5f8c..b81398cf 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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 diff --git a/src/main/services/index.ts b/src/main/services/index.ts index bada604e..93427179 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -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"; diff --git a/src/main/services/search-engine.ts b/src/main/services/search-engine.ts deleted file mode 100644 index 87b268d3..00000000 --- a/src/main/services/search-engine.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/main/workers/index.ts b/src/main/workers/index.ts index 00d51f91..fd17995e 100644 --- a/src/main/workers/index.ts +++ b/src/main/workers/index.ts @@ -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, +}); diff --git a/src/main/workers/repacks.worker.ts b/src/main/workers/repacks.worker.ts new file mode 100644 index 00000000..b781367e --- /dev/null +++ b/src/main/workers/repacks.worker.ts @@ -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 }; + }); +}; diff --git a/src/main/workers/steam-games.worker.ts b/src/main/workers/steam-games.worker.ts index 4a41e06e..8783c7a1 100644 --- a/src/main/workers/steam-games.worker.ts +++ b/src/main/workers/steam-games.worker.ts @@ -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"]); diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index 3a64455d..975940b9 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -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, -}); diff --git a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx index 708dbfed..937ca613 100644 --- a/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx +++ b/src/renderer/src/pages/game-details/hero/hero-panel-actions.tsx @@ -224,47 +224,51 @@ export function HeroPanelActions() { ); } - return ( - <> - {game?.progress === 1 ? ( - <> - setShowBinaryNotFoundModal(false)} - /> + if (game) { + return ( + <> + {game?.progress === 1 ? ( + <> + setShowBinaryNotFoundModal(false)} + /> + + + ) : ( + toggleGameOnLibraryButton + )} + + {isGameRunning ? ( + ) : ( + - - ) : ( - toggleGameOnLibraryButton - )} + )} + + ); + } - {isGameRunning ? ( - - ) : ( - - )} - - ); + return toggleGameOnLibraryButton; } diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index ffe1421a..c7d9bbde 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -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({ >

{validationResult?.name}

- Found {numberFormatter.format(validationResult?.downloadCount)}{" "} + Found{" "} + {validationResult?.downloadCount.toLocaleString(undefined)}{" "} download options diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index fbb766d4..330e92a6 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -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")}

+ + {downloadSources.map((downloadSource) => (
@@ -60,9 +69,7 @@ export function SettingsDownloadSources() { {t("download_options", { count: downloadSource.repackCount, - countFormatted: numberFormatter.format( - downloadSource.repackCount - ), + countFormatted: downloadSource.repackCount.toLocaleString(), })}
@@ -87,16 +94,6 @@ export function SettingsDownloadSources() {
))} - - ); }