mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-22 10:23:56 +00:00
Merge branch 'main' into feature/torbox-integration
# Conflicts: # src/renderer/src/constants.ts # src/shared/constants.ts
This commit is contained in:
@@ -37,6 +37,9 @@ export class Game {
|
||||
@Column("text", { nullable: true })
|
||||
executablePath: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
launchOptions: string | null;
|
||||
|
||||
@Column("text", { nullable: true })
|
||||
winePrefixPath: string | null;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
if (!auth) return null;
|
||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||
|
||||
if (!payload) return null;
|
||||
|
||||
return payload.sessionId;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HydraApi } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import type { GameArtifact, GameShop } from "@types";
|
||||
import { SubscriptionRequiredError } from "@shared";
|
||||
import { SubscriptionRequiredError, UserNotLoggedInError } from "@shared";
|
||||
|
||||
const getGameArtifacts = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -22,6 +22,10 @@ const getGameArtifacts = async (
|
||||
return [];
|
||||
}
|
||||
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return [];
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
15
src/main/events/hardware/check-folder-write-permission.ts
Normal file
15
src/main/events/hardware/check-folder-write-permission.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const checkFolderWritePermission = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
path: string
|
||||
) =>
|
||||
new Promise((resolve) => {
|
||||
fs.access(path, fs.constants.W_OK, (err) => {
|
||||
resolve(!err);
|
||||
});
|
||||
});
|
||||
|
||||
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
||||
@@ -1,10 +1,10 @@
|
||||
import checkDiskSpace from "check-disk-space";
|
||||
import disk from "diskusage";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDiskFreeSpace = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
path: string
|
||||
) => checkDiskSpace(path);
|
||||
) => disk.check(path);
|
||||
|
||||
registerEvent("getDiskFreeSpace", getDiskFreeSpace);
|
||||
|
||||
@@ -11,6 +11,7 @@ import "./catalogue/get-trending-games";
|
||||
import "./catalogue/get-publishers";
|
||||
import "./catalogue/get-developers";
|
||||
import "./hardware/get-disk-free-space";
|
||||
import "./hardware/check-folder-write-permission";
|
||||
import "./library/add-game-to-library";
|
||||
import "./library/create-game-shortcut";
|
||||
import "./library/close-game";
|
||||
@@ -22,13 +23,17 @@ import "./library/open-game-executable-path";
|
||||
import "./library/open-game-installer";
|
||||
import "./library/open-game-installer-path";
|
||||
import "./library/update-executable-path";
|
||||
import "./library/update-launch-options";
|
||||
import "./library/verify-executable-path";
|
||||
import "./library/remove-game";
|
||||
import "./library/remove-game-from-library";
|
||||
import "./library/select-game-wine-prefix";
|
||||
import "./library/reset-game-achievements";
|
||||
import "./misc/open-checkout";
|
||||
import "./misc/open-external";
|
||||
import "./misc/show-open-dialog";
|
||||
import "./misc/get-features";
|
||||
import "./misc/show-item-in-folder";
|
||||
import "./torrenting/cancel-game-download";
|
||||
import "./torrenting/pause-game-download";
|
||||
import "./torrenting/resume-game-download";
|
||||
@@ -70,7 +75,6 @@ import "./cloud-save/delete-game-artifact";
|
||||
import "./cloud-save/select-game-backup-path";
|
||||
import "./notifications/publish-new-repacks-notification";
|
||||
import { isPortableVersion } from "@main/helpers";
|
||||
import "./misc/show-item-in-folder";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => appVersion);
|
||||
|
||||
@@ -7,11 +7,16 @@ import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||
const openGame = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number,
|
||||
executablePath: string
|
||||
executablePath: string,
|
||||
launchOptions: string | null
|
||||
) => {
|
||||
// TODO: revisit this for launchOptions
|
||||
const parsedPath = parseExecutablePath(executablePath);
|
||||
|
||||
await gameRepository.update({ id: gameId }, { executablePath: parsedPath });
|
||||
await gameRepository.update(
|
||||
{ id: gameId },
|
||||
{ executablePath: parsedPath, launchOptions }
|
||||
);
|
||||
|
||||
shell.openPath(parsedPath);
|
||||
};
|
||||
|
||||
56
src/main/events/library/reset-game-achievements.ts
Normal file
56
src/main/events/library/reset-game-achievements.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
||||
import fs from "fs";
|
||||
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
||||
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
||||
|
||||
const resetGameAchievements = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
) => {
|
||||
try {
|
||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
||||
|
||||
if (!game) return;
|
||||
|
||||
const achievementFiles = findAchievementFiles(game);
|
||||
|
||||
if (achievementFiles.length) {
|
||||
for (const achievementFile of achievementFiles) {
|
||||
achievementsLogger.log(`deleting ${achievementFile.filePath}`);
|
||||
await fs.promises.rm(achievementFile.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
await gameAchievementRepository.update(
|
||||
{ objectId: game.objectID },
|
||||
{
|
||||
unlockedAchievements: null,
|
||||
}
|
||||
);
|
||||
|
||||
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
|
||||
() =>
|
||||
achievementsLogger.log(
|
||||
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
|
||||
)
|
||||
);
|
||||
|
||||
const gameAchievements = await getUnlockedAchievements(
|
||||
game.objectID,
|
||||
game.shop,
|
||||
true
|
||||
);
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${game.objectID}-${game.shop}`,
|
||||
gameAchievements
|
||||
);
|
||||
} catch (error) {
|
||||
achievementsLogger.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("resetGameAchievements", resetGameAchievements);
|
||||
19
src/main/events/library/update-launch-options.ts
Normal file
19
src/main/events/library/update-launch-options.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const updateLaunchOptions = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
id: number,
|
||||
launchOptions: string | null
|
||||
) => {
|
||||
return gameRepository.update(
|
||||
{
|
||||
id,
|
||||
},
|
||||
{
|
||||
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
||||
8
src/main/events/misc/get-features.ts
Normal file
8
src/main/events/misc/get-features.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
|
||||
};
|
||||
|
||||
registerEvent("getFeatures", getFeatures);
|
||||
@@ -1,16 +1,10 @@
|
||||
import { shell } from "electron";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
userAuthRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const [userAuth, userPreferences] = await Promise.all([
|
||||
userAuthRepository.findOne({ where: { id: 1 } }),
|
||||
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||
]);
|
||||
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
|
||||
if (!userAuth) {
|
||||
return;
|
||||
@@ -22,7 +16,6 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
|
||||
const params = new URLSearchParams({
|
||||
token: paymentToken,
|
||||
lng: userPreferences?.language || "en",
|
||||
});
|
||||
|
||||
shell.openExternal(
|
||||
|
||||
@@ -16,6 +16,7 @@ import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disab
|
||||
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
|
||||
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
|
||||
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
|
||||
import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game";
|
||||
|
||||
export type HydraMigration = Knex.Migration & { name: string };
|
||||
|
||||
@@ -37,6 +38,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
AddShouldSeedColumn,
|
||||
AddSeedAfterDownloadColumn,
|
||||
AddHiddenAchievementDescriptionColumn,
|
||||
AddLaunchOptionsColumnToGame,
|
||||
]);
|
||||
}
|
||||
getMigrationName(migration: HydraMigration): string {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddLaunchOptionsColumnToGame: HydraMigration = {
|
||||
name: "AddLaunchOptionsColumnToGame",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("game", (table) => {
|
||||
return table.string("launchOptions").nullable();
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.alterTable("game", (table) => {
|
||||
return table.dropColumn("launchOptions");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@main/repository";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import type { DownloadProgress } from "@types";
|
||||
import { GofileApi, QiwiApi } from "../hosters";
|
||||
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
|
||||
import { PythonRPC } from "../python-rpc";
|
||||
import {
|
||||
LibtorrentPayload,
|
||||
@@ -247,7 +247,7 @@ export class DownloadManager {
|
||||
private static async getDownloadPayload(game: Game) {
|
||||
switch (game.downloader) {
|
||||
case Downloader.Gofile: {
|
||||
const id = game!.uri!.split("/").pop();
|
||||
const id = game.uri!.split("/").pop();
|
||||
|
||||
const token = await GofileApi.authorize();
|
||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||
@@ -261,7 +261,7 @@ export class DownloadManager {
|
||||
};
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = game!.uri!.split("/").pop();
|
||||
const id = game.uri!.split("/").pop();
|
||||
|
||||
const name = await axios
|
||||
.get(`https://pixeldrain.com/api/file/${id}/info`)
|
||||
@@ -285,6 +285,16 @@ export class DownloadManager {
|
||||
save_path: game.downloadPath!,
|
||||
};
|
||||
}
|
||||
case Downloader.Datanodes: {
|
||||
const downloadUrl = await DatanodesApi.getDownloadUrl(game.uri!);
|
||||
|
||||
return {
|
||||
action: "start",
|
||||
game_id: game.id,
|
||||
url: downloadUrl,
|
||||
save_path: game.downloadPath!,
|
||||
};
|
||||
}
|
||||
case Downloader.Torrent:
|
||||
return {
|
||||
action: "start",
|
||||
|
||||
46
src/main/services/hosters/datanodes.ts
Normal file
46
src/main/services/hosters/datanodes.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
|
||||
export class DatanodesApi {
|
||||
private static readonly session = axios.create({});
|
||||
|
||||
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
|
||||
const parsedUrl = new URL(downloadUrl);
|
||||
const pathSegments = parsedUrl.pathname.split("/");
|
||||
|
||||
const fileCode = decodeURIComponent(pathSegments[1]);
|
||||
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
op: "download2",
|
||||
id: fileCode,
|
||||
rand: "",
|
||||
referer: "https://datanodes.to/download",
|
||||
method_free: "Free Download >>",
|
||||
method_premium: "",
|
||||
adblock_detected: "",
|
||||
});
|
||||
|
||||
const response: AxiosResponse = await this.session.post(
|
||||
"https://datanodes.to/download",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
|
||||
Host: "datanodes.to",
|
||||
Origin: "https://datanodes.to",
|
||||
Referer: "https://datanodes.to/download",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
},
|
||||
maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 302) {
|
||||
return response.headers["location"];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./gofile";
|
||||
export * from "./qiwi";
|
||||
export * from "./datanodes";
|
||||
|
||||
@@ -64,7 +64,10 @@ export class WindowManager {
|
||||
|
||||
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
(details, callback) => {
|
||||
if (details.webContentsId !== this.mainWindow?.webContents.id) {
|
||||
if (
|
||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||
details.url.includes("chatwoot")
|
||||
) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
@@ -81,15 +84,11 @@ export class WindowManager {
|
||||
|
||||
this.mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
(details, callback) => {
|
||||
if (details.webContentsId !== this.mainWindow?.webContents.id) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
if (details.url.includes("featurebase")) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
if (details.url.includes("chatwoot")) {
|
||||
if (
|
||||
details.webContentsId !== this.mainWindow?.webContents.id ||
|
||||
details.url.includes("featurebase") ||
|
||||
details.url.includes("chatwoot")
|
||||
) {
|
||||
return callback(details);
|
||||
}
|
||||
|
||||
@@ -277,14 +276,9 @@ export class WindowManager {
|
||||
if (process.platform !== "darwin") {
|
||||
await updateSystemTray();
|
||||
|
||||
tray.addListener("click", () => {
|
||||
tray.addListener("double-click", () => {
|
||||
if (this.mainWindow) {
|
||||
if (
|
||||
WindowManager.mainWindow?.isMinimized() ||
|
||||
!WindowManager.mainWindow?.isVisible()
|
||||
) {
|
||||
WindowManager.mainWindow?.show();
|
||||
}
|
||||
this.mainWindow.show();
|
||||
} else {
|
||||
this.createMainWindow();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user