Merge branch 'main' into main

This commit is contained in:
Zamitto 2024-08-15 17:31:51 -03:00 committed by GitHub
commit baf4eafa68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 561 additions and 407 deletions

View file

@ -1,80 +0,0 @@
declare module "aria2" {
export type Aria2Status =
| "active"
| "waiting"
| "paused"
| "error"
| "complete"
| "removed";
export interface StatusResponse {
gid: string;
status: Aria2Status;
totalLength: string;
completedLength: string;
uploadLength: string;
bitfield: string;
downloadSpeed: string;
uploadSpeed: string;
infoHash?: string;
numSeeders?: string;
seeder?: boolean;
pieceLength: string;
numPieces: string;
connections: string;
errorCode?: string;
errorMessage?: string;
followedBy?: string[];
following: string;
belongsTo: string;
dir: string;
files: {
path: string;
length: string;
completedLength: string;
selected: string;
}[];
bittorrent?: {
announceList: string[][];
comment: string;
creationDate: string;
mode: "single" | "multi";
info: {
name: string;
verifiedLength: string;
verifyIntegrityPending: string;
};
};
}
export default class Aria2 {
constructor(options: any);
open: () => Promise<void>;
call(
method: "addUri",
uris: string[],
options: { dir: string }
): Promise<string>;
call(
method: "tellStatus",
gid: string,
keys?: string[]
): Promise<StatusResponse>;
call(method: "pause", gid: string): Promise<string>;
call(method: "forcePause", gid: string): Promise<string>;
call(method: "unpause", gid: string): Promise<string>;
call(method: "remove", gid: string): Promise<string>;
call(method: "forceRemove", gid: string): Promise<string>;
call(method: "pauseAll"): Promise<string>;
call(method: "forcePauseAll"): Promise<string>;
listNotifications: () => [
"onDownloadStart",
"onDownloadPause",
"onDownloadStop",
"onDownloadComplete",
"onDownloadError",
"onBtDownloadComplete",
];
on: (event: string, callback: (params: any) => void) => void;
}
}

View file

@ -26,6 +26,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
/* Disconnects libtorrent */
PythonInstance.killTorrent();
HydraApi.handleSignOut();
await Promise.all([
databaseOperations,
HydraApi.post("/auth/logout").catch(() => {}),

View file

@ -22,8 +22,9 @@ const loadState = async (userPreferences: UserPreferences | null) => {
import("./events");
if (userPreferences?.realDebridApiToken)
if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
}
HydraApi.setupApi().then(() => {
uploadGamesBatch();

View file

@ -1,20 +0,0 @@
import path from "node:path";
import { spawn } from "node:child_process";
import { app } from "electron";
export const startAria2 = () => {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
return spawn(
binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
],
{ stdio: "inherit", windowsHide: true }
);
};

View file

@ -6,6 +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";
export class DownloadManager {
private static currentDownloader: Downloader | null = null;
@ -13,10 +15,12 @@ export class DownloadManager {
public static async watchDownloads() {
let status: DownloadProgress | null = null;
if (this.currentDownloader === Downloader.RealDebrid) {
if (this.currentDownloader === Downloader.Torrent) {
status = await PythonInstance.getStatus();
} else if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus();
} else {
status = await PythonInstance.getStatus();
status = await GenericHTTPDownloader.getStatus();
}
if (status) {
@ -62,10 +66,12 @@ export class DownloadManager {
}
static async pauseDownload() {
if (this.currentDownloader === Downloader.RealDebrid) {
if (this.currentDownloader === Downloader.Torrent) {
await PythonInstance.pauseDownload();
} else if (this.currentDownloader === Downloader.RealDebrid) {
await RealDebridDownloader.pauseDownload();
} else {
await PythonInstance.pauseDownload();
await GenericHTTPDownloader.pauseDownload();
}
WindowManager.mainWindow?.setProgressBar(-1);
@ -73,20 +79,16 @@ export class DownloadManager {
}
static async resumeDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) {
RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid;
} else {
PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent;
}
return this.startDownload(game);
}
static async cancelDownload(gameId: number) {
if (this.currentDownloader === Downloader.RealDebrid) {
if (this.currentDownloader === Downloader.Torrent) {
PythonInstance.cancelDownload(gameId);
} else if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload(gameId);
} else {
PythonInstance.cancelDownload(gameId);
GenericHTTPDownloader.cancelDownload(gameId);
}
WindowManager.mainWindow?.setProgressBar(-1);
@ -94,12 +96,28 @@ export class DownloadManager {
}
static async startDownload(game: Game) {
if (game.downloader === Downloader.RealDebrid) {
RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid;
} else {
if (game.downloader === Downloader.Gofile) {
const id = game!.uri!.split("/").pop();
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();
await GenericHTTPDownloader.startDownload(
game,
`https://pixeldrain.com/api/file/${id}?download`
);
} else if (game.downloader === Downloader.Torrent) {
PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent;
} else if (game.downloader === Downloader.RealDebrid) {
RealDebridDownloader.startDownload(game);
}
this.currentDownloader = game.downloader;
}
}

View file

@ -0,0 +1,107 @@
import { Game } from "@main/entity";
import { gameRepository } from "@main/repository";
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;
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: status.downloadSpeed,
timeRemaining: calculateETA(
status.totalLength,
status.completedLength,
status.downloadSpeed
),
isDownloadingMetadata: false,
isCheckingFiles: false,
progress,
gameId: this.downloadingGame!.id,
} as DownloadProgress;
if (progress === 1) {
this.downloads.delete(this.downloadingGame.id);
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.downloadingGame = null;
}
}
static async startDownload(
game: Game,
downloadUrl: string,
headers?: Record<string, string>
) {
this.downloadingGame = game;
if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!);
return;
}
const gid = await HttpDownload.startDownload(
game.downloadPath!,
downloadUrl,
headers
);
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);
}
}
static async resumeDownload(gameId: number) {
const gid = this.downloads.get(gameId);
if (gid) {
await HttpDownload.resumeDownload(gid);
}
}
}

View file

@ -1,68 +1,69 @@
import type { ChildProcess } from "node:child_process";
import { logger } from "../logger";
import { sleep } from "@main/helpers";
import { startAria2 } from "../aria2c";
import Aria2 from "aria2";
import { DownloadItem } from "electron";
import { WindowManager } from "../window-manager";
import path from "node:path";
export class HttpDownload {
private static connected = false;
private static aria2c: ChildProcess | null = null;
private static id = 0;
private static aria2 = new Aria2({});
private static downloads: Record<string, DownloadItem> = {};
private static async connect() {
this.aria2c = startAria2();
let retries = 0;
while (retries < 4 && !this.connected) {
try {
await this.aria2.open();
logger.log("Connected to aria2");
this.connected = true;
} catch (err) {
await sleep(100);
logger.log("Failed to connect to aria2, retrying...");
retries++;
}
}
}
public static getStatus(gid: string) {
if (this.connected) {
return this.aria2.call("tellStatus", gid);
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 static disconnect() {
if (this.aria2c) {
this.aria2c.kill();
this.connected = false;
}
}
static async cancelDownload(gid: string) {
await this.aria2.call("forceRemove", gid);
const downloadItem = this.downloads[gid];
downloadItem?.cancel();
delete this.downloads[gid];
}
static async pauseDownload(gid: string) {
await this.aria2.call("forcePause", gid);
const downloadItem = this.downloads[gid];
downloadItem?.pause();
}
static async resumeDownload(gid: string) {
await this.aria2.call("unpause", gid);
const downloadItem = this.downloads[gid];
downloadItem?.resume();
}
static async startDownload(downloadPath: string, downloadUrl: string) {
if (!this.connected) await this.connect();
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);
const options = {
dir: downloadPath,
};
const gid = ++this.id;
return this.aria2.call("addUri", [downloadUrl], options);
WindowManager.mainWindow?.webContents.session.on(
"will-download",
(_event, item, _webContents) => {
this.downloads[gid.toString()] = item;
// Set the save path, making Electron not to prompt a save dialog.
item.setSavePath(path.join(downloadPath, item.getFilename()));
resolve(gid.toString());
}
);
});
}
}

View file

@ -13,22 +13,36 @@ export class RealDebridDownloader {
private static async getRealDebridDownloadUrl() {
if (this.realDebridTorrentId) {
const torrentInfo = await RealDebridClient.getTorrentInfo(
let torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
const { status, links } = torrentInfo;
if (status === "waiting_files_selection") {
if (torrentInfo.status === "waiting_files_selection") {
await RealDebridClient.selectAllFiles(this.realDebridTorrentId);
return null;
torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
}
const { links, status } = torrentInfo;
if (status === "downloaded") {
const [link] = links;
const { download } = await RealDebridClient.unrestrictLink(link);
return decodeURIComponent(download);
}
return null;
}
if (this.downloadingGame?.uri) {
const { download } = await RealDebridClient.unrestrictLink(
this.downloadingGame?.uri
);
return decodeURIComponent(download);
}
return null;
@ -37,7 +51,7 @@ export class RealDebridDownloader {
public static async getStatus() {
if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame.id)!;
const status = await HttpDownload.getStatus(gid);
const status = HttpDownload.getStatus(gid);
if (status) {
const progress =
@ -50,6 +64,7 @@ export class RealDebridDownloader {
fileSize: Number(status.totalLength),
progress,
status: "active",
folderName: status.folderName,
}
);
@ -78,40 +93,15 @@ export class RealDebridDownloader {
}
}
if (this.realDebridTorrentId && this.downloadingGame) {
const torrentInfo = await RealDebridClient.getTorrentInfo(
this.realDebridTorrentId
);
const { status } = torrentInfo;
if (status === "downloaded") {
this.startDownload(this.downloadingGame);
}
const progress = torrentInfo.progress / 100;
const totalDownloaded = progress * torrentInfo.bytes;
return {
numPeers: 0,
numSeeds: torrentInfo.seeders,
downloadSpeed: torrentInfo.speed,
timeRemaining: calculateETA(
torrentInfo.bytes,
totalDownloaded,
torrentInfo.speed
),
isDownloadingMetadata: status === "magnet_conversion",
} as DownloadProgress;
}
return null;
}
static async pauseDownload() {
const gid = this.downloads.get(this.downloadingGame!.id!);
if (gid) {
await HttpDownload.pauseDownload(gid);
if (this.downloadingGame) {
const gid = this.downloads.get(this.downloadingGame.id);
if (gid) {
await HttpDownload.pauseDownload(gid);
}
}
this.realDebridTorrentId = null;
@ -119,15 +109,19 @@ export class RealDebridDownloader {
}
static async startDownload(game: Game) {
this.downloadingGame = game;
if (this.downloads.has(game.id)) {
await this.resumeDownload(game.id!);
this.downloadingGame = game;
return;
}
this.realDebridTorrentId = await RealDebridClient.getTorrentId(game!.uri!);
if (game.uri?.startsWith("magnet:")) {
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
game!.uri!
);
}
this.downloadingGame = game;
const downloadUrl = await this.getRealDebridDownloadUrl();
@ -150,6 +144,9 @@ export class RealDebridDownloader {
await HttpDownload.cancelDownload(gid);
this.downloads.delete(gameId);
}
this.realDebridTorrentId = null;
this.downloadingGame = null;
}
static async resumeDownload(gameId: number) {

View file

@ -0,0 +1,61 @@
import axios from "axios";
export interface GofileAccountsReponse {
id: string;
token: string;
}
export interface GofileContentChild {
id: string;
link: string;
}
export interface GofileContentsResponse {
id: string;
type: string;
children: Record<string, GofileContentChild>;
}
export class GofileApi {
private static token: string;
public static async authorize() {
const response = await axios.post<{
status: string;
data: GofileAccountsReponse;
}>("https://api.gofile.io/accounts");
if (response.data.status === "ok") {
this.token = response.data.data.token;
return this.token;
}
throw new Error("Failed to authorize");
}
public static async getDownloadLink(id: string) {
const searchParams = new URLSearchParams({
wt: "4fd6sg89d7s6",
});
const response = await axios.get<{
status: string;
data: GofileContentsResponse;
}>(`https://api.gofile.io/contents/${id}?${searchParams.toString()}`, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});
if (response.data.status === "ok") {
if (response.data.data.type !== "folder") {
throw new Error("Only folders are supported");
}
const [firstChild] = Object.values(response.data.data.children);
return firstChild.link;
}
throw new Error("Failed to get download link");
}
}

View file

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

View file

@ -64,6 +64,14 @@ export class HydraApi {
}
}
static handleSignOut() {
this.userAuth = {
authToken: "",
refreshToken: "",
expirationTimestamp: 0,
};
}
static async setupApi() {
this.instance = axios.create({
baseURL: import.meta.env.MAIN_VITE_API_URL,

View file

@ -46,7 +46,7 @@ export class RealDebridClient {
static async selectAllFiles(id: string) {
const searchParams = new URLSearchParams({ files: "all" });
await this.instance.post(
return this.instance.post(
`/torrents/selectFiles/${id}`,
searchParams.toString()
);

View file

@ -26,7 +26,7 @@ globalStyle("html, body, #root, main", {
globalStyle("body", {
overflow: "hidden",
userSelect: "none",
fontFamily: "'Fira Mono', monospace",
fontFamily: "Noto Sans, sans-serif",
fontSize: vars.size.body,
background: vars.color.background,
color: vars.color.body,

View file

@ -45,7 +45,6 @@ export const description = style({
maxWidth: "700px",
color: vars.color.muted,
textAlign: "left",
fontFamily: "'Fira Sans', sans-serif",
lineHeight: "20px",
marginTop: `${SPACING_UNIT * 2}px`,
});

View file

@ -24,6 +24,7 @@ export const modal = recipe({
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
backgroundColor: vars.color.background,
borderRadius: "4px",
minWidth: "400px",
maxWidth: "600px",
color: vars.color.body,
maxHeight: "100%",

View file

@ -5,4 +5,6 @@ export const VERSION_CODENAME = "Leviticus";
export const DOWNLOADER_NAME = {
[Downloader.RealDebrid]: "Real-Debrid",
[Downloader.Torrent]: "Torrent",
[Downloader.Gofile]: "Gofile",
[Downloader.PixelDrain]: "PixelDrain",
};

View file

@ -8,12 +8,10 @@ import { HashRouter, Route, Routes } from "react-router-dom";
import * as Sentry from "@sentry/electron/renderer";
import "@fontsource/fira-mono/400.css";
import "@fontsource/fira-mono/500.css";
import "@fontsource/fira-mono/700.css";
import "@fontsource/fira-sans/400.css";
import "@fontsource/fira-sans/500.css";
import "@fontsource/fira-sans/700.css";
import "@fontsource/noto-sans/400.css";
import "@fontsource/noto-sans/500.css";
import "@fontsource/noto-sans/700.css";
import "react-loading-skeleton/dist/skeleton.css";
import { App } from "./app";

View file

@ -132,9 +132,7 @@ export function Downloads() {
<ArrowDownIcon size={24} />
</div>
<h2>{t("no_downloads_title")}</h2>
<p style={{ fontFamily: "Fira Sans" }}>
{t("no_downloads_description")}
</p>
<p>{t("no_downloads_description")}</p>
</div>
)}
</>

View file

@ -19,7 +19,10 @@ export function DescriptionHeader() {
date: shopDetails?.release_date.date,
})}
</p>
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
{Array.isArray(shopDetails.publishers) && (
<p>{t("publisher", { publisher: shopDetails.publishers[0] })}</p>
)}
</section>
</div>
);

View file

@ -101,7 +101,6 @@ export const descriptionContent = style({
export const description = style({
userSelect: "text",
lineHeight: "22px",
fontFamily: "'Fira Sans', sans-serif",
fontSize: "16px",
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
"@media": {

View file

@ -9,6 +9,7 @@ export const panel = recipe({
height: "72px",
minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
backgroundColor: vars.color.darkBackground,
display: "flex",
alignItems: "center",
justifyContent: "space-between",

View file

@ -1,11 +1,11 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
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 } from "@shared";
import { Downloader, formatBytes, getDownloadersForUri } from "@shared";
import type { GameRepack } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
@ -23,8 +23,6 @@ export interface DownloadSettingsModalProps {
repack: GameRepack | null;
}
const downloaders = [Downloader.Torrent, Downloader.RealDebrid];
export function DownloadSettingsModal({
visible,
onClose,
@ -36,9 +34,8 @@ export function DownloadSettingsModal({
const [diskFreeSpace, setDiskFreeSpace] = useState<DiskSpace | null>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
const [selectedDownloader, setSelectedDownloader] = useState(
Downloader.Torrent
);
const [selectedDownloader, setSelectedDownloader] =
useState<Downloader | null>(null);
const userPreferences = useAppSelector(
(state) => state.userPreferences.value
@ -50,6 +47,10 @@ export function DownloadSettingsModal({
}
}, [visible, selectedPath]);
const downloaders = useMemo(() => {
return getDownloadersForUri(repack?.magnet ?? "");
}, [repack?.magnet]);
useEffect(() => {
if (userPreferences?.downloadsPath) {
setSelectedPath(userPreferences.downloadsPath);
@ -59,9 +60,27 @@ export function DownloadSettingsModal({
.then((defaultDownloadsPath) => setSelectedPath(defaultDownloadsPath));
}
if (userPreferences?.realDebridApiToken)
setSelectedDownloader(Downloader.RealDebrid);
}, [userPreferences?.downloadsPath, userPreferences?.realDebridApiToken]);
const filteredDownloaders = downloaders.filter((downloader) => {
if (downloader === Downloader.RealDebrid)
return userPreferences?.realDebridApiToken;
return true;
});
/* Gives preference to Real Debrid */
const selectedDownloader = filteredDownloaders.includes(
Downloader.RealDebrid
)
? Downloader.RealDebrid
: filteredDownloaders[0];
setSelectedDownloader(
selectedDownloader === undefined ? null : selectedDownloader
);
}, [
userPreferences?.downloadsPath,
downloaders,
userPreferences?.realDebridApiToken,
]);
const getDiskFreeSpace = (path: string) => {
window.electron.getDiskFreeSpace(path).then((result) => {
@ -85,7 +104,7 @@ export function DownloadSettingsModal({
if (repack) {
setDownloadStarting(true);
startDownload(repack, selectedDownloader, selectedPath).finally(() => {
startDownload(repack, selectedDownloader!, selectedPath).finally(() => {
setDownloadStarting(false);
onClose();
});
@ -167,7 +186,10 @@ export function DownloadSettingsModal({
</p>
</div>
<Button onClick={handleStartClick} disabled={downloadStarting}>
<Button
onClick={handleStartClick}
disabled={downloadStarting || selectedDownloader === null}
>
<DownloadIcon />
{t("download_now")}
</Button>

View file

@ -15,7 +15,6 @@ export const gameOptionHeader = style({
});
export const gameOptionHeaderDescription = style({
fontFamily: "'Fira Sans', sans-serif",
fontWeight: "400",
});

View file

@ -44,8 +44,10 @@ export function RepacksModal({
}, [repacks]);
const getInfoHash = useCallback(async () => {
const torrent = await parseTorrent(game?.uri ?? "");
if (torrent.infoHash) setInfoHash(torrent.infoHash);
if (game?.uri?.startsWith("magnet:")) {
const torrent = await parseTorrent(game?.uri ?? "");
if (torrent.infoHash) setInfoHash(torrent.infoHash);
}
}, [game]);
useEffect(() => {

View file

@ -46,7 +46,6 @@ export const requirementButton = style({
export const requirementsDetails = style({
padding: `${SPACING_UNIT * 2}px`,
lineHeight: "22px",
fontFamily: "'Fira Sans', sans-serif",
fontSize: "16px",
});

View file

@ -82,9 +82,7 @@ export function SettingsDownloadSources() {
onAddDownloadSource={handleAddDownloadSource}
/>
<p style={{ fontFamily: '"Fira Sans"' }}>
{t("download_sources_description")}
</p>
<p>{t("download_sources_description")}</p>
<div className={styles.downloadSourcesHeader}>
<Button

View file

@ -9,6 +9,5 @@ export const form = style({
});
export const description = style({
fontFamily: "'Fira Sans', sans-serif",
marginBottom: `${SPACING_UNIT * 2}px`,
});

View file

@ -25,9 +25,7 @@ export const UserBlockModal = ({
onClose={onClose}
>
<div className={styles.signOutModalContent}>
<p style={{ fontFamily: "Fira Sans" }}>
{t("user_block_modal_text", { displayName })}
</p>
<p>{t("user_block_modal_text", { displayName })}</p>
<div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger">
{t("block_user")}

View file

@ -371,11 +371,7 @@ export function UserContent({
<TelescopeIcon size={24} />
</div>
<h2>{t("no_recent_activity_title")}</h2>
{isMe && (
<p style={{ fontFamily: "Fira Sans" }}>
{t("no_recent_activity_description")}
</p>
)}
{isMe && <p>{t("no_recent_activity_description")}</p>}
</div>
) : (
<div

View file

@ -23,7 +23,7 @@ export const UserSignOutModal = ({
onClose={onClose}
>
<div className={styles.signOutModalContent}>
<p style={{ fontFamily: "Fira Sans" }}>{t("sign_out_modal_text")}</p>
<p>{t("sign_out_modal_text")}</p>
<div className={styles.signOutModalButtonsContainer}>
<Button onClick={onConfirm} theme="danger">
{t("sign_out")}

View file

@ -1,6 +1,8 @@
export enum Downloader {
RealDebrid,
Torrent,
Gofile,
PixelDrain,
}
export enum DownloadSourceStatus {
@ -51,6 +53,8 @@ export const removeSpecialEditionFromName = (name: string) =>
export const removeDuplicateSpaces = (name: string) =>
name.replace(/\s{2,}/g, " ");
export const replaceDotsWithSpace = (name: string) => name.replace(/\./g, " ");
export const replaceUnderscoreWithSpace = (name: string) =>
name.replace(/_/g, " ");
@ -58,8 +62,24 @@ export const formatName = pipe<string>(
removeReleaseYearFromName,
removeSpecialEditionFromName,
replaceUnderscoreWithSpace,
replaceDotsWithSpace,
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
removeSymbolsFromName,
removeDuplicateSpaces,
(str) => str.trim()
);
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 (realDebridHosts.some((host) => uri.startsWith(host)))
return [Downloader.RealDebrid];
if (uri.startsWith("magnet:"))
return [Downloader.Torrent, Downloader.RealDebrid];
return [];
};