diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 973f95bc..9eb8b0e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,21 +8,9 @@ jobs: build: strategy: matrix: - os: - [ - { - name: windows-latest, - build_path: out/Hydra-win32-x64, - artifact: Hydra-win32-x64, - }, - { - name: ubuntu-latest, - build_path: out/Hydra-linux-x64, - artifact: Hydra-linux-x64, - }, - ] + os: [windows-latest, ubuntu-latest] - runs-on: ${{ matrix.os.name }} + runs-on: ${{ matrix.os }} steps: - name: Check out Git repository @@ -47,10 +35,10 @@ jobs: - name: Build with cx_Freeze run: python torrent-client/setup.py build - - name: Publish - run: yarn run publish + - name: Build Linux + if: matrix.os == 'ubuntu-latest' + run: yarn build:linux env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MAIN_VITE_STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} @@ -58,15 +46,55 @@ jobs: MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} - - name: VirusTotal Scan - uses: crazy-max/ghaction-virustotal@v4 - with: - vt_api_key: ${{ secrets.VT_API_KEY }} - files: | - ./hydra-download-manager/hydra-download-manager.exe + - name: Build Windows + if: matrix.os == 'windows-latest' + run: yarn build:win + env: + MAIN_VITE_STEAMGRIDDB_API_KEY: ${{ secrets.STEAMGRIDDB_API_KEY }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + MAIN_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} + RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }} + MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} + MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} - name: Create artifact uses: actions/upload-artifact@v4 with: - name: ${{ matrix.os.artifact }} - path: ${{ matrix.os.build_path }} + name: Build-${{ matrix.os }} + path: | + dist/*.exe + dist/*.zip + dist/*.dmg + dist/*.AppImage + dist/*.snap + dist/*.deb + dist/*.rpm + dist/*.tar.gz + dist/*.yml + dist/*.blockmap + + - name: VirusTotal Scan + uses: crazy-max/ghaction-virustotal@v4 + if: matrix.os == 'windows-latest' + with: + vt_api_key: ${{ secrets.VT_API_KEY }} + files: | + ./dist/*.exe + + - name: Publish + uses: softprops/action-gh-release@v1 + with: + draft: true + files: | + dist/*.exe + dist/*.zip + dist/*.dmg + dist/*.AppImage + dist/*.snap + dist/*.deb + dist/*.rpm + dist/*.tar.gz + dist/*.yml + dist/*.blockmap + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 3bb8eb01..d98d0238 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -31,6 +31,7 @@ export default defineConfig(({ mode }) => { alias: { "@main": resolve("src/main"), "@locales": resolve("src/locales"), + "@resources": resolve("resources"), }, }, plugins: [ diff --git a/package.json b/package.json index 6eed5a1d..e4ed2ebd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build": "npm run typecheck && electron-vite build", "postinstall": "electron-builder install-app-deps && node ./postinstall.cjs", "build:unpack": "npm run build && electron-builder --dir", - "build:win": "npm run build && electron-builder --win", + "build:win": "electron-builder --win", "build:mac": "electron-vite build && electron-builder --mac", "build:linux": "electron-vite build && electron-builder --linux" }, @@ -70,6 +70,7 @@ "@types/jsdom": "^21.1.6", "@types/lodash-es": "^4.17.12", "@types/node": "^20.12.7", + "@types/parse-torrent": "^5.8.7", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@vanilla-extract/vite-plugin": "^4.0.7", diff --git a/src/main/events/library/close-game.ts b/src/main/events/library/close-game.ts index 0d556925..d549f3b7 100644 --- a/src/main/events/library/close-game.ts +++ b/src/main/events/library/close-game.ts @@ -1,9 +1,9 @@ import path from "node:path"; import { gameRepository } from "@main/repository"; +import { getProcesses } from "@main/helpers"; import { registerEvent } from "../register-event"; -import { getProcesses } from "@main/helpers"; const closeGame = async ( _event: Electron.IpcMainInvokeEvent, @@ -12,13 +12,17 @@ const closeGame = async ( const processes = await getProcesses(); const game = await gameRepository.findOne({ where: { id: gameId } }); - const gameProcess = processes.find((runningProcess) => { - const basename = path.win32.basename(game.executablePath); - const basenameWithoutExtension = path.win32.basename( - game.executablePath, - path.extname(game.executablePath) - ); + if (!game) return false; + const executablePath = game.executablePath!; + + const basename = path.win32.basename(executablePath); + const basenameWithoutExtension = path.win32.basename( + executablePath, + path.extname(executablePath) + ); + + const gameProcess = processes.find((runningProcess) => { if (process.platform === "win32") { return runningProcess.name === basename; } diff --git a/src/main/main.ts b/src/main/main.ts index d09f9c2f..ae591720 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -4,7 +4,7 @@ import { getNewGOGGames, getNewRepacksFromCPG, getNewRepacksFromUser, - // getNewRepacksFromXatab, + getNewRepacksFromXatab, getNewRepacksFromOnlineFix, readPipe, startProcessWatcher, @@ -22,13 +22,6 @@ import { Repack } from "./entity"; import { Notification } from "electron"; import { t } from "i18next"; import { In } from "typeorm"; -import creatWorker from "./workers/test?nodeWorker"; - -creatWorker({ workerData: "worker" }) - .on("message", (message) => { - console.log(`\nMessage from worker: ${message}`); - }) - .postMessage(""); startProcessWatcher(); @@ -80,9 +73,9 @@ const checkForNewRepacks = async () => { getNewGOGGames( existingRepacks.filter((repack) => repack.repacker === "GOG") ), - // getNewRepacksFromXatab( - // existingRepacks.filter((repack) => repack.repacker === "Xatab") - // ), + getNewRepacksFromXatab( + existingRepacks.filter((repack) => repack.repacker === "Xatab") + ), getNewRepacksFromCPG( existingRepacks.filter((repack) => repack.repacker === "CPG") ), diff --git a/src/main/services/repack-tracker/1337x.ts b/src/main/services/repack-tracker/1337x.ts index 1cbafa3b..8573079b 100644 --- a/src/main/services/repack-tracker/1337x.ts +++ b/src/main/services/repack-tracker/1337x.ts @@ -4,7 +4,6 @@ import { formatUploadDate } from "@main/helpers"; import { Repack } from "@main/entity"; import { requestWebPage, savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; export const request1337x = async (path: string) => requestWebPage(`https://1337xx.to${path}`); @@ -68,7 +67,7 @@ export const extractTorrentsFromDocument = async ( user: string, document: Document, existingRepacks: Repack[] = [] -): Promise => { +) => { const $trs = Array.from(document.querySelectorAll("tbody tr")); return Promise.all( @@ -108,7 +107,7 @@ export const getNewRepacksFromUser = async ( user: string, existingRepacks: Repack[], page = 1 -): Promise => { +) => { const response = await request1337x(`/user/${user}/${page}`); const { window } = new JSDOM(response); diff --git a/src/main/services/repack-tracker/cpg-repacks.ts b/src/main/services/repack-tracker/cpg-repacks.ts index c4b62c1f..2b939d08 100644 --- a/src/main/services/repack-tracker/cpg-repacks.ts +++ b/src/main/services/repack-tracker/cpg-repacks.ts @@ -3,7 +3,6 @@ import { JSDOM } from "jsdom"; import { Repack } from "@main/entity"; import { requestWebPage, savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; import { logger } from "../logger"; export const getNewRepacksFromCPG = async ( @@ -14,7 +13,7 @@ export const getNewRepacksFromCPG = async ( const { window } = new JSDOM(data); - const repacks: GameRepackInput[] = []; + const repacks = []; try { Array.from(window.document.querySelectorAll(".post")).forEach(($post) => { diff --git a/src/main/services/repack-tracker/gog.ts b/src/main/services/repack-tracker/gog.ts index 73daebf7..00c78e36 100644 --- a/src/main/services/repack-tracker/gog.ts +++ b/src/main/services/repack-tracker/gog.ts @@ -1,7 +1,8 @@ import { JSDOM, VirtualConsole } from "jsdom"; -import { GameRepackInput, requestWebPage, savePage } from "./helpers"; +import { requestWebPage, savePage } from "./helpers"; import { Repack } from "@main/entity"; -import { logger } from "../logger"; + +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; const virtualConsole = new VirtualConsole(); @@ -36,43 +37,35 @@ const getGOGGame = async (url: string) => { }; export const getNewGOGGames = async (existingRepacks: Repack[] = []) => { - try { - const data = await requestWebPage( - "https://freegogpcgames.com/a-z-games-list/" - ); + const data = await requestWebPage( + "https://freegogpcgames.com/a-z-games-list/" + ); - const { window } = new JSDOM(data, { virtualConsole }); + const { window } = new JSDOM(data, { virtualConsole }); - const $uls = Array.from(window.document.querySelectorAll(".az-columns")); + const $uls = Array.from(window.document.querySelectorAll(".az-columns")); - for (const $ul of $uls) { - const repacks: GameRepackInput[] = []; - const $lis = Array.from($ul.querySelectorAll("li")); + for (const $ul of $uls) { + const repacks: QueryDeepPartialEntity[] = []; + const $lis = Array.from($ul.querySelectorAll("li")); - for (const $li of $lis) { - const $a = $li.querySelector("a"); - const href = $a.href; + for (const $li of $lis) { + const $a = $li.querySelector("a"); + const href = $a.href; - const title = $a.textContent.trim(); + const title = $a.textContent.trim(); - const gameExists = existingRepacks.some( - (existingRepack) => existingRepack.title === title - ); + const gameExists = existingRepacks.some( + (existingRepack) => existingRepack.title === title + ); - if (!gameExists) { - try { - const game = await getGOGGame(href); + if (!gameExists) { + const game = await getGOGGame(href); - repacks.push({ ...game, title }); - } catch (err) { - logger.error(err.message, { method: "getGOGGame", url: href }); - } - } + repacks.push({ ...game, title }); } - - if (repacks.length) await savePage(repacks); } - } catch (err) { - logger.error(err.message, { method: "getNewGOGGames" }); + + if (repacks.length) await savePage(repacks); } }; diff --git a/src/main/services/repack-tracker/online-fix.ts b/src/main/services/repack-tracker/online-fix.ts index 2a69dd70..c627eccb 100644 --- a/src/main/services/repack-tracker/online-fix.ts +++ b/src/main/services/repack-tracker/online-fix.ts @@ -1,6 +1,5 @@ import { Repack } from "@main/entity"; import { savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; import { logger } from "../logger"; import parseTorrent, { toMagnetURI, @@ -85,7 +84,7 @@ export const getNewRepacksFromOnlineFix = async ( }); const document = new JSDOM(home.body).window.document; - const repacks: GameRepackInput[] = []; + const repacks = []; const articles = Array.from(document.querySelectorAll(".news")); const totalPages = Number( document.querySelector("nav > a:nth-child(13)")?.textContent diff --git a/src/main/services/repack-tracker/xatab.ts b/src/main/services/repack-tracker/xatab.ts index 847a1026..de9f5285 100644 --- a/src/main/services/repack-tracker/xatab.ts +++ b/src/main/services/repack-tracker/xatab.ts @@ -1,16 +1,14 @@ import { JSDOM } from "jsdom"; -import parseTorrent, { toMagnetURI } from "parse-torrent"; - import { Repack } from "@main/entity"; import { logger } from "../logger"; import { requestWebPage, savePage } from "./helpers"; -import type { GameRepackInput } from "./helpers"; -const getTorrentBuffer = (url: string) => - fetch(url, { method: "GET" }).then((response) => - response.arrayBuffer().then((buffer) => Buffer.from(buffer)) - ); +import createWorker from "@main/workers/torrent-parser.worker?nodeWorker"; +import { toMagnetURI } from "parse-torrent"; +import type { Instance } from "parse-torrent"; + +const worker = createWorker({}); const formatXatabDate = (str: string) => { const date = new Date(); @@ -28,29 +26,36 @@ const formatXatabDate = (str: string) => { const formatXatabDownloadSize = (str: string) => str.replace(",", ".").replace(/Гб/g, "GB").replace(/Мб/g, "MB"); -const getXatabRepack = async (url: string) => { - const data = await requestWebPage(url); - const { window } = new JSDOM(data); - const { document } = window; +const getXatabRepack = (url: string) => { + return new Promise((resolve) => { + (async () => { + const data = await requestWebPage(url); + const { window } = new JSDOM(data); + const { document } = window; - const $uploadDate = document.querySelector(".entry__date"); - const $size = document.querySelector(".entry__info-size"); + const $uploadDate = document.querySelector(".entry__date"); + const $size = document.querySelector(".entry__info-size"); - const $downloadButton = document.querySelector( - ".download-torrent" - ) as HTMLAnchorElement; + const $downloadButton = document.querySelector( + ".download-torrent" + ) as HTMLAnchorElement; - if (!$downloadButton) throw new Error("Download button not found"); + if (!$downloadButton) throw new Error("Download button not found"); - const torrentBuffer = await getTorrentBuffer($downloadButton.href); + const onMessage = (torrent: Instance) => { + resolve({ + fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(), + magnet: toMagnetURI(torrent), + uploadDate: formatXatabDate($uploadDate.textContent), + }); - return { - fileSize: formatXatabDownloadSize($size.textContent).toUpperCase(), - magnet: toMagnetURI({ - infoHash: parseTorrent(torrentBuffer).infoHash, - }), - uploadDate: formatXatabDate($uploadDate.textContent), - }; + worker.removeListener("message", onMessage); + }; + + worker.on("message", onMessage); + worker.postMessage($downloadButton.href); + })(); + }); }; export const getNewRepacksFromXatab = async ( @@ -61,7 +66,7 @@ export const getNewRepacksFromXatab = async ( const { window } = new JSDOM(data); - const repacks: GameRepackInput[] = []; + const repacks = []; for (const $a of Array.from( window.document.querySelectorAll(".entry__title a") @@ -84,7 +89,6 @@ export const getNewRepacksFromXatab = async ( const newRepacks = repacks.filter( (repack) => - repack.uploadDate && !existingRepacks.some( (existingRepack) => existingRepack.title === repack.title ) diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 0aa8afd2..05cb95d6 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -2,8 +2,8 @@ import { BrowserWindow, Menu, Tray, app } from "electron"; import { is } from "@electron-toolkit/utils"; import { t } from "i18next"; import path from "node:path"; -import icon from "../../../resources/icon.png?asset"; -import trayIcon from "../../../resources/tray-icon.png?asset"; +import icon from "@resources/icon.png?asset"; +import trayIcon from "@resources/tray-icon.png?asset"; export class WindowManager { public static mainWindow: Electron.BrowserWindow | null = null; diff --git a/src/main/workers/torrent-parser.worker.ts b/src/main/workers/torrent-parser.worker.ts new file mode 100644 index 00000000..7502fd5f --- /dev/null +++ b/src/main/workers/torrent-parser.worker.ts @@ -0,0 +1,17 @@ +import { parentPort } from "worker_threads"; +import parseTorrent from "parse-torrent"; + +const port = parentPort; +if (!port) throw new Error("IllegalState"); + +const getTorrentBuffer = (url: string) => + fetch(url, { method: "GET" }).then((response) => + response.arrayBuffer().then((buffer) => Buffer.from(buffer)) + ); + +port.on("message", async (url: string) => { + const buffer = await getTorrentBuffer(url); + const torrent = await parseTorrent(buffer); + + port.postMessage(torrent); +}); diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 97dcca0b..4ca7b2fb 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -71,7 +71,8 @@ contextBridge.exposeInMainWorld("electron", { openGame: (gameId: number, executablePath: string) => ipcRenderer.invoke("openGame", gameId, executablePath), closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), - removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId), + removeGameFromLibrary: (gameId: number) => + ipcRenderer.invoke("removeGameFromLibrary", gameId), deleteGameFolder: (gameId: number) => ipcRenderer.invoke("deleteGameFolder", gameId), getGameByObjectID: (objectID: string) => diff --git a/src/preload/index.ts b/src/preload/index.ts index 66fff80f..c4f8ca96 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -80,7 +80,8 @@ contextBridge.exposeInMainWorld("electron", { openGame: (gameId: number, executablePath: string) => ipcRenderer.invoke("openGame", gameId, executablePath), closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId), - removeGame: (gameId: number) => ipcRenderer.invoke("removeGame", gameId), + removeGameFromLibrary: (gameId: number) => + ipcRenderer.invoke("removeGameFromLibrary", gameId), deleteGameFolder: (gameId: number) => ipcRenderer.invoke("deleteGameFolder", gameId), getGameByObjectID: (objectID: string) => diff --git a/src/renderer/index.html b/src/renderer/index.html index abb1eaae..1917de45 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -9,9 +9,8 @@ content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;" /> - +
-

hello

diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx index ed9a54ed..993d6aa5 100644 --- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx +++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx @@ -22,7 +22,7 @@ export function BottomPanel() { }, []); const status = useMemo(() => { - if (isDownloading) { + if (isDownloading && game) { if (game.status === "downloading_metadata") return t("downloading_metadata", { title: game.title }); diff --git a/src/renderer/src/components/modal/modal.tsx b/src/renderer/src/components/modal/modal.tsx index 9b5f8cf5..1f07e7d7 100644 --- a/src/renderer/src/components/modal/modal.tsx +++ b/src/renderer/src/components/modal/modal.tsx @@ -62,12 +62,14 @@ export function Modal({ const onMouseDown = (e: MouseEvent) => { if (!isTopMostModal()) return; - const clickedOutsideContent = !modalContentRef.current.contains( - e.target as Node - ); + if (modalContentRef.current) { + const clickedOutsideContent = modalContentRef.current.contains( + e.target as Node + ); - if (clickedOutsideContent) { - handleCloseClick(); + if (clickedOutsideContent) { + handleCloseClick(); + } } }; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 1d198ea5..a478f687 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -62,7 +62,7 @@ declare global { openGameInstaller: (gameId: number) => Promise; openGame: (gameId: number, executablePath: string) => Promise; closeGame: (gameId: number) => Promise; - removeGame: (gameId: number) => Promise; + removeGameFromLibrary: (gameId: number) => Promise; deleteGameFolder: (gameId: number) => Promise; getGameByObjectID: (objectID: string) => Promise; onPlaytime: (cb: (gameId: number) => void) => () => Electron.IpcRenderer; diff --git a/src/renderer/src/features/use-preferences-slice.ts b/src/renderer/src/features/use-preferences-slice.ts index f6a3cf65..d735e7a2 100644 --- a/src/renderer/src/features/use-preferences-slice.ts +++ b/src/renderer/src/features/use-preferences-slice.ts @@ -14,7 +14,10 @@ export const userPreferencesSlice = createSlice({ name: "userPreferences", initialState, reducers: { - setUserPreferences: (state, action: PayloadAction) => { + setUserPreferences: ( + state, + action: PayloadAction + ) => { state.value = action.payload; }, }, diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts index 27f90e8d..44392242 100644 --- a/src/renderer/src/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -58,17 +58,17 @@ export function useDownload() { deleteGame(gameId); }); - const removeGame = (gameId: number) => - window.electron.removeGame(gameId).then(() => { + const removeGameFromLibrary = (gameId: number) => + window.electron.removeGameFromLibrary(gameId).then(() => { updateLibrary(); }); const isVerifying = ["downloading_metadata", "checking_files"].includes( - lastPacket?.game.status + lastPacket?.game.status ?? "" ); const getETA = () => { - if (isVerifying || !isFinite(lastPacket?.timeRemaining)) { + if (isVerifying || !isFinite(lastPacket?.timeRemaining ?? 0)) { return ""; } @@ -124,7 +124,7 @@ export function useDownload() { pauseDownload, resumeDownload, cancelDownload, - removeGame, + removeGameFromLibrary, deleteGame, isGameDeleting, clearDownload: () => dispatch(clearDownload()), diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index aecf7b5e..ca9903a3 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -33,6 +33,7 @@ export function Downloads() { numSeeds, pauseDownload, resumeDownload, + removeGameFromLibrary, cancelDownload, deleteGame, isGameDeleting, @@ -52,11 +53,6 @@ export function Downloads() { updateLibrary(); }); - const removeGame = (gameId: number) => - window.electron.removeGame(gameId).then(() => { - updateLibrary(); - }); - const getFinalDownloadSize = (game: Game) => { const isGameDownloading = isDownloading && gameDownloading?.id === game?.id; @@ -194,7 +190,7 @@ export function Downloads() {