From 3833e11e989690c204d7506e201fe401b0ba0bc5 Mon Sep 17 00:00:00 2001 From: Alexander Hostrup Date: Wed, 18 Sep 2024 18:04:32 +0200 Subject: [PATCH 1/9] Added a danish README --- README.be.md | 1 + README.cs.md | 1 + README.da.md | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ README.de.md | 1 + README.es.md | 1 + README.fr.md | 1 + README.it.md | 1 + README.md | 1 + README.pl.md | 1 + README.pt-BR.md | 1 + README.ru.md | 1 + README.uk-UA.md | 1 + 12 files changed, 197 insertions(+) create mode 100644 README.da.md diff --git a/README.be.md b/README.be.md index 8c104a81..f2b5243c 100644 --- a/README.be.md +++ b/README.be.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) diff --git a/README.cs.md b/README.cs.md index 86f869b9..83903406 100644 --- a/README.cs.md +++ b/README.cs.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Katalog](./docs/screenshot.png) diff --git a/README.da.md b/README.da.md new file mode 100644 index 00000000..c63b5abe --- /dev/null +++ b/README.da.md @@ -0,0 +1,186 @@ +
+ +
+ +[](https://hydralauncher.site) + +

Hydra Launcher

+ +

+ Hydra er en spil launcher med sin egen indbyggede bittorrent klient. +

+ +[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) +[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases) + +[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md) +[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) +[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md) +[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md) +[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md) +[![es](https://img.shields.io/badge/lang-es-red)](README.es.md) +[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) +[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) +[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) +[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) + +![Hydra Catalogue](./docs/screenshot.png) + +
+ +## Indholdsfortegnelse + +- [Indholdsfortegnelse](#indholdsfortegnelse) +- [Om](#om) +- [Funktioner](#funktioner) +- [Installation](#installation) +- [Bidrag](#-bidrag) + - [Bliv medlem af vores Telegram kanal](#-join-our-telegram) + - [Fork og klon dit repo](#fork-and-clone-your-repository) + - [Måder du kan bidrage](#ways-you-can-contribute) + - [Projekt Struktur](#project-structure) +- [Byg fra kildekode](#build-from-source) + - [Installér Node.js](#install-nodejs) + - [Installér Yarn](#install-yarn) + - [Installér Node Afhængigheder](#install-node-dependencies) + - [Installér Python 3.9](#install-python-39) + - [Installér Python Afhængigheder](#install-python-dependencies) +- [Miljøvariabler](#environment-variables) +- [Køre](#running) +- [Bygge](#build) + - [Bygge bittorrent klienten](#build-the-bittorrent-client) + - [Bygge Electron applikationen](#build-the-electron-application) +- [Bidragere](#contributors) +- [Licens](#license) + +## Om + +**Hydra** er en **Spil Launcher** med sin egen indbyggede **BitTorrent Klient**. +
+Launcheren er skrevet i TypeScript (Electron) og Python, som håndterer torrenting system ved brug af libtorrent. + +## Funktioner + +- Sin egen indbyggede bittorrent klient +- How Long To Beat (HLTB) integration på spil siden +- Downloadsti tilpasning +- Windows og Linux understøttelse +- Konstant opdateret +- Og mere ... + +## Installation + +Følg trinene her under for at installere: + +1. Download den seneste version af Hydra fra [Releases](https://github.com/hydralauncher/hydra/releases/latest) siden. + - Download kun .exe hvis du vil installere Hydra på Windows. + - Download .deb, .rpm eller .zip hvis du vil installere Hydra på Linux. (afhænger af din Linux distro) +2. Kør den downloadede fil. +3. Nyd Hydra! + +## Bidrag + +### Bliv medlem af vores Telegram kanal + +Vi holder vores diskusioner i vores [Telegram](https://t.me/hydralauncher) kanal. + +### Fork og klon dit repo + +1. Fork repoet [(klik her for at forke nu)](https://github.com/hydralauncher/hydra/fork) +2. Klon din forkede kode `git clone https://github.com/dit_brugernavn/hydra` +3. Lav en ny branch +4. Skub dine commits +5. Indsend en ny Pull Request + +### Måder du kan bidrage + +- Oversættelse: Vi vil gerne have at Hydra er tilgængeligt for så mange folk som overhovedet muligt. Du er velkommen til at hjælpe med at oversætte til nye sprog eller at opdatere og forbedre de sprog som allerede er tilgængelige i Hydra. +- Kode: Hydra er lavet med Typescript, Electron og en lille smule Python. Hvis du har lyst til at bidrage, kan du blive medlem af vores [Telegram](https://t.me/hydralauncher) kanal! (Alt kommunikation foregår hovedsageligt på Engelsk, Brasiliansk eller Russisk) + +### Projekt struktur + +- torrent-client: Vi bruger libtorrent, et Python bibliotek, til at administrere torrent downloads +- src/renderer: UI'en i applikationen +- src/main: her har vi al logikken + +## Byg fra kildekode + +### Installér Node.js + +Vær sikker på at du har Node.js installeret på din maskine. Hvis ikke, kan du downloade og installere det fra [nodejs.org](https://nodejs.org/). + +### Installér Yarn + +Yarn er et pakkehåndteringsprogram til Node.js. Hvis du ikke har installeret Yarn endnu, så kan du gøre det ved at følge instruktionerne på [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/). + +### Installér Node Afhængigheder + +Navigér til projekt mappen og installér Node afhængighederne ved bruge af Yarn: + +```bash +cd hydra +yarn +``` + +### Installér Python 3.9 + +Vær sikker på at du har Python 3.9 installeret på din maskine. Du kan downloade og installere det her: [python.org](https://www.python.org/downloads/release/python-3913/). + +### Installér Python Afhængigheder + +Installér de påkrævede Python afhængigheder ved brug af pip: + +```bash +pip install -r requirements.txt +``` + +## Miljøvariabler + +Du får brug for en SteamGridDB API nøgle for at kunne hente spil ikonerne under installationen. + +Når du har det, kan du kopiere og omdøbe `.env.example` filen til `.env` og indsætte nøglen som `STEAMGRIDDB_API_KEY`. + +## Køre + +Når alt er sat op, kan du køre den følgende kommando for at starte både Electron processen og bittorrent klienten: + +```bash +yarn dev +``` + +## Bygge + +### Byg bittorrent klienten + +Byg bittorrent klienten ved brug af følgende kommando: + +```bash +python torrent-client/setup.py build +``` + +### Byg Electron applikationen + +Build Electron applicationen ved brug af følgende kommando: + +På Windows: + +```bash +yarn build:win +``` + +På Linux: + +```bash +yarn build:linux +``` + +## Bidragere + + + + + +## Licens + +Hydra benytter sig af [MIT Licensen](LICENSE). diff --git a/README.de.md b/README.de.md index 3f6e9d90..143ad40b 100644 --- a/README.de.md +++ b/README.de.md @@ -23,6 +23,7 @@ [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Katalog](./docs/screenshot.png) diff --git a/README.es.md b/README.es.md index eead7276..2c5911a4 100644 --- a/README.es.md +++ b/README.es.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) diff --git a/README.fr.md b/README.fr.md index f34fd037..93541274 100644 --- a/README.fr.md +++ b/README.fr.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Catalogue Hydra](./docs/screenshot.png) diff --git a/README.it.md b/README.it.md index d8ca75fb..dfb5665a 100644 --- a/README.it.md +++ b/README.it.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) diff --git a/README.md b/README.md index 29ff6b24..21f808a8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) diff --git a/README.pl.md b/README.pl.md index 9120baac..512cc3d6 100644 --- a/README.pl.md +++ b/README.pl.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) diff --git a/README.pt-BR.md b/README.pt-BR.md index 610fc5c5..383124a6 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) diff --git a/README.ru.md b/README.ru.md index 29fda4a3..3b846c4e 100644 --- a/README.ru.md +++ b/README.ru.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) diff --git a/README.uk-UA.md b/README.uk-UA.md index ed37aeca..69bb69f7 100644 --- a/README.uk-UA.md +++ b/README.uk-UA.md @@ -23,6 +23,7 @@ [![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md) +[![da](https://img.shields.io/badge/lang-da-red)](README.da.md) ![Hydra Catalogue](./docs/screenshot.png) From f8f2124cec0fd8cdeb591f74103cff8d9659ff72 Mon Sep 17 00:00:00 2001 From: Alexander Hostrup Date: Wed, 18 Sep 2024 18:08:30 +0200 Subject: [PATCH 2/9] Forgot something --- README.da.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.da.md b/README.da.md index c63b5abe..9f0eb7f7 100644 --- a/README.da.md +++ b/README.da.md @@ -161,7 +161,7 @@ python torrent-client/setup.py build ### Byg Electron applikationen -Build Electron applicationen ved brug af følgende kommando: +Byg Electron applikationen ved brug af følgende kommando: På Windows: From 849b6de6bce76bbf173ed72fa2ffaee2cece67c5 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sat, 21 Sep 2024 21:19:00 +0100 Subject: [PATCH 3/9] feat: adding dexie --- package.json | 1 + src/main/events/catalogue/get-catalogue.ts | 9 +-- .../events/catalogue/get-game-shop-details.ts | 20 ++--- src/main/events/catalogue/get-games.ts | 10 +-- src/main/events/catalogue/get-repacks.ts | 7 ++ .../events/catalogue/search-game-repacks.ts | 9 --- .../download-sources/add-download-source.ts | 3 - .../download-sources/get-download-sources.ts | 8 +- .../remove-download-source.ts | 5 +- src/main/events/helpers/search-games.ts | 1 - src/main/events/index.ts | 2 +- .../events/torrenting/start-game-download.ts | 27 ++----- src/main/index.ts | 1 - src/main/main.ts | 9 +-- src/preload/index.ts | 2 + src/renderer/src/app.tsx | 78 ++++++++++--------- .../src/components/game-card/game-card.tsx | 18 ++++- .../game-details/game-details.context.tsx | 58 ++++++++------ src/renderer/src/context/index.ts | 1 + .../src/context/repacks/repacks.context.tsx | 58 ++++++++++++++ src/renderer/src/declaration.d.ts | 2 + src/renderer/src/dexie.ts | 13 ++++ src/renderer/src/main.tsx | 2 + .../settings/add-download-source-modal.tsx | 4 + .../settings/settings-download-sources.tsx | 9 ++- .../src/workers/download-sources.worker.ts | 8 ++ src/renderer/src/workers/index.ts | 24 ++++++ src/renderer/src/workers/migration.worker.ts | 32 ++++++++ src/renderer/src/workers/repacks.worker.ts | 52 +++++++++++++ src/types/index.ts | 2 - yarn.lock | 5 ++ 31 files changed, 338 insertions(+), 142 deletions(-) create mode 100644 src/main/events/catalogue/get-repacks.ts delete mode 100644 src/main/events/catalogue/search-game-repacks.ts create mode 100644 src/renderer/src/context/repacks/repacks.context.tsx create mode 100644 src/renderer/src/dexie.ts create mode 100644 src/renderer/src/workers/download-sources.worker.ts create mode 100644 src/renderer/src/workers/index.ts create mode 100644 src/renderer/src/workers/migration.worker.ts create mode 100644 src/renderer/src/workers/repacks.worker.ts diff --git a/package.json b/package.json index c9f3885f..c00d3d1b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "color.js": "^1.2.0", "create-desktop-shortcuts": "^1.11.0", "date-fns": "^3.6.0", + "dexie": "^4.0.8", "electron-log": "^5.1.4", "electron-updater": "^6.1.8", "fetch-cookie": "^3.0.1", diff --git a/src/main/events/catalogue/get-catalogue.ts b/src/main/events/catalogue/get-catalogue.ts index 8d6183a5..4fdb95bd 100644 --- a/src/main/events/catalogue/get-catalogue.ts +++ b/src/main/events/catalogue/get-catalogue.ts @@ -1,8 +1,8 @@ import type { GameShop } from "@types"; import { registerEvent } from "../register-event"; -import { HydraApi, RepacksManager } from "@main/services"; -import { CatalogueCategory, formatName, steamUrlBuilder } from "@shared"; +import { HydraApi } from "@main/services"; +import { CatalogueCategory, steamUrlBuilder } from "@shared"; import { steamGamesWorker } from "@main/workers"; const getCatalogue = async ( @@ -26,14 +26,9 @@ const getCatalogue = async ( name: "getById", }); - const repacks = RepacksManager.search({ - query: formatName(steamGame.name), - }); - return { title: steamGame.name, shop: game.shop, - repacks, cover: steamUrlBuilder.library(game.objectId), objectID: game.objectId, }; diff --git a/src/main/events/catalogue/get-game-shop-details.ts b/src/main/events/catalogue/get-game-shop-details.ts index 0b4535f6..3a435013 100644 --- a/src/main/events/catalogue/get-game-shop-details.ts +++ b/src/main/events/catalogue/get-game-shop-details.ts @@ -45,15 +45,17 @@ const getGameShopDetails = async ( const appDetails = getLocalizedSteamAppDetails(objectID, language).then( (result) => { - gameShopCacheRepository.upsert( - { - objectID, - shop: "steam", - language, - serializedData: JSON.stringify(result), - }, - ["objectID"] - ); + if (result) { + gameShopCacheRepository.upsert( + { + objectID, + shop: "steam", + language, + serializedData: JSON.stringify(result), + }, + ["objectID"] + ); + } return result; } diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts index c34451eb..859a0de5 100644 --- a/src/main/events/catalogue/get-games.ts +++ b/src/main/events/catalogue/get-games.ts @@ -2,8 +2,6 @@ import type { CatalogueEntry } from "@types"; import { registerEvent } from "../register-event"; import { steamGamesWorker } from "@main/workers"; -import { convertSteamGameToCatalogueEntry } from "../helpers/search-games"; -import { RepacksManager } from "@main/services"; const getGames = async ( _event: Electron.IpcMainInvokeEvent, @@ -15,13 +13,9 @@ const getGames = async ( { name: "list" } ); - const entries = RepacksManager.findRepacksForCatalogueEntries( - steamGames.map((game) => convertSteamGameToCatalogueEntry(game)) - ); - return { - results: entries, - cursor: cursor + entries.length, + results: steamGames, + cursor: cursor + steamGames.length, }; }; diff --git a/src/main/events/catalogue/get-repacks.ts b/src/main/events/catalogue/get-repacks.ts new file mode 100644 index 00000000..db39fc7e --- /dev/null +++ b/src/main/events/catalogue/get-repacks.ts @@ -0,0 +1,7 @@ +import { registerEvent } from "../register-event"; +import { knexClient } from "@main/knex-client"; + +const getRepacks = (_event: Electron.IpcMainInvokeEvent) => + knexClient.select("*").from("repack"); + +registerEvent("getRepacks", getRepacks); diff --git a/src/main/events/catalogue/search-game-repacks.ts b/src/main/events/catalogue/search-game-repacks.ts deleted file mode 100644 index e3b9c2b5..00000000 --- a/src/main/events/catalogue/search-game-repacks.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RepacksManager } from "@main/services"; -import { registerEvent } from "../register-event"; - -const searchGameRepacks = ( - _event: Electron.IpcMainInvokeEvent, - query: string -) => RepacksManager.search({ query }); - -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 b0c0e470..b762c95d 100644 --- a/src/main/events/download-sources/add-download-source.ts +++ b/src/main/events/download-sources/add-download-source.ts @@ -4,7 +4,6 @@ import { DownloadSource } from "@main/entity"; import axios from "axios"; import { downloadSourceSchema } from "../helpers/validators"; import { insertDownloadsFromSource } from "@main/helpers"; -import { RepacksManager } from "@main/services"; const addDownloadSource = async ( _event: Electron.IpcMainInvokeEvent, @@ -34,8 +33,6 @@ const addDownloadSource = async ( } ); - await RepacksManager.updateRepacks(); - return downloadSource; }; diff --git a/src/main/events/download-sources/get-download-sources.ts b/src/main/events/download-sources/get-download-sources.ts index b8565645..97f8a6d8 100644 --- a/src/main/events/download-sources/get-download-sources.ts +++ b/src/main/events/download-sources/get-download-sources.ts @@ -1,11 +1,7 @@ -import { downloadSourceRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { knexClient } from "@main/knex-client"; const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => - downloadSourceRepository.find({ - order: { - createdAt: "DESC", - }, - }); + knexClient.select("*").from("download_source"); registerEvent("getDownloadSources", getDownloadSources); diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts index 73f2ffbe..8d67df13 100644 --- a/src/main/events/download-sources/remove-download-source.ts +++ b/src/main/events/download-sources/remove-download-source.ts @@ -5,9 +5,6 @@ import { RepacksManager } from "@main/services"; const removeDownloadSource = async ( _event: Electron.IpcMainInvokeEvent, id: number -) => { - await downloadSourceRepository.delete(id); - await RepacksManager.updateRepacks(); -}; +) => downloadSourceRepository.delete(id); registerEvent("removeDownloadSource", removeDownloadSource); diff --git a/src/main/events/helpers/search-games.ts b/src/main/events/helpers/search-games.ts index 5fb5098e..58e9bc92 100644 --- a/src/main/events/helpers/search-games.ts +++ b/src/main/events/helpers/search-games.ts @@ -17,7 +17,6 @@ export const convertSteamGameToCatalogueEntry = ( title: game.name, shop: "steam" as GameShop, cover: steamUrlBuilder.library(String(game.id)), - repacks: [], }); export const getSteamGameById = async ( diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 54e63a3b..0638f900 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -7,9 +7,9 @@ import "./catalogue/get-games"; import "./catalogue/get-how-long-to-beat"; import "./catalogue/get-random-game"; import "./catalogue/search-games"; -import "./catalogue/search-game-repacks"; import "./catalogue/get-game-stats"; import "./catalogue/get-trending-games"; +import "./catalogue/get-repacks"; import "./hardware/get-disk-free-space"; import "./library/add-game-to-library"; import "./library/create-game-shortcut"; diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 253ab159..491083cb 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -9,36 +9,25 @@ import { steamGamesWorker } from "@main/workers"; import { createGame } from "@main/services/library-sync"; import { steamUrlBuilder } from "@shared"; import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game, Repack } from "@main/entity"; +import { DownloadQueue, Game } from "@main/entity"; const startGameDownload = async ( _event: Electron.IpcMainInvokeEvent, payload: StartGameDownloadPayload ) => { - const { repackId, objectID, title, shop, downloadPath, downloader, uri } = - payload; + const { objectID, title, shop, downloadPath, downloader, uri } = payload; return dataSource.transaction(async (transactionalEntityManager) => { const gameRepository = transactionalEntityManager.getRepository(Game); - const repackRepository = transactionalEntityManager.getRepository(Repack); const downloadQueueRepository = transactionalEntityManager.getRepository(DownloadQueue); - const [game, repack] = await Promise.all([ - gameRepository.findOne({ - where: { - objectID, - shop, - }, - }), - repackRepository.findOne({ - where: { - id: repackId, - }, - }), - ]); - - if (!repack) return; + const game = await gameRepository.findOne({ + where: { + objectID, + shop, + }, + }); await DownloadManager.pauseDownload(); diff --git a/src/main/index.ts b/src/main/index.ts index 00311b46..594220c5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -68,7 +68,6 @@ const runMigrations = async () => { }); await knexClient.migrate.latest(migrationConfig); - await knexClient.destroy(); }; // This method will be called when Electron has finished diff --git a/src/main/main.ts b/src/main/main.ts index af594e20..690282f6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,9 +1,4 @@ -import { - DownloadManager, - RepacksManager, - PythonInstance, - startMainLoop, -} from "./services"; +import { DownloadManager, PythonInstance, startMainLoop } from "./services"; import { downloadQueueRepository, repackRepository, @@ -18,8 +13,6 @@ import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; const loadState = async (userPreferences: UserPreferences | null) => { - RepacksManager.updateRepacks(); - import("./events"); if (userPreferences?.realDebridApiToken) { diff --git a/src/preload/index.ts b/src/preload/index.ts index 0f135b99..38df190d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -49,6 +49,8 @@ contextBridge.exposeInMainWorld("electron", { getGameStats: (objectId: string, shop: GameShop) => ipcRenderer.invoke("getGameStats", objectId, shop), getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"), + /* Meant for Dexie migration */ + getRepacks: () => ipcRenderer.invoke("getRepacks"), /* User preferences */ getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"), diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 5b9e44ca..7b1a2c03 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -26,6 +26,10 @@ import { } from "@renderer/features"; import { useTranslation } from "react-i18next"; import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; +import { RepacksContextProvider } from "./context"; +import { downloadSourcesWorker } from "./workers"; + +downloadSourcesWorker.postMessage("OK"); export interface AppProps { children: React.ReactNode; @@ -197,7 +201,7 @@ export function App() { useEffect(() => { new MutationObserver(() => { - const modal = document.body.querySelector("[role=modal]"); + const modal = document.body.querySelector("[role=dialog]"); dispatch(toggleDraggingDisabled(Boolean(modal))); }).observe(document.body, { @@ -211,46 +215,48 @@ export function App() { }, [dispatch]); return ( - <> - {window.electron.platform === "win32" && ( -
-

Hydra

-
- )} + + <> + {window.electron.platform === "win32" && ( +
+

Hydra

+
+ )} - - - {userDetails && ( - - )} -
- - -
-
+ )} -
- -
-
-
+
+ - - +
+
+ +
+ +
+
+
+ + + +
); } diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index 7181e9b3..9d54bad8 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -1,13 +1,14 @@ import { DownloadIcon, PeopleIcon } from "@primer/octicons-react"; -import type { CatalogueEntry, GameStats } from "@types"; +import type { CatalogueEntry, GameRepack, GameStats } from "@types"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import * as styles from "./game-card.css"; import { useTranslation } from "react-i18next"; import { Badge } from "../badge/badge"; -import { useCallback, useState } from "react"; +import { useCallback, useContext, useEffect, useState } from "react"; import { useFormat } from "@renderer/hooks"; +import { repacksContext } from "@renderer/context"; export interface GameCardProps extends React.DetailedHTMLProps< @@ -25,9 +26,20 @@ export function GameCard({ game, ...props }: GameCardProps) { const { t } = useTranslation("game_card"); const [stats, setStats] = useState(null); + const [repacks, setRepacks] = useState([]); + + const { searchRepacks, isIndexingRepacks } = useContext(repacksContext); + + useEffect(() => { + if (!isIndexingRepacks) { + searchRepacks(game.title).then((repacks) => { + setRepacks(repacks); + }); + } + }, [game, isIndexingRepacks, searchRepacks]); const uniqueRepackers = Array.from( - new Set(game.repacks.map(({ repacker }) => repacker)) + new Set(repacks.map(({ repacker }) => repacker)) ); const handleHover = useCallback(() => { diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index e723779f..120728b1 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -1,4 +1,10 @@ -import { createContext, useCallback, useEffect, useState } from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { setHeaderTitle } from "@renderer/features"; @@ -16,6 +22,7 @@ import type { import { useTranslation } from "react-i18next"; import { GameDetailsContext } from "./game-details.context.types"; import { SteamContentDescriptor } from "@shared"; +import { repacksContext } from "../repacks/repacks.context"; export const gameDetailsContext = createContext({ game: null, @@ -52,7 +59,6 @@ export function GameDetailsContextProvider({ const { objectID, shop } = useParams(); const [shopDetails, setShopDetails] = useState(null); - const [repacks, setRepacks] = useState([]); const [game, setGame] = useState(null); const [hasNSFWContentBlocked, setHasNSFWContentBlocked] = useState(false); @@ -64,10 +70,22 @@ export function GameDetailsContextProvider({ const [showRepacksModal, setShowRepacksModal] = useState(false); const [showGameOptionsModal, setShowGameOptionsModal] = useState(false); + const [repacks, setRepacks] = useState([]); + const [searchParams] = useSearchParams(); const gameTitle = searchParams.get("title")!; + const { searchRepacks, isIndexingRepacks } = useContext(repacksContext); + + useEffect(() => { + if (!isIndexingRepacks) { + searchRepacks(gameTitle).then((repacks) => { + setRepacks(repacks); + }); + } + }, [game, gameTitle, isIndexingRepacks, searchRepacks]); + const { i18n } = useTranslation("game_details"); const dispatch = useAppDispatch(); @@ -91,37 +109,31 @@ export function GameDetailsContextProvider({ }, [updateGame, isGameDownloading, lastPacket?.game.status]); useEffect(() => { - Promise.allSettled([ - window.electron.getGameShopDetails( + window.electron + .getGameShopDetails( objectID!, shop as GameShop, getSteamLanguage(i18n.language) - ), - window.electron.searchGameRepacks(gameTitle), - window.electron.getGameStats(objectID!, shop as GameShop), - ]) - .then(([appDetailsResult, repacksResult, statsResult]) => { - if (appDetailsResult.status === "fulfilled") { - setShopDetails(appDetailsResult.value); + ) + .then((result) => { + setShopDetails(result); - if ( - appDetailsResult.value?.content_descriptors.ids.includes( - SteamContentDescriptor.AdultOnlySexualContent - ) - ) { - setHasNSFWContentBlocked(true); - } + if ( + result?.content_descriptors.ids.includes( + SteamContentDescriptor.AdultOnlySexualContent + ) + ) { + setHasNSFWContentBlocked(true); } - - if (repacksResult.status === "fulfilled") - setRepacks(repacksResult.value); - - if (statsResult.status === "fulfilled") setStats(statsResult.value); }) .finally(() => { setIsLoading(false); }); + window.electron.getGameStats(objectID!, shop as GameShop).then((result) => { + setStats(result); + }); + updateGame(); }, [updateGame, dispatch, gameTitle, objectID, shop, i18n.language]); diff --git a/src/renderer/src/context/index.ts b/src/renderer/src/context/index.ts index d9c1c7e4..8d8b9223 100644 --- a/src/renderer/src/context/index.ts +++ b/src/renderer/src/context/index.ts @@ -1,3 +1,4 @@ export * from "./game-details/game-details.context"; export * from "./settings/settings.context"; export * from "./user-profile/user-profile.context"; +export * from "./repacks/repacks.context"; diff --git a/src/renderer/src/context/repacks/repacks.context.tsx b/src/renderer/src/context/repacks/repacks.context.tsx new file mode 100644 index 00000000..a2e4101b --- /dev/null +++ b/src/renderer/src/context/repacks/repacks.context.tsx @@ -0,0 +1,58 @@ +import type { GameRepack } from "@types"; +import { createContext, useCallback, useEffect, useState } from "react"; + +import { repacksWorker } from "@renderer/workers"; + +export interface RepacksContext { + searchRepacks: (query: string) => Promise; + isIndexingRepacks: boolean; +} + +export const repacksContext = createContext({ + searchRepacks: async () => [] as GameRepack[], + isIndexingRepacks: false, +}); + +const { Provider } = repacksContext; +export const { Consumer: RepacksContextConsumer } = repacksContext; + +export interface RepacksContextProps { + children: React.ReactNode; +} + +export function RepacksContextProvider({ children }: RepacksContextProps) { + const [isIndexingRepacks, setIsIndexingRepacks] = useState(true); + + const searchRepacks = useCallback(async (query: string) => { + return new Promise((resolve) => { + const channelId = crypto.randomUUID(); + repacksWorker.postMessage([channelId, query]); + + const channel = new BroadcastChannel(`repacks:search:${channelId}`); + channel.onmessage = (event: MessageEvent) => { + resolve(event.data); + }; + + return []; + }); + }, []); + + useEffect(() => { + repacksWorker.postMessage("INDEX_REPACKS"); + + repacksWorker.onmessage = () => { + setIsIndexingRepacks(false); + }; + }, []); + + return ( + + {children} + + ); +} diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 70b77eec..3673ec08 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -65,6 +65,8 @@ declare global { searchGameRepacks: (query: string) => Promise; getGameStats: (objectId: string, shop: GameShop) => Promise; getTrendingGames: () => Promise; + /* Meant for Dexie migration */ + getRepacks: () => Promise; /* Library */ addGameToLibrary: ( diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts new file mode 100644 index 00000000..2b9f0aa6 --- /dev/null +++ b/src/renderer/src/dexie.ts @@ -0,0 +1,13 @@ +import { Dexie } from "dexie"; + +export const db = new Dexie("Hydra"); + +db.version(1).stores({ + repacks: `++id, title, uri, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, + downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`, +}); + +export const downloadSourcesTable = db.table("downloadSources"); +export const repacksTable = db.table("repacks"); + +db.open(); diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index b98d5ed9..5d9b2197 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -29,6 +29,8 @@ import { store } from "./store"; import resources from "@locales"; +import "./workers"; + Sentry.init({}); i18n 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 015ee0dc..fba890d1 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -8,6 +8,7 @@ import { useForm } from "react-hook-form"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; +import { downloadSourcesTable } from "@renderer/dexie"; interface AddDownloadSourceModalProps { visible: boolean; @@ -91,6 +92,9 @@ export function AddDownloadSourceModal({ }, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]); const handleAddDownloadSource = async () => { + await downloadSourcesTable.add({ + url, + }); await window.electron.addDownloadSource(url); onClose(); onAddDownloadSource(); diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 1646af22..53c14348 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -11,6 +11,7 @@ import { useToast } from "@renderer/hooks"; import { DownloadSourceStatus } from "@shared"; import { SPACING_UNIT } from "@renderer/theme.css"; import { settingsContext } from "@renderer/context"; +import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie"; export function SettingsDownloadSources() { const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] = @@ -25,7 +26,7 @@ export function SettingsDownloadSources() { const { showSuccessToast } = useToast(); const getDownloadSources = async () => { - return window.electron.getDownloadSources().then((sources) => { + downloadSourcesTable.toArray().then((sources) => { setDownloadSources(sources); }); }; @@ -39,7 +40,11 @@ export function SettingsDownloadSources() { }, [sourceUrl]); const handleRemoveSource = async (id: number) => { - await window.electron.removeDownloadSource(id); + await db.transaction("rw", downloadSourcesTable, repacksTable, async () => { + await downloadSourcesTable.where({ id }).delete(); + await repacksTable.where({ downloadSourceId: id }).delete(); + }); + showSuccessToast(t("removed_download_source")); getDownloadSources(); diff --git a/src/renderer/src/workers/download-sources.worker.ts b/src/renderer/src/workers/download-sources.worker.ts new file mode 100644 index 00000000..609b6bcf --- /dev/null +++ b/src/renderer/src/workers/download-sources.worker.ts @@ -0,0 +1,8 @@ +import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie"; + +self.onmessage = () => { + db.transaction("rw", repacksTable, downloadSourcesTable, async () => { + await repacksTable.where({ downloadSourceId: 10 }).delete(); + await downloadSourcesTable.where({ id: 10 }).delete(); + }); +}; diff --git a/src/renderer/src/workers/index.ts b/src/renderer/src/workers/index.ts new file mode 100644 index 00000000..9a5ab920 --- /dev/null +++ b/src/renderer/src/workers/index.ts @@ -0,0 +1,24 @@ +import MigrationWorker from "./migration.worker?worker"; +import RepacksWorker from "./repacks.worker?worker"; +import DownloadSourcesWorker from "./download-sources.worker?worker"; + +// const migrationWorker = new MigrationWorker(); +export const repacksWorker = new RepacksWorker(); +export const downloadSourcesWorker = new DownloadSourcesWorker(); + +// window.electron.getRepacks().then((repacks) => { +// console.log(repacks); +// migrationWorker.postMessage(["MIGRATE_REPACKS", repacks]); +// }); + +// window.electron.getDownloadSources().then((downloadSources) => { +// migrationWorker.postMessage(["MIGRATE_DOWNLOAD_SOURCES", downloadSources]); +// }); + +// migrationWorker.onmessage = (event) => { +// console.log(event.data); +// }; + +// setTimeout(() => { +// repacksWorker.postMessage("god"); +// }, 500); diff --git a/src/renderer/src/workers/migration.worker.ts b/src/renderer/src/workers/migration.worker.ts new file mode 100644 index 00000000..848dd052 --- /dev/null +++ b/src/renderer/src/workers/migration.worker.ts @@ -0,0 +1,32 @@ +import { downloadSourcesTable, repacksTable } from "@renderer/dexie"; +import { DownloadSource, GameRepack } from "@types"; + +export type Payload = + | ["MIGRATE_REPACKS", GameRepack[]] + | ["MIGRATE_DOWNLOAD_SOURCES", DownloadSource[]]; + +self.onmessage = async (event: MessageEvent) => { + const [type, data] = event.data; + + if (type === "MIGRATE_DOWNLOAD_SOURCES") { + const dexieDownloadSources = await downloadSourcesTable.count(); + + if (data.length !== dexieDownloadSources) { + await downloadSourcesTable.clear(); + await downloadSourcesTable.bulkAdd(data); + } + + self.postMessage("MIGRATE_DOWNLOAD_SOURCES_COMPLETE"); + } + + if (type === "MIGRATE_REPACKS") { + const dexieRepacks = await repacksTable.count(); + + if (data.length !== dexieRepacks) { + await repacksTable.clear(); + await repacksTable.bulkAdd(data); + } + + self.postMessage("MIGRATE_REPACKS_COMPLETE"); + } +}; diff --git a/src/renderer/src/workers/repacks.worker.ts b/src/renderer/src/workers/repacks.worker.ts new file mode 100644 index 00000000..0e3a9ce7 --- /dev/null +++ b/src/renderer/src/workers/repacks.worker.ts @@ -0,0 +1,52 @@ +import { repacksTable } from "@renderer/dexie"; +import { formatName } from "@shared"; +import { GameRepack } from "@types"; +import flexSearch from "flexsearch"; + +const index = new flexSearch.Index(); + +const state = { + repacks: [] as any[], +}; + +interface SerializedGameRepack extends Omit { + uris: string; +} + +self.onmessage = async ( + event: MessageEvent<[string, string] | "INDEX_REPACKS"> +) => { + if (event.data === "INDEX_REPACKS") { + repacksTable + .toCollection() + .sortBy("uploadDate") + .then((results) => { + state.repacks = results.reverse(); + + for (let i = 0; i < state.repacks.length; i++) { + const repack = state.repacks[i]; + const formattedTitle = formatName(repack.title); + index.add(i, formattedTitle); + } + + self.postMessage("INDEXING_COMPLETE"); + }); + } else { + const [requestId, query] = event.data; + + const results = index.search(formatName(query)).map((index) => { + const repack = state.repacks.at(index as number) as SerializedGameRepack; + + const uris = JSON.parse(repack.uris); + + return { + ...repack, + uris: [...uris, repack.magnet].filter(Boolean), + }; + }); + + const channel = new BroadcastChannel(`repacks:search:${requestId}`); + + channel.postMessage(results); + } +}; diff --git a/src/types/index.ts b/src/types/index.ts index 5b961dd6..a8fb3771 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -44,7 +44,6 @@ export interface CatalogueEntry { title: string; /* Epic Games covers cannot be guessed with objectID */ cover: string; - repacks: GameRepack[]; } export interface UserGame { @@ -71,7 +70,6 @@ export interface Game { status: GameStatus | null; folderName: string; downloadPath: string | null; - repacks: GameRepack[]; progress: number; bytesDownloaded: number; playTimeInMilliseconds: number; diff --git a/yarn.lock b/yarn.lock index 9aa73bd4..14651b4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3638,6 +3638,11 @@ detect-node@^2.0.4: resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +dexie@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-4.0.8.tgz#21fca70686bdaa1d86fad45b6b19316f6a084a1d" + integrity sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" From f860439fb527b8116eec62bcf9453a684ddf275d Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Sun, 22 Sep 2024 17:43:05 +0100 Subject: [PATCH 4/9] feat: adding dexie --- src/main/events/catalogue/get-games.ts | 8 +- .../download-sources/add-download-source.ts | 39 ------- .../remove-download-source.ts | 10 -- .../download-sources/sync-download-sources.ts | 12 ++- .../validate-download-source.ts | 23 +--- src/main/events/index.ts | 2 - src/main/events/user/get-user.ts | 1 - src/main/helpers/download-source.ts | 76 ------------- src/main/helpers/index.ts | 4 +- src/main/main.ts | 26 ++--- ...e.worker.ts => download-sources.worker.ts} | 20 +--- src/main/workers/index.ts | 6 +- src/preload/index.ts | 8 +- src/renderer/src/app.tsx | 101 ++++++++++-------- .../src/context/repacks/repacks.context.tsx | 10 +- src/renderer/src/declaration.d.ts | 7 +- src/renderer/src/dexie.ts | 2 +- src/renderer/src/main.tsx | 29 ++--- .../settings/add-download-source-modal.tsx | 71 ++++++------ .../settings/settings-download-sources.tsx | 47 +++++--- .../src/workers/download-sources.worker.ts | 65 ++++++++++- src/renderer/src/workers/index.ts | 19 +--- src/renderer/src/workers/migration.worker.ts | 53 +++++---- src/renderer/src/workers/repacks.worker.ts | 4 +- src/types/index.ts | 13 +++ 25 files changed, 311 insertions(+), 345 deletions(-) delete mode 100644 src/main/events/download-sources/add-download-source.ts delete mode 100644 src/main/events/download-sources/remove-download-source.ts delete mode 100644 src/main/helpers/download-source.ts rename src/main/workers/{download-source.worker.ts => download-sources.worker.ts} (74%) diff --git a/src/main/events/catalogue/get-games.ts b/src/main/events/catalogue/get-games.ts index 859a0de5..81717806 100644 --- a/src/main/events/catalogue/get-games.ts +++ b/src/main/events/catalogue/get-games.ts @@ -2,6 +2,7 @@ import type { CatalogueEntry } from "@types"; import { registerEvent } from "../register-event"; import { steamGamesWorker } from "@main/workers"; +import { steamUrlBuilder } from "@shared"; const getGames = async ( _event: Electron.IpcMainInvokeEvent, @@ -14,7 +15,12 @@ const getGames = async ( ); return { - results: steamGames, + results: steamGames.map((steamGame) => ({ + title: steamGame.name, + shop: "steam", + cover: steamUrlBuilder.library(steamGame.id), + objectID: steamGame.id, + })), cursor: cursor + steamGames.length, }; }; diff --git a/src/main/events/download-sources/add-download-source.ts b/src/main/events/download-sources/add-download-source.ts deleted file mode 100644 index b762c95d..00000000 --- a/src/main/events/download-sources/add-download-source.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { registerEvent } from "../register-event"; -import { dataSource } from "@main/data-source"; -import { DownloadSource } from "@main/entity"; -import axios from "axios"; -import { downloadSourceSchema } from "../helpers/validators"; -import { insertDownloadsFromSource } from "@main/helpers"; - -const addDownloadSource = async ( - _event: Electron.IpcMainInvokeEvent, - url: string -) => { - const response = await axios.get(url); - - const source = downloadSourceSchema.parse(response.data); - - const downloadSource = await dataSource.transaction( - async (transactionalEntityManager) => { - const downloadSource = await transactionalEntityManager - .getRepository(DownloadSource) - .save({ - url, - name: source.name, - downloadCount: source.downloads.length, - }); - - await insertDownloadsFromSource( - transactionalEntityManager, - downloadSource, - source.downloads - ); - - return downloadSource; - } - ); - - return downloadSource; -}; - -registerEvent("addDownloadSource", addDownloadSource); diff --git a/src/main/events/download-sources/remove-download-source.ts b/src/main/events/download-sources/remove-download-source.ts deleted file mode 100644 index 8d67df13..00000000 --- a/src/main/events/download-sources/remove-download-source.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { downloadSourceRepository } from "@main/repository"; -import { registerEvent } from "../register-event"; -import { RepacksManager } from "@main/services"; - -const removeDownloadSource = async ( - _event: Electron.IpcMainInvokeEvent, - id: number -) => downloadSourceRepository.delete(id); - -registerEvent("removeDownloadSource", removeDownloadSource); diff --git a/src/main/events/download-sources/sync-download-sources.ts b/src/main/events/download-sources/sync-download-sources.ts index 2e000e64..49380f30 100644 --- a/src/main/events/download-sources/sync-download-sources.ts +++ b/src/main/events/download-sources/sync-download-sources.ts @@ -1,7 +1,13 @@ +import { downloadSourcesWorker } from "@main/workers"; import { registerEvent } from "../register-event"; -import { fetchDownloadSourcesAndUpdate } from "@main/helpers"; +import type { DownloadSource } from "@types"; -const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => - fetchDownloadSourcesAndUpdate(); +const syncDownloadSources = async ( + _event: Electron.IpcMainInvokeEvent, + downloadSources: DownloadSource[] +) => + downloadSourcesWorker.run(downloadSources, { + name: "getUpdatedRepacks", + }); registerEvent("syncDownloadSources", syncDownloadSources); diff --git a/src/main/events/download-sources/validate-download-source.ts b/src/main/events/download-sources/validate-download-source.ts index fdb67961..4f43ca08 100644 --- a/src/main/events/download-sources/validate-download-source.ts +++ b/src/main/events/download-sources/validate-download-source.ts @@ -1,27 +1,12 @@ import { registerEvent } from "../register-event"; -import { downloadSourceRepository } from "@main/repository"; -import { RepacksManager } from "@main/services"; -import { downloadSourceWorker } from "@main/workers"; +import { downloadSourcesWorker } from "@main/workers"; const validateDownloadSource = async ( _event: Electron.IpcMainInvokeEvent, url: string -) => { - const existingSource = await downloadSourceRepository.findOne({ - where: { url }, +) => + downloadSourcesWorker.run(url, { + name: "validateDownloadSource", }); - if (existingSource) - throw new Error("Source with the same url already exists"); - - const repacks = RepacksManager.repacks; - - return downloadSourceWorker.run( - { url, repacks }, - { - name: "validateDownloadSource", - } - ); -}; - registerEvent("validateDownloadSource", validateDownloadSource); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 5e2c17a1..73bf38f4 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -39,8 +39,6 @@ import "./autoupdater/restart-and-install-update"; import "./user-preferences/authenticate-real-debrid"; import "./download-sources/get-download-sources"; import "./download-sources/validate-download-source"; -import "./download-sources/add-download-source"; -import "./download-sources/remove-download-source"; import "./download-sources/sync-download-sources"; import "./auth/sign-out"; import "./auth/open-auth-window"; diff --git a/src/main/events/user/get-user.ts b/src/main/events/user/get-user.ts index b8bd7a0a..6bbab9c4 100644 --- a/src/main/events/user/get-user.ts +++ b/src/main/events/user/get-user.ts @@ -73,7 +73,6 @@ const getUser = async ( recentGames, }; } catch (err) { - console.log(err); return null; } }; diff --git a/src/main/helpers/download-source.ts b/src/main/helpers/download-source.ts deleted file mode 100644 index c216212a..00000000 --- a/src/main/helpers/download-source.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { dataSource } from "@main/data-source"; -import { DownloadSource, Repack } from "@main/entity"; -import { downloadSourceSchema } from "@main/events/helpers/validators"; -import { downloadSourceRepository } from "@main/repository"; -import { RepacksManager } from "@main/services"; -import { downloadSourceWorker } from "@main/workers"; -import { chunk } from "lodash-es"; -import type { EntityManager } from "typeorm"; -import type { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; -import { z } from "zod"; - -export const insertDownloadsFromSource = async ( - trx: EntityManager, - downloadSource: DownloadSource, - downloads: z.infer["downloads"] -) => { - const repacks: QueryDeepPartialEntity[] = downloads.map( - (download) => ({ - title: download.title, - uris: JSON.stringify(download.uris), - magnet: download.uris[0]!, - fileSize: download.fileSize, - repacker: downloadSource.name, - uploadDate: download.uploadDate, - downloadSource: { id: downloadSource.id }, - }) - ); - - const downloadsChunks = chunk(repacks, 800); - - for (const chunk of downloadsChunks) { - await trx - .getRepository(Repack) - .createQueryBuilder() - .insert() - .values(chunk) - .updateEntity(false) - .orIgnore() - .execute(); - } -}; - -export const fetchDownloadSourcesAndUpdate = async () => { - const downloadSources = await downloadSourceRepository.find({ - order: { - id: "desc", - }, - }); - - const results = await downloadSourceWorker.run(downloadSources, { - name: "getUpdatedRepacks", - }); - - await dataSource.transaction(async (transactionalEntityManager) => { - for (const result of results) { - if (result.etag !== null) { - await transactionalEntityManager.getRepository(DownloadSource).update( - { id: result.id }, - { - etag: result.etag, - status: result.status, - downloadCount: result.downloads.length, - } - ); - - await insertDownloadsFromSource( - transactionalEntityManager, - result, - result.downloads - ); - } - } - - await RepacksManager.updateRepacks(); - }); -}; diff --git a/src/main/helpers/index.ts b/src/main/helpers/index.ts index 91ce0eb9..a9dcae6c 100644 --- a/src/main/helpers/index.ts +++ b/src/main/helpers/index.ts @@ -36,6 +36,4 @@ export const requestWebPage = async (url: string) => { }; export const isPortableVersion = () => - process.env.PORTABLE_EXECUTABLE_FILE != null; - -export * from "./download-source"; + process.env.PORTABLE_EXECUTABLE_FILE !== null; diff --git a/src/main/main.ts b/src/main/main.ts index 690282f6..b71bab8c 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,14 +1,14 @@ import { DownloadManager, PythonInstance, startMainLoop } from "./services"; import { downloadQueueRepository, - repackRepository, + // repackRepository, userPreferencesRepository, } from "./repository"; import { UserPreferences } from "./entity"; import { RealDebridClient } from "./services/real-debrid"; -import { fetchDownloadSourcesAndUpdate } from "./helpers"; -import { publishNewRepacksNotifications } from "./services/notifications"; -import { MoreThan } from "typeorm"; +// import { fetchDownloadSourcesAndUpdate } from "./helpers"; +// import { publishNewRepacksNotifications } from "./services/notifications"; +// import { MoreThan } from "typeorm"; import { HydraApi } from "./services/hydra-api"; import { uploadGamesBatch } from "./services/library-sync"; @@ -40,17 +40,17 @@ const loadState = async (userPreferences: UserPreferences | null) => { startMainLoop(); - const now = new Date(); + // const now = new Date(); - fetchDownloadSourcesAndUpdate().then(async () => { - const newRepacksCount = await repackRepository.count({ - where: { - createdAt: MoreThan(now), - }, - }); + // fetchDownloadSourcesAndUpdate().then(async () => { + // const newRepacksCount = await repackRepository.count({ + // where: { + // createdAt: MoreThan(now), + // }, + // }); - if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount); - }); + // if (newRepacksCount > 0) publishNewRepacksNotifications(newRepacksCount); + // }); }; userPreferencesRepository diff --git a/src/main/workers/download-source.worker.ts b/src/main/workers/download-sources.worker.ts similarity index 74% rename from src/main/workers/download-source.worker.ts rename to src/main/workers/download-sources.worker.ts index 5ec37c7f..c660ad00 100644 --- a/src/main/workers/download-source.worker.ts +++ b/src/main/workers/download-sources.worker.ts @@ -1,6 +1,6 @@ import { downloadSourceSchema } from "@main/events/helpers/validators"; import { DownloadSourceStatus } from "@shared"; -import type { DownloadSource, GameRepack } from "@types"; +import type { DownloadSource } from "@types"; import axios, { AxiosError, AxiosHeaders } from "axios"; import { z } from "zod"; @@ -49,23 +49,11 @@ export const getUpdatedRepacks = async (downloadSources: DownloadSource[]) => { return results; }; -export const validateDownloadSource = async ({ - url, - repacks, -}: { - url: string; - repacks: GameRepack[]; -}) => { +export const validateDownloadSource = async (url: string) => { const response = await axios.get(url); - const source = downloadSourceSchema.parse(response.data); - - 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, + ...downloadSourceSchema.parse(response.data), + etag: response.headers["etag"], }; }; diff --git a/src/main/workers/index.ts b/src/main/workers/index.ts index b0f9721f..799ed2ef 100644 --- a/src/main/workers/index.ts +++ b/src/main/workers/index.ts @@ -1,6 +1,6 @@ import path from "node:path"; import steamGamesWorkerPath from "./steam-games.worker?modulePath"; -import downloadSourceWorkerPath from "./download-source.worker?modulePath"; +import downloadSourcesWorkerPath from "./download-sources.worker?modulePath"; import Piscina from "piscina"; @@ -14,6 +14,6 @@ export const steamGamesWorker = new Piscina({ maxThreads: 1, }); -export const downloadSourceWorker = new Piscina({ - filename: downloadSourceWorkerPath, +export const downloadSourcesWorker = new Piscina({ + filename: downloadSourcesWorkerPath, }); diff --git a/src/preload/index.ts b/src/preload/index.ts index 38df190d..4d7b7183 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -11,6 +11,7 @@ import type { GameRunning, FriendRequestAction, UpdateProfileRequest, + DownloadSource, } from "@types"; import type { CatalogueCategory } from "@shared"; @@ -64,11 +65,8 @@ contextBridge.exposeInMainWorld("electron", { getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"), validateDownloadSource: (url: string) => ipcRenderer.invoke("validateDownloadSource", url), - addDownloadSource: (url: string) => - ipcRenderer.invoke("addDownloadSource", url), - removeDownloadSource: (id: number) => - ipcRenderer.invoke("removeDownloadSource", id), - syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"), + syncDownloadSources: (downloadSources: DownloadSource[]) => + ipcRenderer.invoke("syncDownloadSources", downloadSources), /* Library */ addGameToLibrary: (objectID: string, title: string, shop: GameShop) => diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 7b1a2c03..1488cd49 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useContext, useEffect, useRef } from "react"; import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components"; @@ -26,10 +26,8 @@ import { } from "@renderer/features"; import { useTranslation } from "react-i18next"; import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; -import { RepacksContextProvider } from "./context"; -import { downloadSourcesWorker } from "./workers"; - -downloadSourcesWorker.postMessage("OK"); +import { migrationWorker } from "./workers"; +import { repacksContext } from "./context"; export interface AppProps { children: React.ReactNode; @@ -43,6 +41,8 @@ export function App() { const { clearDownload, setLastPacket } = useDownload(); + const { indexRepacks } = useContext(repacksContext); + const { isFriendsModalVisible, friendRequetsModalTab, @@ -210,53 +210,70 @@ export function App() { }); }, [dispatch, draggingDisabled]); + useEffect(() => { + // window.electron.getRepacks().then((repacks) => { + // migrationWorker.postMessage(["MIGRATE_REPACKS", repacks]); + // }); + // window.electron.getDownloadSources().then((downloadSources) => { + // migrationWorker.postMessage([ + // "MIGRATE_DOWNLOAD_SOURCES", + // downloadSources, + // ]); + // }); + // migrationWorker.onmessage = ( + // event: MessageEvent<"MIGRATE_REPACKS_COMPLETE"> + // ) => { + // if (event.data === "MIGRATE_REPACKS_COMPLETE") { + // indexRepacks(); + // } + // }; + }, [indexRepacks]); + const handleToastClose = useCallback(() => { dispatch(closeToast()); }, [dispatch]); return ( - - <> - {window.electron.platform === "win32" && ( -
-

Hydra

-
- )} + <> + {window.electron.platform === "win32" && ( +
+

Hydra

+
+ )} - + + {userDetails && ( + + )} - {userDetails && ( - + + +
+
- )} -
- +
+ +
+
+ -
-
- -
- -
-
- - - - -
+ + ); } diff --git a/src/renderer/src/context/repacks/repacks.context.tsx b/src/renderer/src/context/repacks/repacks.context.tsx index a2e4101b..c59d5792 100644 --- a/src/renderer/src/context/repacks/repacks.context.tsx +++ b/src/renderer/src/context/repacks/repacks.context.tsx @@ -5,11 +5,13 @@ import { repacksWorker } from "@renderer/workers"; export interface RepacksContext { searchRepacks: (query: string) => Promise; + indexRepacks: () => void; isIndexingRepacks: boolean; } export const repacksContext = createContext({ searchRepacks: async () => [] as GameRepack[], + indexRepacks: () => {}, isIndexingRepacks: false, }); @@ -37,7 +39,8 @@ export function RepacksContextProvider({ children }: RepacksContextProps) { }); }, []); - useEffect(() => { + const indexRepacks = useCallback(() => { + setIsIndexingRepacks(true); repacksWorker.postMessage("INDEX_REPACKS"); repacksWorker.onmessage = () => { @@ -45,10 +48,15 @@ export function RepacksContextProvider({ children }: RepacksContextProps) { }; }, []); + useEffect(() => { + indexRepacks(); + }, [indexRepacks]); + return ( diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 3673ec08..28c5caf7 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -25,6 +25,7 @@ import type { UserStats, UserDetails, FriendRequestSync, + DownloadSourceValidationResult, } from "@types"; import type { DiskSpace } from "check-disk-space"; @@ -106,10 +107,8 @@ declare global { getDownloadSources: () => Promise; validateDownloadSource: ( url: string - ) => Promise<{ name: string; downloadCount: number }>; - addDownloadSource: (url: string) => Promise; - removeDownloadSource: (id: number) => Promise; - syncDownloadSources: () => Promise; + ) => Promise; + syncDownloadSources: (downloadSources: DownloadSource[]) => Promise; /* Hardware */ getDiskFreeSpace: (path: string) => Promise; diff --git a/src/renderer/src/dexie.ts b/src/renderer/src/dexie.ts index 2b9f0aa6..23f0bf83 100644 --- a/src/renderer/src/dexie.ts +++ b/src/renderer/src/dexie.ts @@ -3,7 +3,7 @@ import { Dexie } from "dexie"; export const db = new Dexie("Hydra"); db.version(1).stores({ - repacks: `++id, title, uri, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, + repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`, downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`, }); diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 5d9b2197..d845e028 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -30,6 +30,7 @@ import { store } from "./store"; import resources from "@locales"; import "./workers"; +import { RepacksContextProvider } from "./context"; Sentry.init({}); @@ -56,19 +57,21 @@ i18n ReactDOM.createRoot(document.getElementById("root")!).render( - - - }> - - - - - - - - - - + + + + }> + + + + + + + + + + + ); 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 fba890d1..8e34cbe2 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -9,6 +9,8 @@ import { useForm } from "react-hook-form"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; import { downloadSourcesTable } from "@renderer/dexie"; +import { DownloadSourceValidationResult } from "@types"; +import { downloadSourcesWorker } from "@renderer/workers"; interface AddDownloadSourceModalProps { visible: boolean; @@ -40,41 +42,35 @@ export function AddDownloadSourceModal({ setValue, setError, clearErrors, - formState: { errors }, + formState: { errors, isSubmitting }, } = useForm({ resolver: yupResolver(schema), }); - const [validationResult, setValidationResult] = useState<{ - name: string; - downloadCount: number; - } | null>(null); + const [validationResult, setValidationResult] = + useState(null); const { sourceUrl } = useContext(settingsContext); const onSubmit = useCallback( async (values: FormValues) => { - setIsLoading(true); + const existingDownloadSource = await downloadSourcesTable + .where({ url: values.url }) + .first(); - try { - const result = await window.electron.validateDownloadSource(values.url); - setValidationResult(result); + if (existingDownloadSource) { + setError("url", { + type: "server", + message: t("source_already_exists"), + }); - setUrl(values.url); - } catch (error: unknown) { - if (error instanceof Error) { - if ( - error.message.endsWith("Source with the same url already exists") - ) { - setError("url", { - type: "server", - message: t("source_already_exists"), - }); - } - } - } finally { - setIsLoading(false); + return; } + + const result = await window.electron.validateDownloadSource(values.url); + setValidationResult(result); + + setUrl(values.url); }, [setError, t] ); @@ -92,12 +88,23 @@ export function AddDownloadSourceModal({ }, [visible, clearErrors, handleSubmit, onSubmit, setValue, sourceUrl]); const handleAddDownloadSource = async () => { - await downloadSourcesTable.add({ - url, - }); - await window.electron.addDownloadSource(url); - onClose(); - onAddDownloadSource(); + setIsLoading(true); + + if (validationResult) { + const channel = new BroadcastChannel(`download_sources:import:${url}`); + + downloadSourcesWorker.postMessage([ + "IMPORT_DOWNLOAD_SOURCE", + { ...validationResult, url }, + ]); + + channel.onmessage = () => { + setIsLoading(false); + + onClose(); + onAddDownloadSource(); + }; + } }; return ( @@ -126,7 +133,7 @@ export function AddDownloadSourceModal({ theme="outline" style={{ alignSelf: "flex-end" }} onClick={handleSubmit(onSubmit)} - disabled={isLoading} + disabled={isSubmitting || isLoading} > {t("validate_download_source")} @@ -152,9 +159,9 @@ export function AddDownloadSourceModal({

{validationResult?.name}

{t("found_download_option", { - count: validationResult?.downloadCount, + count: validationResult?.downloads.length, countFormatted: - validationResult?.downloadCount.toLocaleString(), + validationResult?.downloads.length.toLocaleString(), })} diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 53c14348..4f70ff6b 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -10,8 +10,9 @@ import { AddDownloadSourceModal } from "./add-download-source-modal"; import { useToast } from "@renderer/hooks"; import { DownloadSourceStatus } from "@shared"; import { SPACING_UNIT } from "@renderer/theme.css"; -import { settingsContext } from "@renderer/context"; -import { db, downloadSourcesTable, repacksTable } from "@renderer/dexie"; +import { repacksContext, settingsContext } from "@renderer/context"; +import { downloadSourcesTable } from "@renderer/dexie"; +import { downloadSourcesWorker } from "@renderer/workers"; export function SettingsDownloadSources() { const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] = @@ -19,16 +20,23 @@ export function SettingsDownloadSources() { const [downloadSources, setDownloadSources] = useState([]); const [isSyncingDownloadSources, setIsSyncingDownloadSources] = useState(false); + const [isRemovingDownloadSource, setIsRemovingDownloadSource] = + useState(false); const { sourceUrl, clearSourceUrl } = useContext(settingsContext); const { t } = useTranslation("settings"); const { showSuccessToast } = useToast(); + const { indexRepacks } = useContext(repacksContext); + const getDownloadSources = async () => { - downloadSourcesTable.toArray().then((sources) => { - setDownloadSources(sources); - }); + await downloadSourcesTable + .toCollection() + .sortBy("createdAt") + .then((sources) => { + setDownloadSources(sources.reverse()); + }); }; useEffect(() => { @@ -39,18 +47,23 @@ export function SettingsDownloadSources() { if (sourceUrl) setShowAddDownloadSourceModal(true); }, [sourceUrl]); - const handleRemoveSource = async (id: number) => { - await db.transaction("rw", downloadSourcesTable, repacksTable, async () => { - await downloadSourcesTable.where({ id }).delete(); - await repacksTable.where({ downloadSourceId: id }).delete(); - }); + const handleRemoveSource = (id: number) => { + setIsRemovingDownloadSource(true); + const channel = new BroadcastChannel(`download_sources:delete:${id}`); - showSuccessToast(t("removed_download_source")); + downloadSourcesWorker.postMessage(["DELETE_DOWNLOAD_SOURCE", id]); - getDownloadSources(); + channel.onmessage = () => { + showSuccessToast(t("removed_download_source")); + + getDownloadSources(); + indexRepacks(); + setIsRemovingDownloadSource(false); + }; }; const handleAddDownloadSource = async () => { + indexRepacks(); await getDownloadSources(); showSuccessToast(t("added_download_source")); }; @@ -59,7 +72,7 @@ export function SettingsDownloadSources() { setIsSyncingDownloadSources(true); window.electron - .syncDownloadSources() + .syncDownloadSources(downloadSources) .then(() => { showSuccessToast(t("download_sources_synced")); getDownloadSources(); @@ -93,7 +106,11 @@ export function SettingsDownloadSources() {