mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-25 03:41:02 +00:00
Merge branch 'main' of https://github.com/hydralauncher/hydra
This commit is contained in:
@@ -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
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(() => {}))
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()];
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user