This commit is contained in:
lilezek
2024-04-30 10:01:52 +02:00
156 changed files with 2841 additions and 7759 deletions

View File

@@ -46,10 +46,9 @@ export class TorrentClient {
const commonArgs = [BITTORRENT_PORT, writePipePath, readPipePath];
if (app.isPackaged) {
const binaryName = binaryNameByPlatform[process.platform];
const binaryName = binaryNameByPlatform[process.platform]!;
const binaryPath = path.join(
process.resourcesPath,
"dist",
"hydra-download-manager",
binaryName
);

View File

@@ -28,10 +28,11 @@ export const startProcessWatcher = async () => {
const processes = await getProcesses();
for (const game of games) {
const basename = path.win32.basename(game.executablePath);
const executablePath = game.executablePath!;
const basename = path.win32.basename(executablePath);
const basenameWithoutExtension = path.win32.basename(
game.executablePath,
path.extname(game.executablePath)
executablePath,
path.extname(executablePath)
);
const gameProcess = processes.find((runningProcess) => {
@@ -46,7 +47,7 @@ export const startProcessWatcher = async () => {
if (gameProcess) {
if (gamesPlaytime.has(game.id)) {
const zero = gamesPlaytime.get(game.id);
const zero = gamesPlaytime.get(game.id) ?? 0;
const delta = performance.now() - zero;
if (WindowManager.mainWindow) {

View File

@@ -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<GameRepackInput[]> => {
) => {
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<Repack[]> => {
) => {
const response = await request1337x(`/user/${user}/${page}`);
const { window } = new JSDOM(response);

View File

@@ -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,22 +13,22 @@ export const getNewRepacksFromCPG = async (
const { window } = new JSDOM(data);
const repacks: GameRepackInput[] = [];
const repacks = [];
try {
Array.from(window.document.querySelectorAll(".post")).forEach(($post) => {
const $title = $post.querySelector(".entry-title");
const uploadDate = $post.querySelector("time").getAttribute("datetime");
const uploadDate = $post.querySelector("time")?.getAttribute("datetime");
const $downloadInfo = Array.from(
$post.querySelectorAll(".wp-block-heading")
).find(($heading) => $heading.textContent.startsWith("Download"));
).find(($heading) => $heading.textContent?.startsWith("Download"));
/* Side note: CPG often misspells "Magnet" as "Magent" */
const $magnet = Array.from($post.querySelectorAll("a")).find(
($a) =>
$a.textContent.startsWith("Magnet") ||
$a.textContent.startsWith("Magent")
$a.textContent?.startsWith("Magnet") ||
$a.textContent?.startsWith("Magent")
);
const fileSize = $downloadInfo.textContent

View File

@@ -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<Repack>[] = [];
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);
}
};

View File

@@ -1,13 +1,9 @@
import type { Repack } from "@main/entity";
import { repackRepository } from "@main/repository";
import type { GameRepack } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
export type GameRepackInput = Omit<
GameRepack,
"id" | "repackerFriendlyName" | "createdAt" | "updatedAt"
>;
export const savePage = async (repacks: GameRepackInput[]) =>
export const savePage = async (repacks: QueryDeepPartialEntity<Repack>[]) =>
Promise.all(
repacks.map((repack) => repackRepository.insert(repack).catch(() => {}))
);

View File

@@ -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,
@@ -21,7 +20,8 @@ export const getNewRepacksFromOnlineFix = async (
cookieJar = new CookieJar()
): Promise<void> => {
const hasCredentials =
process.env.ONLINEFIX_USERNAME && process.env.ONLINEFIX_PASSWORD;
import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME &&
import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD;
if (!hasCredentials) return;
const http = gotScraping.extend({
@@ -58,8 +58,8 @@ export const getNewRepacksFromOnlineFix = async (
if (!preLogin.field || !preLogin.value) return;
const params = new URLSearchParams({
login_name: process.env.ONLINEFIX_USERNAME,
login_password: process.env.ONLINEFIX_PASSWORD,
login_name: import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME,
login_password: import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD,
login: "submit",
[preLogin.field]: preLogin.value,
});
@@ -84,10 +84,10 @@ 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
document.querySelector("nav > a:nth-child(13)")?.textContent
);
try {
@@ -185,8 +185,10 @@ export const getNewRepacksFromOnlineFix = async (
});
})
);
} catch (err) {
logger.error(err.message, { method: "getNewRepacksFromOnlineFix" });
} catch (err: unknown) {
logger.error((err as Error).message, {
method: "getNewRepacksFromOnlineFix",
});
}
const newRepacks = repacks.filter(

View File

@@ -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,28 +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 getXatabRepack = (url: string) => {
return new Promise((resolve) => {
(async () => {
const data = await requestWebPage(url);
const { window } = new JSDOM(data);
const { document } = window;
const $uploadDate = window.document.querySelector(".entry__date");
const $size = window.document.querySelector(".entry__info-size");
const $uploadDate = document.querySelector(".entry__date");
const $size = document.querySelector(".entry__info-size");
const $downloadButton = window.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 (
@@ -60,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")
@@ -74,14 +80,15 @@ export const getNewRepacksFromXatab = async (
...repack,
page,
});
} catch (err) {
logger.error(err.message, { method: "getNewRepacksFromXatab" });
} catch (err: unknown) {
logger.error((err as Error).message, {
method: "getNewRepacksFromXatab",
});
}
}
const newRepacks = repacks.filter(
(repack) =>
repack.uploadDate &&
!existingRepacks.some(
(existingRepack) => existingRepack.title === repack.title
)

View File

@@ -1,24 +1,31 @@
import axios from "axios";
import { JSDOM } from "jsdom";
import shuffle from "lodash/shuffle";
export interface Steam250Game {
title: string;
objectID: string;
}
export const requestSteam250 = async (path: string) => {
return axios.get(`https://steam250.com${path}`).then((response) => {
const { window } = new JSDOM(response.data);
const { document } = window;
return axios
.get(`https://steam250.com${path}`)
.then((response) => {
const { window } = new JSDOM(response.data);
const { document } = window;
return Array.from(document.querySelectorAll(".appline .title a")).map(
($title: HTMLAnchorElement) => {
const steamGameUrl = $title.href;
if (!steamGameUrl) return null;
return Array.from(document.querySelectorAll(".appline .title a"))
.map(($title) => {
const steamGameUrl = ($title as HTMLAnchorElement).href;
if (!steamGameUrl) return null;
return {
title: $title.textContent,
objectID: steamGameUrl.split("/").pop(),
};
}
);
});
return {
title: $title.textContent,
objectID: steamGameUrl.split("/").pop(),
} as Steam250Game;
})
.filter((game) => game != null);
})
.catch((_) => [] as Steam250Game[]);
};
const steam250Paths = [
@@ -28,7 +35,15 @@ const steam250Paths = [
"/most_played",
];
export const getRandomSteam250List = async () => {
const [path] = shuffle(steam250Paths);
return requestSteam250(path);
export const getSteam250List = async () => {
const gamesList = (
await Promise.all(steam250Paths.map((path) => requestSteam250(path)))
).flat();
const gamesMap: Map<string, Steam250Game> = gamesList.reduce((map, item) => {
map.set(item.objectID, item);
return map;
}, new Map());
return [...gamesMap.values()];
};

View File

@@ -32,7 +32,7 @@ export const getSteamGridData = async (
{
method: "GET",
headers: {
Authorization: `Bearer ${process.env.STEAMGRIDDB_API_KEY}`,
Authorization: `Bearer ${import.meta.env.MAIN_VITE_STEAMGRIDDB_API_KEY}`,
},
}
);

View File

@@ -1,7 +1,7 @@
import path from "node:path";
import { app } from "electron";
import chunk from "lodash/chunk";
import { chunk } from "lodash-es";
import { createDataSource, dataSource } from "@main/data-source";
import { Repack, RepackerFriendlyName, SteamGame } from "@main/entity";
@@ -109,7 +109,7 @@ export const resolveDatabaseUpdates = async () => {
const updateDataSource = createDataSource({
database: app.isPackaged
? path.join(process.resourcesPath, "hydra.db")
: path.join(__dirname, "..", "..", "resources", "hydra.db"),
: path.join(__dirname, "..", "..", "hydra.db"),
});
return updateDataSource.initialize().then(async () => {

View File

@@ -1,16 +1,30 @@
import { BrowserWindow, Menu, Tray, app } from "electron";
import { is } from "@electron-toolkit/utils";
import { t } from "i18next";
import path from "node:path";
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
// whether you're running in development or production).
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
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;
private static loadURL(hash = "") {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.mainWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`
);
} else {
this.mainWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash,
}
);
}
}
public static createMainWindow() {
// Create the browser window.
this.mainWindow = new BrowserWindow({
@@ -19,7 +33,7 @@ export class WindowManager {
minWidth: 1024,
minHeight: 540,
titleBarStyle: "hidden",
icon: path.join(__dirname, "..", "..", "images", "icon.png"),
...(process.platform === "linux" ? { icon } : {}),
trafficLightPosition: { x: 16, y: 16 },
titleBarOverlay: {
symbolColor: "#DADBE1",
@@ -27,40 +41,29 @@ export class WindowManager {
height: 34,
},
webPreferences: {
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
});
this.loadURL();
this.mainWindow.removeMenu();
this.mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
this.mainWindow.webContents.on("did-finish-load", () => {
if (!app.isPackaged) {
// Open the DevTools.
this.mainWindow.webContents.openDevTools();
}
});
this.mainWindow.on("close", () => {
WindowManager.mainWindow.setProgressBar(-1);
WindowManager.mainWindow?.setProgressBar(-1);
});
}
public static redirect(path: string) {
public static redirect(hash: string) {
if (!this.mainWindow) this.createMainWindow();
this.mainWindow.loadURL(`${MAIN_WINDOW_WEBPACK_ENTRY}#${path}`);
this.loadURL(hash);
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
this.mainWindow.focus();
if (this.mainWindow?.isMinimized()) this.mainWindow.restore();
this.mainWindow?.focus();
}
public static createSystemTray(language: string) {
const tray = new Tray(
app.isPackaged
? path.join(process.resourcesPath, "icon_tray.png")
: path.join(__dirname, "..", "..", "resources", "icon_tray.png")
);
const tray = new Tray(trayIcon);
const contextMenu = Menu.buildFromTemplate([
{
@@ -93,10 +96,10 @@ export class WindowManager {
if (process.platform === "win32") {
tray.addListener("click", () => {
if (this.mainWindow) {
if (WindowManager.mainWindow.isMinimized())
if (WindowManager.mainWindow?.isMinimized())
WindowManager.mainWindow.restore();
WindowManager.mainWindow.focus();
WindowManager.mainWindow?.focus();
return;
}