mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
Merge branch 'main' of https://github.com/hydralauncher/hydra
This commit is contained in:
commit
62b5b12397
18 changed files with 188 additions and 344 deletions
|
@ -69,6 +69,8 @@
|
|||
"copied_link_to_clipboard": "Link copied",
|
||||
"hours": "hours",
|
||||
"minutes": "minutes",
|
||||
"amount_hours": "{{amount}} hours",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"accuracy": "{{accuracy}}% accuracy",
|
||||
"add_to_library": "Add to library",
|
||||
"remove_from_library": "Remove from library",
|
||||
|
@ -135,7 +137,9 @@
|
|||
"enable_repack_list_notifications": "When a new repack is added",
|
||||
"telemetry": "Telemetry",
|
||||
"telemetry_description": "Enable anonymous usage statistics",
|
||||
"real_debrid_api_token_description": "(Optional) Real Debrid API token"
|
||||
"real_debrid_api_token_description": "(Optional) Real Debrid API token",
|
||||
"behavior": "Behavior",
|
||||
"quit_app_instead_hiding": "Close app instead of minimizing to tray"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
|
|
@ -61,6 +61,8 @@
|
|||
"copied_link_to_clipboard": "Enlace copiado",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos",
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"accuracy": "{{accuracy}}% precisión",
|
||||
"add_to_library": "Agregar a la biblioteca",
|
||||
"remove_from_library": "Eliminar de la biblioteca",
|
||||
|
|
|
@ -61,6 +61,8 @@
|
|||
"copied_link_to_clipboard": "Lien copié",
|
||||
"hours": "heures",
|
||||
"minutes": "minutes",
|
||||
"amount_hours": "{{amount}} heures",
|
||||
"amount_minutes": "{{amount}} minutes",
|
||||
"accuracy": "{{accuracy}}% précision",
|
||||
"add_to_library": "Ajouter à la bibliothèque",
|
||||
"remove_from_library": "Supprimer de la bibliothèque",
|
||||
|
|
|
@ -66,6 +66,8 @@
|
|||
"copied_link_to_clipboard": "Link másolva",
|
||||
"hours": "óra",
|
||||
"minutes": "perc",
|
||||
"amount_hours": "{{amount}} óra",
|
||||
"amount_minutes": "{{amount}} perc",
|
||||
"accuracy": "{{accuracy}}% pontosság",
|
||||
"add_to_library": "Hozzáadás a könyvtárhoz",
|
||||
"remove_from_library": "Eltávolítás a könyvtárból",
|
||||
|
|
|
@ -69,6 +69,8 @@
|
|||
"copied_link_to_clipboard": "Link copiato",
|
||||
"hours": "ore",
|
||||
"minutes": "minuti",
|
||||
"amount_hours": "{{amount}} ore",
|
||||
"amount_minutes": "{{amount}} minuti",
|
||||
"accuracy": "{{accuratezza}}% di accuratezza",
|
||||
"add_to_library": "Aggiungi alla libreria",
|
||||
"remove_from_library": "Rimuovi dalla libreria",
|
||||
|
|
|
@ -64,6 +64,8 @@
|
|||
"copied_link_to_clipboard": "Link copiado",
|
||||
"hours": "horas",
|
||||
"minutes": "minutos",
|
||||
"amount_hours": "{{amount}} horas",
|
||||
"amount_minutes": "{{amount}} minutos",
|
||||
"accuracy": "{{accuracy}}% de precisão",
|
||||
"add_to_library": "Adicionar à biblioteca",
|
||||
"remove_from_library": "Remover da biblioteca",
|
||||
|
@ -131,7 +133,9 @@
|
|||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||
"telemetry": "Telemetria",
|
||||
"telemetry_description": "Habilitar estatísticas de uso anônimas",
|
||||
"real_debrid_api_token_description": "(Opcional) Real Debrid API token"
|
||||
"real_debrid_api_token_description": "(Opcional) Real Debrid API token",
|
||||
"behavior": "Comportamento",
|
||||
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download concluído",
|
||||
|
|
|
@ -29,6 +29,9 @@ export class UserPreferences {
|
|||
@Column("boolean", { default: true })
|
||||
telemetryEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
getNewRepacksFromCPG,
|
||||
getNewRepacksFromUser,
|
||||
getNewRepacksFromXatab,
|
||||
// getNewRepacksFromOnlineFix,
|
||||
getNewRepacksFromOnlineFix,
|
||||
readPipe,
|
||||
startProcessWatcher,
|
||||
writePipe,
|
||||
|
@ -76,9 +76,9 @@ const checkForNewRepacks = async () => {
|
|||
getNewRepacksFromCPG(
|
||||
existingRepacks.filter((repack) => repack.repacker === "CPG")
|
||||
),
|
||||
// getNewRepacksFromOnlineFix(
|
||||
// existingRepacks.filter((repack) => repack.repacker === "onlinefix")
|
||||
// ),
|
||||
getNewRepacksFromOnlineFix(
|
||||
existingRepacks.filter((repack) => repack.repacker === "onlinefix")
|
||||
),
|
||||
track1337xUsers(existingRepacks),
|
||||
]).then(() => {
|
||||
repackRepository.count().then((count) => {
|
||||
|
|
|
@ -8,20 +8,18 @@ import { WindowManager } from "./window-manager";
|
|||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const startProcessWatcher = async () => {
|
||||
const sleepTime = 300;
|
||||
const sleepTime = 500;
|
||||
const gamesPlaytime = new Map<number, number>();
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
await sleep(sleepTime);
|
||||
|
||||
const games = await gameRepository.find({
|
||||
where: {
|
||||
executablePath: Not(IsNull()),
|
||||
},
|
||||
});
|
||||
|
||||
if (games.length == 0) {
|
||||
if (games.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -71,5 +69,7 @@ export const startProcessWatcher = async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(sleepTime);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,3 +20,19 @@ export const requestWebPage = async (url: string) => {
|
|||
},
|
||||
}).then((response) => response.text());
|
||||
};
|
||||
|
||||
export const decodeNonUtf8Response = async (res: Response) => {
|
||||
const contentType = res.headers.get("content-type");
|
||||
if (!contentType) return res.text();
|
||||
|
||||
const charset = contentType.substring(contentType.indexOf("charset=") + 8);
|
||||
|
||||
const text = await res.arrayBuffer().then((ab) => {
|
||||
const dataView = new DataView(ab);
|
||||
const decoder = new TextDecoder(charset);
|
||||
|
||||
return decoder.decode(dataView);
|
||||
});
|
||||
|
||||
return text;
|
||||
};
|
||||
|
|
|
@ -2,4 +2,4 @@ export * from "./1337x";
|
|||
export * from "./xatab";
|
||||
export * from "./cpg-repacks";
|
||||
export * from "./gog";
|
||||
// export * from "./online-fix";
|
||||
export * from "./online-fix";
|
||||
|
|
|
@ -1,56 +1,42 @@
|
|||
import { Repack } from "@main/entity";
|
||||
import { savePage } from "./helpers";
|
||||
import { decodeNonUtf8Response, savePage } from "./helpers";
|
||||
import { logger } from "../logger";
|
||||
import parseTorrent, {
|
||||
toMagnetURI,
|
||||
Instance as TorrentInstance,
|
||||
} from "parse-torrent";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { gotScraping } from "got-scraping";
|
||||
import { CookieJar } from "tough-cookie";
|
||||
|
||||
import { format, parse, sub } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
import { decode } from "windows-1251";
|
||||
|
||||
import { onlinefixFormatter } from "@main/helpers";
|
||||
import makeFetchCookie from "fetch-cookie";
|
||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
|
||||
export const getNewRepacksFromOnlineFix = async (
|
||||
existingRepacks: Repack[] = [],
|
||||
page = 1,
|
||||
cookieJar = new CookieJar()
|
||||
cookieJar = new makeFetchCookie.toughCookie.CookieJar()
|
||||
): Promise<void> => {
|
||||
const hasCredentials =
|
||||
import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME &&
|
||||
import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD;
|
||||
if (!hasCredentials) return;
|
||||
|
||||
const http = gotScraping.extend({
|
||||
headerGeneratorOptions: {
|
||||
browsers: [
|
||||
{
|
||||
name: "chrome",
|
||||
minVersion: 87,
|
||||
maxVersion: 89,
|
||||
},
|
||||
],
|
||||
devices: ["desktop"],
|
||||
locales: ["en-US"],
|
||||
operatingSystems: ["windows", "linux"],
|
||||
},
|
||||
cookieJar: cookieJar,
|
||||
});
|
||||
const http = makeFetchCookie(fetch, cookieJar);
|
||||
|
||||
if (page === 1) {
|
||||
await http.get("https://online-fix.me/");
|
||||
await http("https://online-fix.me/");
|
||||
|
||||
const preLogin =
|
||||
((await http
|
||||
.get("https://online-fix.me/engine/ajax/authtoken.php", {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Referer: "https://online-fix.me/",
|
||||
},
|
||||
})
|
||||
.json()) as {
|
||||
((await http("https://online-fix.me/engine/ajax/authtoken.php", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
Referer: "https://online-fix.me/",
|
||||
},
|
||||
}).then((res) => res.json())) as {
|
||||
field: string;
|
||||
value: string;
|
||||
}) || undefined;
|
||||
|
@ -64,27 +50,25 @@ export const getNewRepacksFromOnlineFix = async (
|
|||
[preLogin.field]: preLogin.value,
|
||||
});
|
||||
|
||||
await http
|
||||
.post("https://online-fix.me/", {
|
||||
encoding: "binary",
|
||||
headers: {
|
||||
Referer: "https://online-fix.me",
|
||||
Origin: "https://online-fix.me",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params.toString(),
|
||||
})
|
||||
.text();
|
||||
await http("https://online-fix.me/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Referer: "https://online-fix.me",
|
||||
Origin: "https://online-fix.me",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
const pageParams = page > 1 ? `${`/page/${page}`}` : "";
|
||||
|
||||
const home = await http.get(`https://online-fix.me${pageParams}`, {
|
||||
encoding: "binary",
|
||||
});
|
||||
const document = new JSDOM(home.body).window.document;
|
||||
const home = await http(`https://online-fix.me${pageParams}`).then((res) =>
|
||||
decodeNonUtf8Response(res)
|
||||
);
|
||||
const document = new JSDOM(home).window.document;
|
||||
|
||||
const repacks = [];
|
||||
const repacks: QueryDeepPartialEntity<Repack>[] = [];
|
||||
const articles = Array.from(document.querySelectorAll(".news"));
|
||||
const totalPages = Number(
|
||||
document.querySelector("nav > a:nth-child(13)")?.textContent
|
||||
|
@ -93,25 +77,23 @@ export const getNewRepacksFromOnlineFix = async (
|
|||
try {
|
||||
await Promise.all(
|
||||
articles.map(async (article) => {
|
||||
const gameName = onlinefixFormatter(
|
||||
decode(article.querySelector("h2.title")?.textContent?.trim())
|
||||
);
|
||||
const gameText = article.querySelector("h2.title")?.textContent?.trim();
|
||||
if (!gameText) return;
|
||||
|
||||
const gameName = onlinefixFormatter(gameText);
|
||||
|
||||
const gameLink = article.querySelector("a")?.getAttribute("href");
|
||||
|
||||
if (!gameLink) return;
|
||||
|
||||
const gamePage = await http
|
||||
.get(gameLink, {
|
||||
encoding: "binary",
|
||||
})
|
||||
.text();
|
||||
|
||||
const gamePage = await http(gameLink).then((res) =>
|
||||
decodeNonUtf8Response(res)
|
||||
);
|
||||
const gameDocument = new JSDOM(gamePage).window.document;
|
||||
|
||||
const uploadDateText = gameDocument.querySelector("time").textContent;
|
||||
const uploadDateText = gameDocument.querySelector("time")?.textContent;
|
||||
if (!uploadDateText) return;
|
||||
|
||||
let decodedDateText = decode(uploadDateText);
|
||||
let decodedDateText = uploadDateText;
|
||||
|
||||
// "Вчера" means yesterday.
|
||||
if (decodedDateText.includes("Вчера")) {
|
||||
|
@ -141,14 +123,11 @@ export const getNewRepacksFromOnlineFix = async (
|
|||
const torrentPrePage = torrentButtons[0]?.getAttribute("href");
|
||||
if (!torrentPrePage) return;
|
||||
|
||||
const torrentPage = await http
|
||||
.get(torrentPrePage, {
|
||||
encoding: "binary",
|
||||
headers: {
|
||||
Referer: gameLink,
|
||||
},
|
||||
})
|
||||
.text();
|
||||
const torrentPage = await http(torrentPrePage, {
|
||||
headers: {
|
||||
Referer: gameLink,
|
||||
},
|
||||
}).then((res) => res.text());
|
||||
|
||||
const torrentDocument = new JSDOM(torrentPage).window.document;
|
||||
|
||||
|
@ -157,11 +136,9 @@ export const getNewRepacksFromOnlineFix = async (
|
|||
?.getAttribute("href");
|
||||
|
||||
const torrentFile = Buffer.from(
|
||||
await http
|
||||
.get(`${torrentPrePage}/${torrentLink}`, {
|
||||
responseType: "buffer",
|
||||
})
|
||||
.buffer()
|
||||
await http(`${torrentPrePage}/${torrentLink}`).then((res) =>
|
||||
res.arrayBuffer()
|
||||
)
|
||||
);
|
||||
|
||||
const torrent = parseTorrent(torrentFile) as TorrentInstance;
|
||||
|
@ -170,6 +147,8 @@ export const getNewRepacksFromOnlineFix = async (
|
|||
});
|
||||
|
||||
const torrentSizeInBytes = torrent.length;
|
||||
if (!torrentSizeInBytes) return;
|
||||
|
||||
const fileSizeFormatted =
|
||||
torrentSizeInBytes >= 1024 ** 3
|
||||
? `${(torrentSizeInBytes / 1024 ** 3).toFixed(1)}GBs`
|
||||
|
|
|
@ -4,6 +4,7 @@ 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 { userPreferencesRepository } from "@main/repository";
|
||||
|
||||
export class WindowManager {
|
||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||
|
@ -25,7 +26,7 @@ export class WindowManager {
|
|||
}
|
||||
}
|
||||
|
||||
public static createMainWindow() {
|
||||
public static async createMainWindow() {
|
||||
// Create the browser window.
|
||||
this.mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
|
@ -49,7 +50,14 @@ export class WindowManager {
|
|||
this.loadURL();
|
||||
this.mainWindow.removeMenu();
|
||||
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
this.mainWindow.on("close", () => {
|
||||
if (userPreferences?.preferQuitInsteadOfHiding) {
|
||||
app.quit();
|
||||
}
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { format } from "date-fns";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
|
@ -23,6 +23,8 @@ export interface HeroPanelProps {
|
|||
getGame: () => void;
|
||||
}
|
||||
|
||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
export function HeroPanel({
|
||||
game,
|
||||
gameDetails,
|
||||
|
@ -31,7 +33,7 @@ export function HeroPanel({
|
|||
getGame,
|
||||
isGamePlaying,
|
||||
}: HeroPanelProps) {
|
||||
const { t } = useTranslation("game_details");
|
||||
const { t, i18n } = useTranslation("game_details");
|
||||
|
||||
const [showBinaryNotFoundModal, setShowBinaryNotFoundModal] = useState(false);
|
||||
const [lastTimePlayed, setLastTimePlayed] = useState("");
|
||||
|
@ -49,29 +51,36 @@ export function HeroPanel({
|
|||
} = useDownload();
|
||||
const isGameDownloading = isDownloading && gameDownloading?.id === game?.id;
|
||||
|
||||
const updateLastTimePlayed = useCallback(() => {
|
||||
setLastTimePlayed(
|
||||
formatDistance(game.lastTimePlayed, new Date(), {
|
||||
addSuffix: true,
|
||||
})
|
||||
);
|
||||
}, [game?.lastTimePlayed, formatDistance]);
|
||||
|
||||
useEffect(() => {
|
||||
if (game?.lastTimePlayed) {
|
||||
updateLastTimePlayed();
|
||||
setLastTimePlayed(
|
||||
formatDistance(game.lastTimePlayed, new Date(), {
|
||||
addSuffix: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [game?.lastTimePlayed, formatDistance]);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
updateLastTimePlayed();
|
||||
}, 1000);
|
||||
const numberFormatter = useMemo(() => {
|
||||
return new Intl.NumberFormat(i18n.language, {
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
}, [i18n]);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
const formatPlayTime = () => {
|
||||
const milliseconds = game?.playTimeInMilliseconds || 0;
|
||||
const seconds = milliseconds / 1000;
|
||||
const minutes = seconds / 60;
|
||||
|
||||
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
|
||||
return t("amount_minutes", {
|
||||
amount: minutes.toFixed(0),
|
||||
});
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [game?.lastTimePlayed, updateLastTimePlayed]);
|
||||
const hours = minutes / 60;
|
||||
return t("amount_hours", { amount: numberFormatter.format(hours) });
|
||||
};
|
||||
|
||||
const finalDownloadSize = useMemo(() => {
|
||||
if (!game) return "N/A";
|
||||
|
@ -140,7 +149,7 @@ export function HeroPanel({
|
|||
<>
|
||||
<p>
|
||||
{t("play_time", {
|
||||
amount: formatDistance(0, game.playTimeInMilliseconds),
|
||||
amount: formatPlayTime(game.playTimeInMilliseconds),
|
||||
})}
|
||||
</p>
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ export function Settings() {
|
|||
downloadNotificationsEnabled: false,
|
||||
repackUpdatesNotificationsEnabled: false,
|
||||
telemetryEnabled: false,
|
||||
realDebridApiToken: null,
|
||||
realDebridApiToken: null as string | null,
|
||||
preferQuitInsteadOfHiding: false,
|
||||
});
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
|
@ -28,7 +29,9 @@ export function Settings() {
|
|||
repackUpdatesNotificationsEnabled:
|
||||
userPreferences?.repackUpdatesNotificationsEnabled ?? false,
|
||||
telemetryEnabled: userPreferences?.telemetryEnabled ?? false,
|
||||
realDebridApiToken: userPreferences.realDebridApiToken,
|
||||
realDebridApiToken: userPreferences?.realDebridApiToken ?? null,
|
||||
preferQuitInsteadOfHiding:
|
||||
userPreferences?.preferQuitInsteadOfHiding ?? false,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
@ -110,13 +113,25 @@ export function Settings() {
|
|||
}
|
||||
/>
|
||||
|
||||
<h3>{t("behavior")}</h3>
|
||||
|
||||
<CheckboxField
|
||||
label={t("quit_app_instead_hiding")}
|
||||
checked={form.preferQuitInsteadOfHiding}
|
||||
onChange={() =>
|
||||
updateUserPreferences(
|
||||
"preferQuitInsteadOfHiding",
|
||||
!form.preferQuitInsteadOfHiding
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={t("real_debrid_api_token_description")}
|
||||
value={form.realDebridApiToken ?? ""}
|
||||
onChange={(event) => {
|
||||
updateUserPreferences("realDebridApiToken", event.target.value);
|
||||
}}
|
||||
/>
|
||||
}} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -109,6 +109,7 @@ export interface UserPreferences {
|
|||
repackUpdatesNotificationsEnabled: boolean;
|
||||
telemetryEnabled: boolean;
|
||||
realDebridApiToken: string | null;
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
}
|
||||
|
||||
export interface HowLongToBeatCategory {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue