Merge pull request #901 from hydralauncher/fix/http-downloads-duplicate

feat: reducing http downloads duplicate
This commit is contained in:
Chubby Granny Chaser 2024-08-20 01:22:47 +01:00 committed by GitHub
commit aaf20b2aac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 244 additions and 284 deletions

View file

@ -174,12 +174,9 @@
"validate_download_source": "Validate", "validate_download_source": "Validate",
"remove_download_source": "Remove", "remove_download_source": "Remove",
"add_download_source": "Add source", "add_download_source": "Add source",
"download_count_zero": "No downloads in list", "download_count_zero": "No download options",
"download_count_one": "{{countFormatted}} download in list", "download_count_one": "{{countFormatted}} download option",
"download_count_other": "{{countFormatted}} downloads in list", "download_count_other": "{{countFormatted}} download options",
"download_options_zero": "No download available",
"download_options_one": "{{countFormatted}} download available",
"download_options_other": "{{countFormatted}} downloads available",
"download_source_url": "Download source URL", "download_source_url": "Download source URL",
"add_download_source_description": "Insert the URL containing the .json file", "add_download_source_description": "Insert the URL containing the .json file",
"download_source_up_to_date": "Up-to-date", "download_source_up_to_date": "Up-to-date",

View file

@ -6,12 +6,12 @@ import {
GameShopCache, GameShopCache,
Repack, Repack,
UserPreferences, UserPreferences,
UserAuth,
} from "@main/entity"; } from "@main/entity";
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions"; import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
import { databasePath } from "./constants"; import { databasePath } from "./constants";
import migrations from "./migrations"; import migrations from "./migrations";
import { UserAuth } from "./entity/user-auth";
export const createDataSource = ( export const createDataSource = (
options: Partial<BetterSqlite3ConnectionOptions> options: Partial<BetterSqlite3ConnectionOptions>

View file

@ -16,11 +16,14 @@ export class Repack {
@Column("text", { unique: true }) @Column("text", { unique: true })
title: string; title: string;
/**
* @deprecated Use uris instead
*/
@Column("text", { unique: true }) @Column("text", { unique: true })
magnet: string; magnet: string;
/** /**
* @deprecated * @deprecated Direct scraping capability has been removed
*/ */
@Column("int", { nullable: true }) @Column("int", { nullable: true })
page: number; page: number;
@ -37,6 +40,9 @@ export class Repack {
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" }) @ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
downloadSource: DownloadSource; downloadSource: DownloadSource;
@Column("text", { default: "[]" })
uris: string;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;

View file

@ -1,16 +1,11 @@
import { downloadSourceRepository } from "@main/repository"; import { downloadSourceRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => { const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) =>
return downloadSourceRepository downloadSourceRepository.find({
.createQueryBuilder("downloadSource") order: {
.leftJoin("downloadSource.repacks", "repacks") createdAt: "DESC",
.orderBy("downloadSource.createdAt", "DESC") },
.loadRelationCountAndMap( });
"downloadSource.repackCount",
"downloadSource.repacks"
)
.getMany();
};
registerEvent("getDownloadSources", getDownloadSources); registerEvent("getDownloadSources", getDownloadSources);

View file

@ -18,7 +18,8 @@ const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
payload: StartGameDownloadPayload 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([ const [game, repack] = await Promise.all([
gameRepository.findOne({ gameRepository.findOne({
@ -54,7 +55,7 @@ const startGameDownload = async (
bytesDownloaded: 0, bytesDownloaded: 0,
downloadPath, downloadPath,
downloader, downloader,
uri: repack.magnet, uri,
isDeleted: false, isDeleted: false,
} }
); );
@ -76,7 +77,7 @@ const startGameDownload = async (
shop, shop,
status: "active", status: "active",
downloadPath, downloadPath,
uri: repack.magnet, uri,
}) })
.then((result) => { .then((result) => {
if (iconUrl) { if (iconUrl) {
@ -100,6 +101,7 @@ const startGameDownload = async (
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } }); await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } }); await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!); await DownloadManager.startDownload(updatedGame!);
}; };

View file

@ -17,7 +17,8 @@ export const insertDownloadsFromSource = async (
const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map( const repacks: QueryDeepPartialEntity<Repack>[] = downloads.map(
(download) => ({ (download) => ({
title: download.title, title: download.title,
magnet: download.uris[0], uris: JSON.stringify(download.uris),
magnet: download.uris[0]!,
fileSize: download.fileSize, fileSize: download.fileSize,
repacker: downloadSource.name, repacker: downloadSource.name,
uploadDate: download.uploadDate, uploadDate: download.uploadDate,

View file

@ -1,4 +1,5 @@
import axios from "axios"; import axios from "axios";
import { JSDOM } from "jsdom";
import UserAgent from "user-agents"; import UserAgent from "user-agents";
export const getSteamAppAsset = ( export const getSteamAppAsset = (
@ -48,13 +49,16 @@ export const sleep = (ms: number) =>
export const requestWebPage = async (url: string) => { export const requestWebPage = async (url: string) => {
const userAgent = new UserAgent(); const userAgent = new UserAgent();
return axios const data = await axios
.get(url, { .get(url, {
headers: { headers: {
"User-Agent": userAgent.toString(), "User-Agent": userAgent.toString(),
}, },
}) })
.then((response) => response.data); .then((response) => response.data);
const { window } = new JSDOM(data);
return window.document;
}; };
export const isPortableVersion = () => export const isPortableVersion = () =>

View file

@ -20,8 +20,6 @@ autoUpdater.setFeedURL({
autoUpdater.logger = logger; autoUpdater.logger = logger;
logger.log("Init Hydra");
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit(); if (!gotTheLock) app.quit();
@ -123,7 +121,6 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => { app.on("before-quit", () => {
/* Disconnects libtorrent */ /* Disconnects libtorrent */
PythonInstance.kill(); PythonInstance.kill();
logger.log("Quit Hydra");
}); });
app.on("activate", () => { app.on("activate", () => {

View file

@ -6,8 +6,8 @@ import { downloadQueueRepository, gameRepository } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications"; import { publishDownloadCompleteNotification } from "../notifications";
import { RealDebridDownloader } from "./real-debrid-downloader"; import { RealDebridDownloader } from "./real-debrid-downloader";
import type { DownloadProgress } from "@types"; import type { DownloadProgress } from "@types";
import { GofileApi } from "../hosters"; import { GofileApi, QiwiApi } from "../hosters";
import { GenericHTTPDownloader } from "./generic-http-downloader"; import { GenericHttpDownloader } from "./generic-http-downloader";
export class DownloadManager { export class DownloadManager {
private static currentDownloader: Downloader | null = null; private static currentDownloader: Downloader | null = null;
@ -20,7 +20,7 @@ export class DownloadManager {
} else if (this.currentDownloader === Downloader.RealDebrid) { } else if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus(); status = await RealDebridDownloader.getStatus();
} else { } else {
status = await GenericHTTPDownloader.getStatus(); status = await GenericHttpDownloader.getStatus();
} }
if (status) { if (status) {
@ -71,7 +71,7 @@ export class DownloadManager {
} else if (this.currentDownloader === Downloader.RealDebrid) { } else if (this.currentDownloader === Downloader.RealDebrid) {
await RealDebridDownloader.pauseDownload(); await RealDebridDownloader.pauseDownload();
} else { } else {
await GenericHTTPDownloader.pauseDownload(); await GenericHttpDownloader.pauseDownload();
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -88,7 +88,7 @@ export class DownloadManager {
} else if (this.currentDownloader === Downloader.RealDebrid) { } else if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload(gameId); RealDebridDownloader.cancelDownload(gameId);
} else { } else {
GenericHTTPDownloader.cancelDownload(gameId); GenericHttpDownloader.cancelDownload(gameId);
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -96,26 +96,38 @@ export class DownloadManager {
} }
static async startDownload(game: Game) { static async startDownload(game: Game) {
if (game.downloader === Downloader.Gofile) { switch (game.downloader) {
const id = game!.uri!.split("/").pop(); case Downloader.Gofile: {
const id = game!.uri!.split("/").pop();
const token = await GofileApi.authorize(); const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!); const downloadLink = await GofileApi.getDownloadLink(id!);
GenericHTTPDownloader.startDownload(game, downloadLink, { GenericHttpDownloader.startDownload(game, downloadLink, {
Cookie: `accountToken=${token}`, Cookie: `accountToken=${token}`,
}); });
} else if (game.downloader === Downloader.PixelDrain) { break;
const id = game!.uri!.split("/").pop(); }
case Downloader.PixelDrain: {
const id = game!.uri!.split("/").pop();
await GenericHTTPDownloader.startDownload( await GenericHttpDownloader.startDownload(
game, game,
`https://pixeldrain.com/api/file/${id}?download` `https://pixeldrain.com/api/file/${id}?download`
); );
} else if (game.downloader === Downloader.Torrent) { break;
PythonInstance.startDownload(game); }
} else if (game.downloader === Downloader.RealDebrid) { case Downloader.Qiwi: {
RealDebridDownloader.startDownload(game); 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; this.currentDownloader = game.downloader;

View file

@ -4,14 +4,14 @@ import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types"; import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download"; import { HttpDownload } from "./http-download";
export class GenericHTTPDownloader { export class GenericHttpDownloader {
private static downloads = new Map<number, string>(); public static downloads = new Map<number, HttpDownload>();
private static downloadingGame: Game | null = null; public static downloadingGame: Game | null = null;
public static async getStatus() { public static async getStatus() {
if (this.downloadingGame) { if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame.id)!; const download = this.downloads.get(this.downloadingGame.id)!;
const status = HttpDownload.getStatus(gid); const status = download.getStatus();
if (status) { if (status) {
const progress = const progress =
@ -57,10 +57,10 @@ export class GenericHTTPDownloader {
static async pauseDownload() { static async pauseDownload() {
if (this.downloadingGame) { if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame!.id!); const httpDownload = this.downloads.get(this.downloadingGame!.id!);
if (gid) { if (httpDownload) {
await HttpDownload.pauseDownload(gid); await httpDownload.pauseDownload();
} }
this.downloadingGame = null; this.downloadingGame = null;
@ -79,29 +79,31 @@ export class GenericHTTPDownloader {
return; return;
} }
const gid = await HttpDownload.startDownload( const httpDownload = new HttpDownload(
game.downloadPath!, game.downloadPath!,
downloadUrl, downloadUrl,
headers headers
); );
this.downloads.set(game.id!, gid); httpDownload.startDownload();
this.downloads.set(game.id!, httpDownload);
} }
static async cancelDownload(gameId: number) { static async cancelDownload(gameId: number) {
const gid = this.downloads.get(gameId); const httpDownload = this.downloads.get(gameId);
if (gid) { if (httpDownload) {
await HttpDownload.cancelDownload(gid); await httpDownload.cancelDownload();
this.downloads.delete(gameId); this.downloads.delete(gameId);
} }
} }
static async resumeDownload(gameId: number) { static async resumeDownload(gameId: number) {
const gid = this.downloads.get(gameId); const httpDownload = this.downloads.get(gameId);
if (gid) { if (httpDownload) {
await HttpDownload.resumeDownload(gid); await httpDownload.resumeDownload();
} }
} }
} }

View file

@ -1,67 +1,52 @@
import { DownloadItem } from "electron";
import { WindowManager } from "../window-manager"; import { WindowManager } from "../window-manager";
import path from "node:path"; import path from "node:path";
export class HttpDownload { 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): { public getStatus() {
completedLength: number; return {
totalLength: number; completedLength: this.downloadItem.getReceivedBytes(),
downloadSpeed: number; totalLength: this.downloadItem.getTotalBytes(),
folderName: string; downloadSpeed: this.downloadItem.getCurrentBytesPerSecond(),
} | null { folderName: this.downloadItem.getFilename(),
const downloadItem = this.downloads[gid]; };
if (downloadItem) {
return {
completedLength: downloadItem.getReceivedBytes(),
totalLength: downloadItem.getTotalBytes(),
downloadSpeed: downloadItem.getCurrentBytesPerSecond(),
folderName: downloadItem.getFilename(),
};
}
return null;
} }
static async cancelDownload(gid: string) { async cancelDownload() {
const downloadItem = this.downloads[gid]; this.downloadItem.cancel();
downloadItem?.cancel();
delete this.downloads[gid];
} }
static async pauseDownload(gid: string) { async pauseDownload() {
const downloadItem = this.downloads[gid]; this.downloadItem.pause();
downloadItem?.pause();
} }
static async resumeDownload(gid: string) { async resumeDownload() {
const downloadItem = this.downloads[gid]; this.downloadItem.resume();
downloadItem?.resume();
} }
static async startDownload( async startDownload() {
downloadPath: string, return new Promise((resolve) => {
downloadUrl: string, const options = this.headers ? { headers: this.headers } : {};
headers?: Record<string, string> WindowManager.mainWindow?.webContents.downloadURL(
) { this.downloadUrl,
return new Promise<string>((resolve) => { options
const options = headers ? { headers } : {}; );
WindowManager.mainWindow?.webContents.downloadURL(downloadUrl, options);
const gid = ++this.id; WindowManager.mainWindow?.webContents.session.once(
WindowManager.mainWindow?.webContents.session.on(
"will-download", "will-download",
(_event, item, _webContents) => { (_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(this.downloadPath, item.getFilename()));
item.setSavePath(path.join(downloadPath, item.getFilename()));
resolve(gid.toString()); resolve(null);
} }
); );
}); });

View file

@ -1,14 +1,9 @@
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import { RealDebridClient } from "../real-debrid"; import { RealDebridClient } from "../real-debrid";
import { gameRepository } from "@main/repository";
import { calculateETA } from "./helpers";
import { DownloadProgress } from "@types";
import { HttpDownload } from "./http-download"; import { HttpDownload } from "./http-download";
import { GenericHttpDownloader } from "./generic-http-downloader";
export class RealDebridDownloader { export class RealDebridDownloader extends GenericHttpDownloader {
private static downloads = new Map<number, string>();
private static downloadingGame: Game | null = null;
private static realDebridTorrentId: string | null = null; private static realDebridTorrentId: string | null = null;
private static async getRealDebridDownloadUrl() { private static async getRealDebridDownloadUrl() {
@ -48,66 +43,6 @@ export class RealDebridDownloader {
return null; 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) { static async startDownload(game: Game) {
if (this.downloads.has(game.id)) { if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!); await this.resumeDownload(game.id!);
@ -128,32 +63,10 @@ export class RealDebridDownloader {
if (downloadUrl) { if (downloadUrl) {
this.realDebridTorrentId = null; this.realDebridTorrentId = null;
const gid = await HttpDownload.startDownload( const httpDownload = new HttpDownload(game.downloadPath!, downloadUrl);
game.downloadPath!, httpDownload.startDownload();
downloadUrl
);
this.downloads.set(game.id!, gid); this.downloads.set(game.id!, httpDownload);
}
}
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);
} }
} }
} }

View file

@ -16,6 +16,8 @@ export interface GofileContentsResponse {
children: Record<string, GofileContentChild>; children: Record<string, GofileContentChild>;
} }
export const WT = "4fd6sg89d7s6";
export class GofileApi { export class GofileApi {
private static token: string; private static token: string;
@ -35,7 +37,7 @@ export class GofileApi {
public static async getDownloadLink(id: string) { public static async getDownloadLink(id: string) {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
wt: "4fd6sg89d7s6", wt: WT,
}); });
const response = await axios.get<{ const response = await axios.get<{

View file

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

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

View file

@ -1,5 +1,4 @@
import axios from "axios"; import axios from "axios";
import { JSDOM } from "jsdom";
import { requestWebPage } from "@main/helpers"; import { requestWebPage } from "@main/helpers";
import { HowLongToBeatCategory } from "@types"; import { HowLongToBeatCategory } from "@types";
import { formatName } from "@shared"; import { formatName } from "@shared";
@ -52,10 +51,7 @@ const parseListItems = ($lis: Element[]) => {
export const getHowLongToBeatGame = async ( export const getHowLongToBeatGame = async (
id: string id: string
): Promise<HowLongToBeatCategory[]> => { ): Promise<HowLongToBeatCategory[]> => {
const response = await requestWebPage(`https://howlongtobeat.com/game/${id}`); const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
const { window } = new JSDOM(response);
const { document } = window;
const $ul = document.querySelector(".shadow_shadow ul"); const $ul = document.querySelector(".shadow_shadow ul");
if (!$ul) return []; if (!$ul) return [];

View file

@ -77,54 +77,54 @@ export class HydraApi {
baseURL: import.meta.env.MAIN_VITE_API_URL, baseURL: import.meta.env.MAIN_VITE_API_URL,
}); });
this.instance.interceptors.request.use( // this.instance.interceptors.request.use(
(request) => { // (request) => {
logger.log(" ---- REQUEST -----"); // logger.log(" ---- REQUEST -----");
logger.log(request.method, request.url, request.params, request.data); // logger.log(request.method, request.url, request.params, request.data);
return request; // return request;
}, // },
(error) => { // (error) => {
logger.error("request error", error); // logger.error("request error", error);
return Promise.reject(error); // return Promise.reject(error);
} // }
); // );
this.instance.interceptors.response.use( // this.instance.interceptors.response.use(
(response) => { // (response) => {
logger.log(" ---- RESPONSE -----"); // logger.log(" ---- RESPONSE -----");
logger.log( // logger.log(
response.status, // response.status,
response.config.method, // response.config.method,
response.config.url, // response.config.url,
response.data // response.data
); // );
return response; // return response;
}, // },
(error) => { // (error) => {
logger.error(" ---- RESPONSE ERROR -----"); // logger.error(" ---- RESPONSE ERROR -----");
const { config } = error; // const { config } = error;
logger.error( // logger.error(
config.method, // config.method,
config.baseURL, // config.baseURL,
config.url, // config.url,
config.headers, // config.headers,
config.data // config.data
); // );
if (error.response) { // if (error.response) {
logger.error("Response", error.response.status, error.response.data); // logger.error("Response", error.response.status, error.response.data);
} else if (error.request) { // } else if (error.request) {
logger.error("Request", error.request); // logger.error("Request", error.request);
} else { // } else {
logger.error("Error", error.message); // logger.error("Error", error.message);
} // }
logger.error(" ----- END RESPONSE ERROR -------"); // logger.error(" ----- END RESPONSE ERROR -------");
return Promise.reject(error); // return Promise.reject(error);
} // }
); // );
const userAuth = await userAuthRepository.findOne({ const userAuth = await userAuthRepository.findOne({
where: { id: 1 }, where: { id: 1 },

View file

@ -8,11 +8,25 @@ export class RepacksManager {
private static repacksIndex = new flexSearch.Index(); private static repacksIndex = new flexSearch.Index();
public static async updateRepacks() { public static async updateRepacks() {
this.repacks = await repackRepository.find({ this.repacks = await repackRepository
order: { .find({
createdAt: "DESC", 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++) { for (let i = 0; i < this.repacks.length; i++) {
this.repacksIndex.remove(i); this.repacksIndex.remove(i);

View file

@ -7,4 +7,5 @@ export const DOWNLOADER_NAME = {
[Downloader.Torrent]: "Torrent", [Downloader.Torrent]: "Torrent",
[Downloader.Gofile]: "Gofile", [Downloader.Gofile]: "Gofile",
[Downloader.PixelDrain]: "PixelDrain", [Downloader.PixelDrain]: "PixelDrain",
[Downloader.Qiwi]: "Qiwi",
}; };

View file

@ -22,9 +22,10 @@ export function useDownload() {
); );
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const startDownload = (payload: StartGameDownloadPayload) => { const startDownload = async (payload: StartGameDownloadPayload) => {
dispatch(clearDownload()); dispatch(clearDownload());
window.electron.startGameDownload(payload).then((game) => {
return window.electron.startGameDownload(payload).then((game) => {
updateLibrary(); updateLibrary();
return game; return game;

View file

@ -23,7 +23,7 @@ import {
} from "@renderer/context"; } from "@renderer/context";
import { useDownload } from "@renderer/hooks"; import { useDownload } from "@renderer/hooks";
import { GameOptionsModal, RepacksModal } from "./modals"; import { GameOptionsModal, RepacksModal } from "./modals";
import { Downloader } from "@shared"; import { Downloader, getDownloadersForUri } from "@shared";
export function GameDetails() { export function GameDetails() {
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null); 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 ( return (
<GameDetailsContextProvider> <GameDetailsContextProvider>
<GameDetailsContextConsumer> <GameDetailsContextConsumer>
@ -96,6 +99,7 @@ export function GameDetails() {
downloader, downloader,
shop: shop as GameShop, shop: shop as GameShop,
downloadPath, downloadPath,
uri: selectRepackUri(repack, downloader),
}); });
await updateGame(); await updateGame();

View file

@ -20,13 +20,16 @@ export const hintText = style({
}); });
export const downloaders = style({ export const downloaders = style({
display: "flex", display: "grid",
gap: `${SPACING_UNIT}px`, gap: `${SPACING_UNIT}px`,
gridTemplateColumns: "repeat(2, 1fr)",
}); });
export const downloaderOption = style({ export const downloaderOption = style({
flex: "1",
position: "relative", position: "relative",
":only-child": {
gridColumn: "1 / -1",
},
}); });
export const downloaderIcon = style({ export const downloaderIcon = style({

View file

@ -5,7 +5,7 @@ import { DiskSpace } from "check-disk-space";
import * as styles from "./download-settings-modal.css"; import * as styles from "./download-settings-modal.css";
import { Button, Link, Modal, TextField } from "@renderer/components"; import { Button, Link, Modal, TextField } from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react"; 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 type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css"; import { SPACING_UNIT } from "@renderer/theme.css";
@ -48,8 +48,8 @@ export function DownloadSettingsModal({
}, [visible, selectedPath]); }, [visible, selectedPath]);
const downloaders = useMemo(() => { const downloaders = useMemo(() => {
return getDownloadersForUri(repack?.magnet ?? ""); return getDownloadersForUris(repack?.uris ?? []);
}, [repack?.magnet]); }, [repack?.uris]);
useEffect(() => { useEffect(() => {
if (userPreferences?.downloadsPath) { if (userPreferences?.downloadsPath) {

View file

@ -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 ( return (
<> <>
<DownloadSettingsModal <DownloadSettingsModal
@ -97,9 +104,7 @@ export function RepacksModal({
<div className={styles.repacks}> <div className={styles.repacks}>
{filteredRepacks.map((repack) => { {filteredRepacks.map((repack) => {
const isLastDownloadedOption = const isLastDownloadedOption = checkIfLastDownloadedOption(repack);
infoHash !== null &&
repack.magnet.toLowerCase().includes(infoHash);
return ( return (
<Button <Button

View file

@ -42,10 +42,3 @@ export const downloadSourcesHeader = style({
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
}); });
export const separator = style({
height: "100%",
width: "1px",
backgroundColor: vars.color.border,
margin: `${SPACING_UNIT}px 0`,
});

View file

@ -134,15 +134,6 @@ export function SettingsDownloadSources() {
downloadSource.downloadCount.toLocaleString(), downloadSource.downloadCount.toLocaleString(),
})} })}
</small> </small>
<div className={styles.separator} />
<small>
{t("download_options", {
count: downloadSource.repackCount,
countFormatted: downloadSource.repackCount.toLocaleString(),
})}
</small>
</div> </div>
</div> </div>

View file

@ -3,6 +3,7 @@ export enum Downloader {
Torrent, Torrent,
Gofile, Gofile,
PixelDrain, PixelDrain,
Qiwi,
} }
export enum DownloadSourceStatus { export enum DownloadSourceStatus {
@ -73,13 +74,27 @@ const realDebridHosts = ["https://1fichier.com", "https://mediafire.com"];
export const getDownloadersForUri = (uri: string) => { export const getDownloadersForUri = (uri: string) => {
if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile]; if (uri.startsWith("https://gofile.io")) return [Downloader.Gofile];
if (uri.startsWith("https://pixeldrain.com")) return [Downloader.PixelDrain]; 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))) if (realDebridHosts.some((host) => uri.startsWith(host)))
return [Downloader.RealDebrid]; return [Downloader.RealDebrid];
if (uri.startsWith("magnet:")) if (uri.startsWith("magnet:")) {
return [Downloader.Torrent, Downloader.RealDebrid]; return [Downloader.Torrent, Downloader.RealDebrid];
}
return []; 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);
};

View file

@ -67,7 +67,11 @@ export interface SteamAppDetails {
export interface GameRepack { export interface GameRepack {
id: number; id: number;
title: string; title: string;
/**
* @deprecated Use uris instead
*/
magnet: string; magnet: string;
uris: string[];
repacker: string; repacker: string;
fileSize: string | null; fileSize: string | null;
uploadDate: Date | string | null; uploadDate: Date | string | null;
@ -194,6 +198,7 @@ export interface StartGameDownloadPayload {
objectID: string; objectID: string;
title: string; title: string;
shop: GameShop; shop: GameShop;
uri: string;
downloadPath: string; downloadPath: string;
downloader: Downloader; downloader: Downloader;
} }