mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-12 11:12:07 +00:00
Merge pull request #901 from hydralauncher/fix/http-downloads-duplicate
feat: reducing http downloads duplicate
This commit is contained in:
commit
aaf20b2aac
28 changed files with 244 additions and 284 deletions
|
@ -174,12 +174,9 @@
|
|||
"validate_download_source": "Validate",
|
||||
"remove_download_source": "Remove",
|
||||
"add_download_source": "Add source",
|
||||
"download_count_zero": "No downloads in list",
|
||||
"download_count_one": "{{countFormatted}} download in list",
|
||||
"download_count_other": "{{countFormatted}} downloads in list",
|
||||
"download_options_zero": "No download available",
|
||||
"download_options_one": "{{countFormatted}} download available",
|
||||
"download_options_other": "{{countFormatted}} downloads available",
|
||||
"download_count_zero": "No download options",
|
||||
"download_count_one": "{{countFormatted}} download option",
|
||||
"download_count_other": "{{countFormatted}} download options",
|
||||
"download_source_url": "Download source URL",
|
||||
"add_download_source_description": "Insert the URL containing the .json file",
|
||||
"download_source_up_to_date": "Up-to-date",
|
||||
|
|
|
@ -6,12 +6,12 @@ import {
|
|||
GameShopCache,
|
||||
Repack,
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
} from "@main/entity";
|
||||
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
import migrations from "./migrations";
|
||||
import { UserAuth } from "./entity/user-auth";
|
||||
|
||||
export const createDataSource = (
|
||||
options: Partial<BetterSqlite3ConnectionOptions>
|
||||
|
|
|
@ -16,11 +16,14 @@ export class Repack {
|
|||
@Column("text", { unique: true })
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* @deprecated Use uris instead
|
||||
*/
|
||||
@Column("text", { unique: true })
|
||||
magnet: string;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @deprecated Direct scraping capability has been removed
|
||||
*/
|
||||
@Column("int", { nullable: true })
|
||||
page: number;
|
||||
|
@ -37,6 +40,9 @@ export class Repack {
|
|||
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
|
||||
downloadSource: DownloadSource;
|
||||
|
||||
@Column("text", { default: "[]" })
|
||||
uris: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return downloadSourceRepository
|
||||
.createQueryBuilder("downloadSource")
|
||||
.leftJoin("downloadSource.repacks", "repacks")
|
||||
.orderBy("downloadSource.createdAt", "DESC")
|
||||
.loadRelationCountAndMap(
|
||||
"downloadSource.repackCount",
|
||||
"downloadSource.repacks"
|
||||
)
|
||||
.getMany();
|
||||
};
|
||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
downloadSourceRepository.find({
|
||||
order: {
|
||||
createdAt: "DESC",
|
||||
},
|
||||
});
|
||||
|
||||
registerEvent("getDownloadSources", getDownloadSources);
|
||||
|
|
|
@ -18,7 +18,8 @@ const startGameDownload = async (
|
|||
_event: Electron.IpcMainInvokeEvent,
|
||||
payload: StartGameDownloadPayload
|
||||
) => {
|
||||
const { repackId, objectID, title, shop, downloadPath, downloader } = payload;
|
||||
const { repackId, objectID, title, shop, downloadPath, downloader, uri } =
|
||||
payload;
|
||||
|
||||
const [game, repack] = await Promise.all([
|
||||
gameRepository.findOne({
|
||||
|
@ -54,7 +55,7 @@ const startGameDownload = async (
|
|||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
uri: repack.magnet,
|
||||
uri,
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
|
@ -76,7 +77,7 @@ const startGameDownload = async (
|
|||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
uri: repack.magnet,
|
||||
uri,
|
||||
})
|
||||
.then((result) => {
|
||||
if (iconUrl) {
|
||||
|
@ -100,6 +101,7 @@ const startGameDownload = async (
|
|||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ export const insertDownloadsFromSource = async (
|
|||
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
|
||||
(download) => ({
|
||||
title: download.title,
|
||||
magnet: download.uris[0],
|
||||
uris: JSON.stringify(download.uris),
|
||||
magnet: download.uris[0]!,
|
||||
fileSize: download.fileSize,
|
||||
repacker: downloadSource.name,
|
||||
uploadDate: download.uploadDate,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import axios from "axios";
|
||||
import { JSDOM } from "jsdom";
|
||||
import UserAgent from "user-agents";
|
||||
|
||||
export const getSteamAppAsset = (
|
||||
|
@ -48,13 +49,16 @@ export const sleep = (ms: number) =>
|
|||
export const requestWebPage = async (url: string) => {
|
||||
const userAgent = new UserAgent();
|
||||
|
||||
return axios
|
||||
const data = await axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
"User-Agent": userAgent.toString(),
|
||||
},
|
||||
})
|
||||
.then((response) => response.data);
|
||||
|
||||
const { window } = new JSDOM(data);
|
||||
return window.document;
|
||||
};
|
||||
|
||||
export const isPortableVersion = () =>
|
||||
|
|
|
@ -20,8 +20,6 @@ autoUpdater.setFeedURL({
|
|||
|
||||
autoUpdater.logger = logger;
|
||||
|
||||
logger.log("Init Hydra");
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) app.quit();
|
||||
|
||||
|
@ -123,7 +121,6 @@ app.on("window-all-closed", () => {
|
|||
app.on("before-quit", () => {
|
||||
/* Disconnects libtorrent */
|
||||
PythonInstance.kill();
|
||||
logger.log("Quit Hydra");
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
|
|
|
@ -6,8 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
|
|||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import { RealDebridDownloader } from "./real-debrid-downloader";
|
||||
import type { DownloadProgress } from "@types";
|
||||
import { GofileApi } from "../hosters";
|
||||
import { GenericHTTPDownloader } from "./generic-http-downloader";
|
||||
import { GofileApi, QiwiApi } from "../hosters";
|
||||
import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||
|
||||
export class DownloadManager {
|
||||
private static currentDownloader: Downloader | null = null;
|
||||
|
@ -20,7 +20,7 @@ export class DownloadManager {
|
|||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
||||
status = await RealDebridDownloader.getStatus();
|
||||
} else {
|
||||
status = await GenericHTTPDownloader.getStatus();
|
||||
status = await GenericHttpDownloader.getStatus();
|
||||
}
|
||||
|
||||
if (status) {
|
||||
|
@ -71,7 +71,7 @@ export class DownloadManager {
|
|||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
||||
await RealDebridDownloader.pauseDownload();
|
||||
} else {
|
||||
await GenericHTTPDownloader.pauseDownload();
|
||||
await GenericHttpDownloader.pauseDownload();
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
|
@ -88,7 +88,7 @@ export class DownloadManager {
|
|||
} else if (this.currentDownloader === Downloader.RealDebrid) {
|
||||
RealDebridDownloader.cancelDownload(gameId);
|
||||
} else {
|
||||
GenericHTTPDownloader.cancelDownload(gameId);
|
||||
GenericHttpDownloader.cancelDownload(gameId);
|
||||
}
|
||||
|
||||
WindowManager.mainWindow?.setProgressBar(-1);
|
||||
|
@ -96,26 +96,38 @@ export class DownloadManager {
|
|||
}
|
||||
|
||||
static async startDownload(game: Game) {
|
||||
if (game.downloader === Downloader.Gofile) {
|
||||
const id = game!.uri!.split("/").pop();
|
||||
switch (game.downloader) {
|
||||
case Downloader.Gofile: {
|
||||
const id = game!.uri!.split("/").pop();
|
||||
|
||||
const token = await GofileApi.authorize();
|
||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||
const token = await GofileApi.authorize();
|
||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||
|
||||
GenericHTTPDownloader.startDownload(game, downloadLink, {
|
||||
Cookie: `accountToken=${token}`,
|
||||
});
|
||||
} else if (game.downloader === Downloader.PixelDrain) {
|
||||
const id = game!.uri!.split("/").pop();
|
||||
GenericHttpDownloader.startDownload(game, downloadLink, {
|
||||
Cookie: `accountToken=${token}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case Downloader.PixelDrain: {
|
||||
const id = game!.uri!.split("/").pop();
|
||||
|
||||
await GenericHTTPDownloader.startDownload(
|
||||
game,
|
||||
`https://pixeldrain.com/api/file/${id}?download`
|
||||
);
|
||||
} else if (game.downloader === Downloader.Torrent) {
|
||||
PythonInstance.startDownload(game);
|
||||
} else if (game.downloader === Downloader.RealDebrid) {
|
||||
RealDebridDownloader.startDownload(game);
|
||||
await GenericHttpDownloader.startDownload(
|
||||
game,
|
||||
`https://pixeldrain.com/api/file/${id}?download`
|
||||
);
|
||||
break;
|
||||
}
|
||||
case Downloader.Qiwi: {
|
||||
const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
|
||||
|
||||
await GenericHttpDownloader.startDownload(game, downloadUrl);
|
||||
break;
|
||||
}
|
||||
case Downloader.Torrent:
|
||||
PythonInstance.startDownload(game);
|
||||
break;
|
||||
case Downloader.RealDebrid:
|
||||
RealDebridDownloader.startDownload(game);
|
||||
}
|
||||
|
||||
this.currentDownloader = game.downloader;
|
||||
|
|
|
@ -4,14 +4,14 @@ import { calculateETA } from "./helpers";
|
|||
import { DownloadProgress } from "@types";
|
||||
import { HttpDownload } from "./http-download";
|
||||
|
||||
export class GenericHTTPDownloader {
|
||||
private static downloads = new Map<number, string>();
|
||||
private static downloadingGame: Game | null = null;
|
||||
export class GenericHttpDownloader {
|
||||
public static downloads = new Map<number, HttpDownload>();
|
||||
public static downloadingGame: Game | null = null;
|
||||
|
||||
public static async getStatus() {
|
||||
if (this.downloadingGame) {
|
||||
const gid = this.downloads.get(this.downloadingGame.id)!;
|
||||
const status = HttpDownload.getStatus(gid);
|
||||
const download = this.downloads.get(this.downloadingGame.id)!;
|
||||
const status = download.getStatus();
|
||||
|
||||
if (status) {
|
||||
const progress =
|
||||
|
@ -57,10 +57,10 @@ export class GenericHTTPDownloader {
|
|||
|
||||
static async pauseDownload() {
|
||||
if (this.downloadingGame) {
|
||||
const gid = this.downloads.get(this.downloadingGame!.id!);
|
||||
const httpDownload = this.downloads.get(this.downloadingGame!.id!);
|
||||
|
||||
if (gid) {
|
||||
await HttpDownload.pauseDownload(gid);
|
||||
if (httpDownload) {
|
||||
await httpDownload.pauseDownload();
|
||||
}
|
||||
|
||||
this.downloadingGame = null;
|
||||
|
@ -79,29 +79,31 @@ export class GenericHTTPDownloader {
|
|||
return;
|
||||
}
|
||||
|
||||
const gid = await HttpDownload.startDownload(
|
||||
const httpDownload = new HttpDownload(
|
||||
game.downloadPath!,
|
||||
downloadUrl,
|
||||
headers
|
||||
);
|
||||
|
||||
this.downloads.set(game.id!, gid);
|
||||
httpDownload.startDownload();
|
||||
|
||||
this.downloads.set(game.id!, httpDownload);
|
||||
}
|
||||
|
||||
static async cancelDownload(gameId: number) {
|
||||
const gid = this.downloads.get(gameId);
|
||||
const httpDownload = this.downloads.get(gameId);
|
||||
|
||||
if (gid) {
|
||||
await HttpDownload.cancelDownload(gid);
|
||||
if (httpDownload) {
|
||||
await httpDownload.cancelDownload();
|
||||
this.downloads.delete(gameId);
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeDownload(gameId: number) {
|
||||
const gid = this.downloads.get(gameId);
|
||||
const httpDownload = this.downloads.get(gameId);
|
||||
|
||||
if (gid) {
|
||||
await HttpDownload.resumeDownload(gid);
|
||||
if (httpDownload) {
|
||||
await httpDownload.resumeDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,67 +1,52 @@
|
|||
import { DownloadItem } from "electron";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import path from "node:path";
|
||||
|
||||
export class HttpDownload {
|
||||
private static id = 0;
|
||||
private downloadItem: Electron.DownloadItem;
|
||||
|
||||
private static downloads: Record<string, DownloadItem> = {};
|
||||
constructor(
|
||||
private downloadPath: string,
|
||||
private downloadUrl: string,
|
||||
private headers?: Record<string, string>
|
||||
) {}
|
||||
|
||||
public static getStatus(gid: string): {
|
||||
completedLength: number;
|
||||
totalLength: number;
|
||||
downloadSpeed: number;
|
||||
folderName: string;
|
||||
} | null {
|
||||
const downloadItem = this.downloads[gid];
|
||||
if (downloadItem) {
|
||||
return {
|
||||
completedLength: downloadItem.getReceivedBytes(),
|
||||
totalLength: downloadItem.getTotalBytes(),
|
||||
downloadSpeed: downloadItem.getCurrentBytesPerSecond(),
|
||||
folderName: downloadItem.getFilename(),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
public getStatus() {
|
||||
return {
|
||||
completedLength: this.downloadItem.getReceivedBytes(),
|
||||
totalLength: this.downloadItem.getTotalBytes(),
|
||||
downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
|
||||
folderName: this.downloadItem.getFilename(),
|
||||
};
|
||||
}
|
||||
|
||||
static async cancelDownload(gid: string) {
|
||||
const downloadItem = this.downloads[gid];
|
||||
downloadItem?.cancel();
|
||||
delete this.downloads[gid];
|
||||
async cancelDownload() {
|
||||
this.downloadItem.cancel();
|
||||
}
|
||||
|
||||
static async pauseDownload(gid: string) {
|
||||
const downloadItem = this.downloads[gid];
|
||||
downloadItem?.pause();
|
||||
async pauseDownload() {
|
||||
this.downloadItem.pause();
|
||||
}
|
||||
|
||||
static async resumeDownload(gid: string) {
|
||||
const downloadItem = this.downloads[gid];
|
||||
downloadItem?.resume();
|
||||
async resumeDownload() {
|
||||
this.downloadItem.resume();
|
||||
}
|
||||
|
||||
static async startDownload(
|
||||
downloadPath: string,
|
||||
downloadUrl: string,
|
||||
headers?: Record<string, string>
|
||||
) {
|
||||
return new Promise<string>((resolve) => {
|
||||
const options = headers ? { headers } : {};
|
||||
WindowManager.mainWindow?.webContents.downloadURL(downloadUrl, options);
|
||||
async startDownload() {
|
||||
return new Promise((resolve) => {
|
||||
const options = this.headers ? { headers: this.headers } : {};
|
||||
WindowManager.mainWindow?.webContents.downloadURL(
|
||||
this.downloadUrl,
|
||||
options
|
||||
);
|
||||
|
||||
const gid = ++this.id;
|
||||
|
||||
WindowManager.mainWindow?.webContents.session.on(
|
||||
WindowManager.mainWindow?.webContents.session.once(
|
||||
"will-download",
|
||||
(_event, item, _webContents) => {
|
||||
this.downloads[gid.toString()] = item;
|
||||
this.downloadItem = item;
|
||||
|
||||
// Set the save path, making Electron not to prompt a save dialog.
|
||||
item.setSavePath(path.join(downloadPath, item.getFilename()));
|
||||
item.setSavePath(path.join(this.downloadPath, item.getFilename()));
|
||||
|
||||
resolve(gid.toString());
|
||||
resolve(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import { Game } from "@main/entity";
|
||||
import { RealDebridClient } from "../real-debrid";
|
||||
import { gameRepository } from "@main/repository";
|
||||
import { calculateETA } from "./helpers";
|
||||
import { DownloadProgress } from "@types";
|
||||
import { HttpDownload } from "./http-download";
|
||||
import { GenericHttpDownloader } from "./generic-http-downloader";
|
||||
|
||||
export class RealDebridDownloader {
|
||||
private static downloads = new Map<number, string>();
|
||||
private static downloadingGame: Game | null = null;
|
||||
|
||||
export class RealDebridDownloader extends GenericHttpDownloader {
|
||||
private static realDebridTorrentId: string | null = null;
|
||||
|
||||
private static async getRealDebridDownloadUrl() {
|
||||
|
@ -48,66 +43,6 @@ export class RealDebridDownloader {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static async getStatus() {
|
||||
if (this.downloadingGame) {
|
||||
const gid = this.downloads.get(this.downloadingGame.id)!;
|
||||
const status = HttpDownload.getStatus(gid);
|
||||
|
||||
if (status) {
|
||||
const progress =
|
||||
Number(status.completedLength) / Number(status.totalLength);
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: this.downloadingGame!.id },
|
||||
{
|
||||
bytesDownloaded: Number(status.completedLength),
|
||||
fileSize: Number(status.totalLength),
|
||||
progress,
|
||||
status: "active",
|
||||
folderName: status.folderName,
|
||||
}
|
||||
);
|
||||
|
||||
const result = {
|
||||
numPeers: 0,
|
||||
numSeeds: 0,
|
||||
downloadSpeed: Number(status.downloadSpeed),
|
||||
timeRemaining: calculateETA(
|
||||
Number(status.totalLength),
|
||||
Number(status.completedLength),
|
||||
Number(status.downloadSpeed)
|
||||
),
|
||||
isDownloadingMetadata: false,
|
||||
isCheckingFiles: false,
|
||||
progress,
|
||||
gameId: this.downloadingGame!.id,
|
||||
} as DownloadProgress;
|
||||
|
||||
if (progress === 1) {
|
||||
this.downloads.delete(this.downloadingGame.id);
|
||||
this.realDebridTorrentId = null;
|
||||
this.downloadingGame = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async pauseDownload() {
|
||||
if (this.downloadingGame) {
|
||||
const gid = this.downloads.get(this.downloadingGame.id);
|
||||
if (gid) {
|
||||
await HttpDownload.pauseDownload(gid);
|
||||
}
|
||||
}
|
||||
|
||||
this.realDebridTorrentId = null;
|
||||
this.downloadingGame = null;
|
||||
}
|
||||
|
||||
static async startDownload(game: Game) {
|
||||
if (this.downloads.has(game.id)) {
|
||||
await this.resumeDownload(game.id!);
|
||||
|
@ -128,32 +63,10 @@ export class RealDebridDownloader {
|
|||
if (downloadUrl) {
|
||||
this.realDebridTorrentId = null;
|
||||
|
||||
const gid = await HttpDownload.startDownload(
|
||||
game.downloadPath!,
|
||||
downloadUrl
|
||||
);
|
||||
const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
|
||||
httpDownload.startDownload();
|
||||
|
||||
this.downloads.set(game.id!, gid);
|
||||
}
|
||||
}
|
||||
|
||||
static async cancelDownload(gameId: number) {
|
||||
const gid = this.downloads.get(gameId);
|
||||
|
||||
if (gid) {
|
||||
await HttpDownload.cancelDownload(gid);
|
||||
this.downloads.delete(gameId);
|
||||
}
|
||||
|
||||
this.realDebridTorrentId = null;
|
||||
this.downloadingGame = null;
|
||||
}
|
||||
|
||||
static async resumeDownload(gameId: number) {
|
||||
const gid = this.downloads.get(gameId);
|
||||
|
||||
if (gid) {
|
||||
await HttpDownload.resumeDownload(gid);
|
||||
this.downloads.set(game.id!, httpDownload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ export interface GofileContentsResponse {
|
|||
children: Record<string, GofileContentChild>;
|
||||
}
|
||||
|
||||
export const WT = "4fd6sg89d7s6";
|
||||
|
||||
export class GofileApi {
|
||||
private static token: string;
|
||||
|
||||
|
@ -35,7 +37,7 @@ export class GofileApi {
|
|||
|
||||
public static async getDownloadLink(id: string) {
|
||||
const searchParams = new URLSearchParams({
|
||||
wt: "4fd6sg89d7s6",
|
||||
wt: WT,
|
||||
});
|
||||
|
||||
const response = await axios.get<{
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from "./gofile";
|
||||
export * from "./qiwi";
|
||||
|
|
15
src/main/services/hosters/qiwi.ts
Normal file
15
src/main/services/hosters/qiwi.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { requestWebPage } from "@main/helpers";
|
||||
|
||||
export class QiwiApi {
|
||||
public static async getDownloadUrl(url: string) {
|
||||
const document = await requestWebPage(url);
|
||||
const fileName = document.querySelector("h1")?.textContent;
|
||||
|
||||
const slug = url.split("/").pop();
|
||||
const extension = fileName?.split(".").pop();
|
||||
|
||||
const downloadUrl = `https://spyderrock.com/${slug}.${extension}`;
|
||||
|
||||
return downloadUrl;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import axios from "axios";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { requestWebPage } from "@main/helpers";
|
||||
import { HowLongToBeatCategory } from "@types";
|
||||
import { formatName } from "@shared";
|
||||
|
@ -52,10 +51,7 @@ const parseListItems = ($lis: Element[]) => {
|
|||
export const getHowLongToBeatGame = async (
|
||||
id: string
|
||||
): Promise<HowLongToBeatCategory[]> => {
|
||||
const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
||||
|
||||
const { window } = new JSDOM(response);
|
||||
const { document } = window;
|
||||
const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
||||
|
||||
const $ul = document.querySelector(".shadow_shadow ul");
|
||||
if (!$ul) return [];
|
||||
|
|
|
@ -77,54 +77,54 @@ export class HydraApi {
|
|||
baseURL: import.meta.env.MAIN_VITE_API_URL,
|
||||
});
|
||||
|
||||
this.instance.interceptors.request.use(
|
||||
(request) => {
|
||||
logger.log(" ---- REQUEST -----");
|
||||
logger.log(request.method, request.url, request.params, request.data);
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
logger.error("request error", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// this.instance.interceptors.request.use(
|
||||
// (request) => {
|
||||
// logger.log(" ---- REQUEST -----");
|
||||
// logger.log(request.method, request.url, request.params, request.data);
|
||||
// return request;
|
||||
// },
|
||||
// (error) => {
|
||||
// logger.error("request error", error);
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
logger.log(" ---- RESPONSE -----");
|
||||
logger.log(
|
||||
response.status,
|
||||
response.config.method,
|
||||
response.config.url,
|
||||
response.data
|
||||
);
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
logger.error(" ---- RESPONSE ERROR -----");
|
||||
// this.instance.interceptors.response.use(
|
||||
// (response) => {
|
||||
// logger.log(" ---- RESPONSE -----");
|
||||
// logger.log(
|
||||
// response.status,
|
||||
// response.config.method,
|
||||
// response.config.url,
|
||||
// response.data
|
||||
// );
|
||||
// return response;
|
||||
// },
|
||||
// (error) => {
|
||||
// logger.error(" ---- RESPONSE ERROR -----");
|
||||
|
||||
const { config } = error;
|
||||
// const { config } = error;
|
||||
|
||||
logger.error(
|
||||
config.method,
|
||||
config.baseURL,
|
||||
config.url,
|
||||
config.headers,
|
||||
config.data
|
||||
);
|
||||
// logger.error(
|
||||
// config.method,
|
||||
// config.baseURL,
|
||||
// config.url,
|
||||
// config.headers,
|
||||
// config.data
|
||||
// );
|
||||
|
||||
if (error.response) {
|
||||
logger.error("Response", error.response.status, error.response.data);
|
||||
} else if (error.request) {
|
||||
logger.error("Request", error.request);
|
||||
} else {
|
||||
logger.error("Error", error.message);
|
||||
}
|
||||
// if (error.response) {
|
||||
// logger.error("Response", error.response.status, error.response.data);
|
||||
// } else if (error.request) {
|
||||
// logger.error("Request", error.request);
|
||||
// } else {
|
||||
// logger.error("Error", error.message);
|
||||
// }
|
||||
|
||||
logger.error(" ----- END RESPONSE ERROR -------");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// logger.error(" ----- END RESPONSE ERROR -------");
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
|
||||
const userAuth = await userAuthRepository.findOne({
|
||||
where: { id: 1 },
|
||||
|
|
|
@ -8,11 +8,25 @@ export class RepacksManager {
|
|||
private static repacksIndex = new flexSearch.Index();
|
||||
|
||||
public static async updateRepacks() {
|
||||
this.repacks = await repackRepository.find({
|
||||
order: {
|
||||
createdAt: "DESC",
|
||||
},
|
||||
});
|
||||
this.repacks = await repackRepository
|
||||
.find({
|
||||
order: {
|
||||
createdAt: "DESC",
|
||||
},
|
||||
})
|
||||
.then((repacks) =>
|
||||
repacks.map((repack) => {
|
||||
const uris: string[] = [];
|
||||
const magnet = repack?.magnet;
|
||||
|
||||
if (magnet) uris.push(magnet);
|
||||
|
||||
return {
|
||||
...repack,
|
||||
uris: [...uris, ...JSON.parse(repack.uris)],
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
for (let i = 0; i < this.repacks.length; i++) {
|
||||
this.repacksIndex.remove(i);
|
||||
|
|
|
@ -7,4 +7,5 @@ export const DOWNLOADER_NAME = {
|
|||
[Downloader.Torrent]: "Torrent",
|
||||
[Downloader.Gofile]: "Gofile",
|
||||
[Downloader.PixelDrain]: "PixelDrain",
|
||||
[Downloader.Qiwi]: "Qiwi",
|
||||
};
|
||||
|
|
|
@ -22,9 +22,10 @@ export function useDownload() {
|
|||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const startDownload = (payload: StartGameDownloadPayload) => {
|
||||
const startDownload = async (payload: StartGameDownloadPayload) => {
|
||||
dispatch(clearDownload());
|
||||
window.electron.startGameDownload(payload).then((game) => {
|
||||
|
||||
return window.electron.startGameDownload(payload).then((game) => {
|
||||
updateLibrary();
|
||||
|
||||
return game;
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from "@renderer/context";
|
||||
import { useDownload } from "@renderer/hooks";
|
||||
import { GameOptionsModal, RepacksModal } from "./modals";
|
||||
import { Downloader } from "@shared";
|
||||
import { Downloader, getDownloadersForUri } from "@shared";
|
||||
|
||||
export function GameDetails() {
|
||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||
|
@ -70,6 +70,9 @@ export function GameDetails() {
|
|||
}
|
||||
};
|
||||
|
||||
const selectRepackUri = (repack: GameRepack, downloader: Downloader) =>
|
||||
repack.uris.find((uri) => getDownloadersForUri(uri).includes(downloader))!;
|
||||
|
||||
return (
|
||||
<GameDetailsContextProvider>
|
||||
<GameDetailsContextConsumer>
|
||||
|
@ -96,6 +99,7 @@ export function GameDetails() {
|
|||
downloader,
|
||||
shop: shop as GameShop,
|
||||
downloadPath,
|
||||
uri: selectRepackUri(repack, downloader),
|
||||
});
|
||||
|
||||
await updateGame();
|
||||
|
|
|
@ -20,13 +20,16 @@ export const hintText = style({
|
|||
});
|
||||
|
||||
export const downloaders = style({
|
||||
display: "flex",
|
||||
display: "grid",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
});
|
||||
|
||||
export const downloaderOption = style({
|
||||
flex: "1",
|
||||
position: "relative",
|
||||
":only-child": {
|
||||
gridColumn: "1 / -1",
|
||||
},
|
||||
});
|
||||
|
||||
export const downloaderIcon = style({
|
||||
|
|
|
@ -5,7 +5,7 @@ import { DiskSpace } from "check-disk-space";
|
|||
import * as styles from "./download-settings-modal.css";
|
||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
|
||||
import { Downloader, formatBytes, getDownloadersForUri } from "@shared";
|
||||
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
|
||||
|
||||
import type { GameRepack } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
@ -48,8 +48,8 @@ export function DownloadSettingsModal({
|
|||
}, [visible, selectedPath]);
|
||||
|
||||
const downloaders = useMemo(() => {
|
||||
return getDownloadersForUri(repack?.magnet ?? "");
|
||||
}, [repack?.magnet]);
|
||||
return getDownloadersForUris(repack?.uris ?? []);
|
||||
}, [repack?.uris]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userPreferences?.downloadsPath) {
|
||||
|
|
|
@ -76,6 +76,13 @@ export function RepacksModal({
|
|||
);
|
||||
};
|
||||
|
||||
const checkIfLastDownloadedOption = (repack: GameRepack) => {
|
||||
if (infoHash) return repack.uris.some((uri) => uri.includes(infoHash));
|
||||
if (!game?.uri) return false;
|
||||
|
||||
return repack.uris.some((uri) => uri.includes(game?.uri ?? ""));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DownloadSettingsModal
|
||||
|
@ -97,9 +104,7 @@ export function RepacksModal({
|
|||
|
||||
<div className={styles.repacks}>
|
||||
{filteredRepacks.map((repack) => {
|
||||
const isLastDownloadedOption =
|
||||
infoHash !== null &&
|
||||
repack.magnet.toLowerCase().includes(infoHash);
|
||||
const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
|
@ -42,10 +42,3 @@ export const downloadSourcesHeader = style({
|
|||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const separator = style({
|
||||
height: "100%",
|
||||
width: "1px",
|
||||
backgroundColor: vars.color.border,
|
||||
margin: `${SPACING_UNIT}px 0`,
|
||||
});
|
||||
|
|
|
@ -134,15 +134,6 @@ export function SettingsDownloadSources() {
|
|||
downloadSource.downloadCount.toLocaleString(),
|
||||
})}
|
||||
</small>
|
||||
|
||||
<div className={styles.separator} />
|
||||
|
||||
<small>
|
||||
{t("download_options", {
|
||||
count: downloadSource.repackCount,
|
||||
countFormatted: downloadSource.repackCount.toLocaleString(),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ export enum Downloader {
|
|||
Torrent,
|
||||
Gofile,
|
||||
PixelDrain,
|
||||
Qiwi,
|
||||
}
|
||||
|
||||
export enum DownloadSourceStatus {
|
||||
|
@ -73,13 +74,27 @@ const realDebridHosts = ["https://1fichier.com", "https://mediafire.com"];
|
|||
|
||||
export const getDownloadersForUri = (uri: string) => {
|
||||
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
|
||||
|
||||
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain];
|
||||
if (uri.startsWith("https://qiwi.gg")) return [Downloader.Qiwi];
|
||||
|
||||
if (realDebridHosts.some((host) => uri.startsWith(host)))
|
||||
return [Downloader.RealDebrid];
|
||||
|
||||
if (uri.startsWith("magnet:"))
|
||||
if (uri.startsWith("magnet:")) {
|
||||
return [Downloader.Torrent, Downloader.RealDebrid];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getDownloadersForUris = (uris: string[]) => {
|
||||
const downloadersSet = uris.reduce<Set<Downloader>>((prev, next) => {
|
||||
const downloaders = getDownloadersForUri(next);
|
||||
downloaders.forEach((downloader) => prev.add(downloader));
|
||||
|
||||
return prev;
|
||||
}, new Set());
|
||||
|
||||
return Array.from(downloadersSet);
|
||||
};
|
||||
|
|
|
@ -67,7 +67,11 @@ export interface SteamAppDetails {
|
|||
export interface GameRepack {
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* @deprecated Use uris instead
|
||||
*/
|
||||
magnet: string;
|
||||
uris: string[];
|
||||
repacker: string;
|
||||
fileSize: string | null;
|
||||
uploadDate: Date | string | null;
|
||||
|
@ -194,6 +198,7 @@ export interface StartGameDownloadPayload {
|
|||
objectID: string;
|
||||
title: string;
|
||||
shop: GameShop;
|
||||
uri: string;
|
||||
downloadPath: string;
|
||||
downloader: Downloader;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue