Merge branch 'main' into feature/torbox-integration

# Conflicts:
#	src/renderer/src/constants.ts
#	src/shared/constants.ts
This commit is contained in:
Zamitto
2025-01-11 15:19:01 -03:00
61 changed files with 2170 additions and 765 deletions

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;
});
};

View 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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
};

View 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);

View 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);

View 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);

View File

@@ -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(

View File

@@ -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 {

View File

@@ -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");
});
},
};

View File

@@ -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",

View 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 "";
}
}

View File

@@ -1,2 +1,3 @@
export * from "./gofile";
export * from "./qiwi";
export * from "./datanodes";

View File

@@ -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();
}