feat: removing crypto from level

This commit is contained in:
Chubby Granny Chaser
2025-02-15 19:49:54 +00:00
128 changed files with 1883 additions and 660 deletions

View File

@@ -141,7 +141,7 @@ const processAchievementFileDiff = async (
export class AchievementWatcherManager {
private static hasFinishedMergingWithRemote = false;
public static watchAchievements = () => {
public static watchAchievements() {
if (!this.hasFinishedMergingWithRemote) return;
if (process.platform === "win32") {
@@ -149,12 +149,12 @@ export class AchievementWatcherManager {
}
return watchAchievementsWithWine();
};
}
private static preProcessGameAchievementFiles = (
private static preProcessGameAchievementFiles(
game: Game,
gameAchievementFiles: AchievementFile[]
) => {
) {
const unlockedAchievements: UnlockedAchievement[] = [];
for (const achievementFile of gameAchievementFiles) {
const parsedAchievements = parseAchievementFile(
@@ -182,7 +182,7 @@ export class AchievementWatcherManager {
}
return mergeAchievements(game, unlockedAchievements, false);
};
}
private static preSearchAchievementsWindows = async () => {
const games = await gamesSublevel
@@ -230,7 +230,7 @@ export class AchievementWatcherManager {
);
};
public static preSearchAchievements = async () => {
public static async preSearchAchievements() {
try {
const newAchievementsCount =
process.platform === "win32"
@@ -256,5 +256,5 @@ export class AchievementWatcherManager {
}
this.hasFinishedMergingWithRemote = true;
};
}
}

View File

@@ -40,7 +40,7 @@ export const getGameAchievementData = async (
throw err;
}
logger.error("Failed to get game achievements", err);
logger.error("Failed to get game achievements for", objectId, err);
return [];
});

View File

@@ -59,7 +59,7 @@ export const mergeAchievements = async (
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
const newAchievementsMap = new Map(
achievements.reverse().map((achievement) => {
achievements.toReversed().map((achievement) => {
return [achievement.name.toUpperCase(), achievement];
})
);
@@ -87,7 +87,7 @@ export const mergeAchievements = async (
userPreferences?.achievementNotificationsEnabled
) {
const achievementsInfo = newAchievements
.sort((a, b) => {
.toSorted((a, b) => {
return a.unlockTime - b.unlockTime;
})
.map((achievement) => {

View File

@@ -1,8 +1,8 @@
import { Downloader } from "@shared";
import { Downloader, DownloadError } from "@shared";
import { WindowManager } from "../window-manager";
import { publishDownloadCompleteNotification } from "../notifications";
import type { Download, DownloadProgress, UserPreferences } from "@types";
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
import { PythonRPC } from "../python-rpc";
import {
LibtorrentPayload,
@@ -15,6 +15,7 @@ import path from "path";
import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox";
export class DownloadManager {
private static downloadingGameId: string | null = null;
@@ -25,17 +26,20 @@ export class DownloadManager {
) {
PythonRPC.spawn(
download?.status === "active"
? await this.getDownloadPayload(download).catch(() => undefined)
? await this.getDownloadPayload(download).catch((err) => {
logger.error("Error getting download payload", err);
return undefined;
})
: undefined,
downloadsToSeed?.map((download) => ({
game_id: `${download.shop}-${download.objectId}`,
game_id: levelKeys.game(download.shop, download.objectId),
url: download.uri,
save_path: download.downloadPath,
}))
);
if (download) {
this.downloadingGameId = `${download.shop}-${download.objectId}`;
this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
}
}
@@ -106,7 +110,7 @@ export class DownloadManager {
if (!download || !game) return;
const userPreferences = await db.get<string, UserPreferences>(
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
@@ -230,7 +234,9 @@ export class DownloadManager {
});
WindowManager.mainWindow?.setProgressBar(-1);
if (downloadKey === this.downloadingGameId) {
WindowManager.mainWindow?.webContents.send("on-download-progress", null);
this.downloadingGameId = null;
}
}
@@ -260,6 +266,8 @@ export class DownloadManager {
const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!);
await GofileApi.checkDownloadUrl(downloadLink);
return {
action: "start",
game_id: downloadId,
@@ -270,10 +278,11 @@ export class DownloadManager {
}
case Downloader.PixelDrain: {
const id = download.uri.split("/").pop();
return {
action: "start",
game_id: downloadId,
url: `https://pixeldrain.com/api/file/${id}?download`,
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
save_path: download.downloadPath,
};
}
@@ -295,6 +304,16 @@ export class DownloadManager {
save_path: download.downloadPath,
};
}
case Downloader.Mediafire: {
const downloadUrl = await MediafireApi.getDownloadUrl(download.uri);
return {
action: "start",
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
};
}
case Downloader.Torrent:
return {
action: "start",
@@ -305,10 +324,7 @@ export class DownloadManager {
case Downloader.RealDebrid: {
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
if (!downloadUrl)
throw new Error(
"This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available."
);
if (!downloadUrl) throw new Error(DownloadError.NotCachedInRealDebrid);
return {
action: "start",
@@ -317,6 +333,18 @@ export class DownloadManager {
save_path: download.downloadPath,
};
}
case Downloader.TorBox: {
const { name, url } = await TorBoxClient.getDownloadInfo(download.uri);
if (!url) return;
return {
action: "start",
game_id: downloadId,
url,
save_path: download.downloadPath,
out: name,
};
}
}
}

View File

@@ -6,24 +6,23 @@ import type {
TorBoxAddTorrentRequest,
TorBoxRequestLinkRequest,
} from "@types";
import { logger } from "../logger";
export class TorBoxClient {
private static instance: AxiosInstance;
private static readonly baseURL = "https://api.torbox.app/v1/api";
public static apiToken: string;
private static apiToken: string;
static authorize(apiToken: string) {
this.apiToken = apiToken;
this.instance = axios.create({
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
this.apiToken = apiToken;
}
static async addMagnet(magnet: string) {
private static async addMagnet(magnet: string) {
const form = new FormData();
form.append("magnet", magnet);
@@ -32,6 +31,10 @@ export class TorBoxClient {
form
);
if (!response.data.success) {
throw new Error(response.data.detail);
}
return response.data.data;
}
@@ -55,22 +58,16 @@ export class TorBoxClient {
}
static async requestLink(id: number) {
const searchParams = new URLSearchParams({});
searchParams.set("token", this.apiToken);
searchParams.set("torrent_id", id.toString());
searchParams.set("zip_link", "true");
const searchParams = new URLSearchParams({
token: this.apiToken,
torrent_id: id.toString(),
zip_link: "true",
});
const response = await this.instance.get<TorBoxRequestLinkRequest>(
"/torrents/requestdl?" + searchParams.toString()
);
if (response.status !== 200) {
logger.error(response.data.error);
logger.error(response.data.detail);
return null;
}
return response.data.data;
}
@@ -81,7 +78,7 @@ export class TorBoxClient {
return response.data.data;
}
static async getTorrentId(magnetUri: string) {
private static async getTorrentIdAndName(magnetUri: string) {
const userTorrents = await this.getAllTorrentsFromUser();
const { infoHash } = await parseTorrent(magnetUri);
@@ -89,9 +86,18 @@ export class TorBoxClient {
(userTorrent) => userTorrent.hash === infoHash
);
if (userTorrent) return userTorrent.id;
if (userTorrent) return { id: userTorrent.id, name: userTorrent.name };
const torrent = await this.addMagnet(magnetUri);
return torrent.torrent_id;
return { id: torrent.torrent_id, name: torrent.name };
}
static async getDownloadInfo(uri: string) {
const torrentData = await this.getTorrentIdAndName(uri);
const url = await this.requestLink(torrentData.id);
const name = torrentData.name ? `${torrentData.name}.zip` : undefined;
return { url, name };
}
}

View File

@@ -60,4 +60,12 @@ export class GofileApi {
throw new Error("Failed to get download link");
}
public static async checkDownloadUrl(url: string) {
return axios.head(url, {
headers: {
Cookie: `accountToken=${this.token}`,
},
});
}
}

View File

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

View File

@@ -0,0 +1,54 @@
import fetch from "node-fetch";
export class MediafireApi {
private static readonly validMediafireIdentifierDL = /^[a-zA-Z0-9]+$/m;
private static readonly validMediafirePreDL =
/(?<=['"])(https?:)?(\/\/)?(www\.)?mediafire\.com\/(file|view|download)\/[^'"?]+\?dkey=[^'"]+(?=['"])/;
private static readonly validDynamicDL =
/(?<=['"])https?:\/\/download\d+\.mediafire\.com\/[^'"]+(?=['"])/;
private static readonly checkHTTP = /^https?:\/\//m;
public static async getDownloadUrl(mediafireUrl: string): Promise<string> {
try {
const processedUrl = this.processUrl(mediafireUrl);
const response = await fetch(processedUrl);
if (!response.ok) throw new Error("Failed to fetch Mediafire page");
const html = await response.text();
return this.extractDirectUrl(html);
} catch (error) {
throw new Error(`Failed to get download URL`);
}
}
private static processUrl(url: string): string {
let processed = url.replace("http://", "https://");
if (this.validMediafireIdentifierDL.test(processed)) {
processed = `https://mediafire.com/?${processed}`;
}
if (!this.checkHTTP.test(processed)) {
processed = processed.startsWith("//")
? `https:${processed}`
: `https://${processed}`;
}
return processed;
}
private static extractDirectUrl(html: string): string {
const preMatch = this.validMediafirePreDL.exec(html);
if (preMatch?.[0]) {
return preMatch[0];
}
const dlMatch = this.validDynamicDL.exec(html);
if (dlMatch?.[0]) {
return dlMatch[0];
}
throw new Error("No valid download links found");
}
}

View File

@@ -3,7 +3,7 @@ import { WindowManager } from "./window-manager";
import url from "url";
import { uploadGamesBatch } from "./library-sync";
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
import { logger } from "./logger";
import { networkLogger as logger } from "./logger";
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
import { omit } from "lodash-es";
import { appVersion } from "@main/constants";
@@ -154,7 +154,8 @@ export class HydraApi {
(error) => {
logger.error(" ---- RESPONSE ERROR -----");
const { config } = error;
const data = JSON.parse(config.data);
const data = JSON.parse(config.data ?? null);
logger.error(
config.method,
@@ -175,14 +176,22 @@ export class HydraApi {
error.response.status,
error.response.data
);
} else if (error.request) {
const errorData = error.toJSON();
logger.error("Request error:", errorData.message);
} else {
logger.error("Error", error.message);
return Promise.reject(error as Error);
}
logger.error(" ----- END RESPONSE ERROR -------");
return Promise.reject(error);
if (error.request) {
const errorData = error.toJSON();
logger.error("Request error:", errorData.code, errorData.message);
return Promise.reject(
new Error(
`Request failed with ${errorData.code} ${errorData.message}`
)
);
}
logger.error("Error", error.message);
return Promise.reject(error as Error);
}
);
}

View File

@@ -1,4 +1,3 @@
export * from "./crypto";
export * from "./logger";
export * from "./steam";
export * from "./steam-250";

View File

@@ -35,22 +35,20 @@ export const mergeWithRemoteGames = async () => {
name: "getById",
});
if (steamGame) {
const iconUrl = steamGame?.clientIcon
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
: null;
const iconUrl = steamGame?.clientIcon
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
: null;
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
objectId: game.objectId,
title: steamGame?.name,
remoteId: game.id,
shop: game.shop,
iconUrl,
lastTimePlayed: game.lastTimePlayed,
playTimeInMilliseconds: game.playTimeInMilliseconds,
isDeleted: false,
});
}
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
objectId: game.objectId,
title: steamGame?.name,
remoteId: game.id,
shop: game.shop,
iconUrl,
lastTimePlayed: game.lastTimePlayed,
playTimeInMilliseconds: game.playTimeInMilliseconds,
isDeleted: false,
});
}
}
})

View File

@@ -15,7 +15,7 @@ export const uploadGamesBatch = async () => {
);
});
const gamesChunks = chunk(games, 200);
const gamesChunks = chunk(games, 50);
for (const chunk of gamesChunks) {
await HydraApi.post(

View File

@@ -6,8 +6,12 @@ log.transports.file.resolvePathFn = (
_: log.PathVariables,
message?: log.LogMessage | undefined
) => {
if (message?.scope === "python-instance") {
return path.join(logsPath, "pythoninstance.txt");
if (message?.scope === "python-rpc") {
return path.join(logsPath, "pythonrpc.txt");
}
if (message?.scope === "network") {
return path.join(logsPath, "network.txt");
}
if (message?.scope == "achievements") {
@@ -34,3 +38,4 @@ log.initialize();
export const pythonRpcLogger = log.scope("python-rpc");
export const logger = log.scope("main");
export const achievementsLogger = log.scope("achievements");
export const networkLogger = log.scope("network");

View File

@@ -2,6 +2,7 @@ import { sleep } from "@main/helpers";
import { DownloadManager } from "./download";
import { watchProcesses } from "./process-watcher";
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
import { UpdateManager } from "./update-manager";
export const startMainLoop = async () => {
// eslint-disable-next-line no-constant-condition
@@ -11,6 +12,7 @@ export const startMainLoop = async () => {
DownloadManager.watchDownloads(),
AchievementWatcherManager.watchAchievements(),
DownloadManager.getSeedStatus(),
UpdateManager.checkForUpdatePeriodically(),
]);
await sleep(1500);

View File

@@ -9,6 +9,7 @@ import { achievementSoundPath } from "@main/constants";
import icon from "@resources/icon.png?asset";
import { NotificationOptions, toXmlString } from "./xml";
import { logger } from "../logger";
import { WindowManager } from "../window-manager";
import type { Game, UserPreferences } from "@types";
import { db, levelKeys } from "@main/level";
@@ -96,7 +97,9 @@ export const publishCombinedNewAchievementNotification = async (
toastXml: toXmlString(options),
}).show();
if (process.platform !== "linux") {
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
} else if (process.platform !== "linux") {
sound.play(achievementSoundPath);
}
};
@@ -143,7 +146,9 @@ export const publishNewAchievementNotification = async (info: {
toastXml: toXmlString(options),
}).show();
if (process.platform !== "linux") {
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
} else if (process.platform !== "linux") {
sound.play(achievementSoundPath);
}
};

View File

@@ -21,11 +21,18 @@ export const getSteamAppDetails = async (
});
return axios
.get(
.get<SteamAppDetailsResponse>(
`http://store.steampowered.com/api/appdetails?${searchParams.toString()}`
)
.then((response) => {
if (response.data[objectId].success) return response.data[objectId].data;
if (response.data[objectId].success) {
const data = response.data[objectId].data;
return {
...data,
objectId,
};
}
return null;
})
.catch((err) => {

View File

@@ -0,0 +1,60 @@
import updater, { UpdateInfo } from "electron-updater";
import { logger, WindowManager } from "@main/services";
import { AppUpdaterEvent } from "@types";
import { app } from "electron";
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
const isAutoInstallAvailable =
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
const { autoUpdater } = updater;
const sendEventsForDebug = false;
export class UpdateManager {
private static hasNotified = false;
private static newVersion = "";
private static checkTick = 0;
private static mockValuesForDebug() {
this.sendEvent({ type: "update-available", info: { version: "1.3.0" } });
this.sendEvent({ type: "update-downloaded" });
}
private static sendEvent(event: AppUpdaterEvent) {
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
}
public static checkForUpdates() {
autoUpdater
.once("update-available", (info: UpdateInfo) => {
this.sendEvent({ type: "update-available", info });
this.newVersion = info.version;
})
.once("update-downloaded", () => {
this.sendEvent({ type: "update-downloaded" });
if (!this.hasNotified) {
this.hasNotified = true;
publishNotificationUpdateReadyToInstall(this.newVersion);
}
});
if (app.isPackaged) {
autoUpdater.autoDownload = isAutoInstallAvailable;
autoUpdater.checkForUpdates().then((result) => {
logger.log(`Check for updates result: ${result}`);
});
} else if (sendEventsForDebug) {
this.mockValuesForDebug();
}
return isAutoInstallAvailable;
}
public static checkForUpdatePeriodically() {
if (this.checkTick % 2000 == 0) {
this.checkForUpdates();
}
this.checkTick++;
}
}

View File

@@ -29,7 +29,6 @@ export const getUserData = async () => {
})
.catch(async (err) => {
if (err instanceof UserNotLoggedInError) {
logger.info("User is not logged in", err);
return null;
}
logger.error("Failed to get logged user");
@@ -59,6 +58,7 @@ export const getUserData = async () => {
expiresAt: loggedUser.subscription.expiresAt,
}
: null,
featurebaseJwt: "",
} as UserDetails;
}

View File

@@ -19,6 +19,7 @@ import { db, gamesSublevel, levelKeys } from "@main/level";
import { slice, sortBy } from "lodash-es";
import type { UserPreferences } from "@types";
import { AuthPage } from "@shared";
import { isStaging } from "@main/constants";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
@@ -52,7 +53,7 @@ export class WindowManager {
minHeight: 540,
backgroundColor: "#1c1c1c",
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
...(process.platform === "linux" ? { icon } : {}),
icon,
trafficLightPosition: { x: 16, y: 16 },
titleBarOverlay: {
symbolColor: "#DADBE1",
@@ -129,7 +130,8 @@ export class WindowManager {
this.mainWindow.removeMenu();
this.mainWindow.on("ready-to-show", () => {
if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools();
if (!app.isPackaged || isStaging)
WindowManager.mainWindow?.webContents.openDevTools();
WindowManager.mainWindow?.show();
});
@@ -147,6 +149,11 @@ export class WindowManager {
WindowManager.mainWindow?.setProgressBar(-1);
WindowManager.mainWindow = null;
});
this.mainWindow.webContents.setWindowOpenHandler((handler) => {
shell.openExternal(handler.url);
return { action: "deny" };
});
}
public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {