chore: adding download sorting

This commit is contained in:
Chubby Granny Chaser 2025-02-01 23:52:12 +00:00
parent 7fed104afd
commit 7e54a6d0a9
No known key found for this signature in database
15 changed files with 141 additions and 66 deletions

View File

@ -184,7 +184,11 @@
"reset_achievements_description": "This will reset all achievements for {{game}}", "reset_achievements_description": "This will reset all achievements for {{game}}",
"reset_achievements_title": "Are you sure?", "reset_achievements_title": "Are you sure?",
"reset_achievements_success": "Achievements successfully reset", "reset_achievements_success": "Achievements successfully reset",
"reset_achievements_error": "Failed to reset achievements" "reset_achievements_error": "Failed to reset achievements",
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available."
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View File

@ -173,7 +173,11 @@
"reset_achievements_title": "Tem certeza?", "reset_achievements_title": "Tem certeza?",
"reset_achievements_success": "Conquistas resetadas com sucesso", "reset_achievements_success": "Conquistas resetadas com sucesso",
"reset_achievements_error": "Falha ao resetar conquistas", "reset_achievements_error": "Falha ao resetar conquistas",
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais." "no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais.",
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique sua assinatura e tente novamente.",
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível."
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",

View File

@ -1,11 +1,12 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { Download, StartGameDownloadPayload } from "@types"; import type { Download, StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi } from "@main/services"; import { DownloadManager, HydraApi, logger } from "@main/services";
import { steamGamesWorker } from "@main/workers"; import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync"; import { createGame } from "@main/services/library-sync";
import { steamUrlBuilder } from "@shared"; import { Downloader, DownloadError, steamUrlBuilder } from "@shared";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { AxiosError } from "axios";
const startGameDownload = async ( const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -77,21 +78,49 @@ const startGameDownload = async (
await downloadsSublevel.put(gameKey, download); await downloadsSublevel.put(gameKey, download);
await DownloadManager.startDownload(download); try {
await DownloadManager.startDownload(download);
const updatedGame = await gamesSublevel.get(gameKey); const updatedGame = await gamesSublevel.get(gameKey);
await Promise.all([ await Promise.all([
createGame(updatedGame!).catch(() => {}), createGame(updatedGame!).catch(() => {}),
HydraApi.post( HydraApi.post(
"/games/download", "/games/download",
{ {
objectId, objectId,
shop, shop,
}, },
{ needsAuth: false } { needsAuth: false }
).catch(() => {}), ).catch(() => {}),
]); ]);
return { ok: true };
} catch (err: unknown) {
logger.error("Failed to start download", err);
if (err instanceof AxiosError) {
if (err.response?.status === 429 && downloader === Downloader.Gofile) {
return { ok: false, error: DownloadError.GofileQuotaExceeded };
}
if (
err.response?.status === 403 &&
downloader === Downloader.RealDebrid
) {
return {
ok: false,
error: DownloadError.RealDebridAccountNotAuthorized,
};
}
}
if (err instanceof Error) {
return { ok: false, error: err.message };
}
return { ok: false };
}
}; };
registerEvent("startGameDownload", startGameDownload); registerEvent("startGameDownload", startGameDownload);

View File

@ -1,4 +1,4 @@
import { Downloader } from "@shared"; import { Downloader, DownloadError } from "@shared";
import { WindowManager } from "../window-manager"; import { WindowManager } from "../window-manager";
import { publishDownloadCompleteNotification } from "../notifications"; import { publishDownloadCompleteNotification } from "../notifications";
import type { Download, DownloadProgress, UserPreferences } from "@types"; import type { Download, DownloadProgress, UserPreferences } from "@types";
@ -263,6 +263,8 @@ export class DownloadManager {
const token = await GofileApi.authorize(); const token = await GofileApi.authorize();
const downloadLink = await GofileApi.getDownloadLink(id!); const downloadLink = await GofileApi.getDownloadLink(id!);
await GofileApi.checkDownloadUrl(downloadLink);
return { return {
action: "start", action: "start",
game_id: downloadId, game_id: downloadId,
@ -318,10 +320,7 @@ export class DownloadManager {
case Downloader.RealDebrid: { case Downloader.RealDebrid: {
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri); const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
if (!downloadUrl) if (!downloadUrl) throw new Error(DownloadError.NotCachedInRealDebrid);
throw new Error(
"This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available."
);
return { return {
action: "start", action: "start",

View File

@ -60,4 +60,12 @@ export class GofileApi {
throw new Error("Failed to get download link"); 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

@ -282,6 +282,7 @@ export function App() {
message={toast.message} message={toast.message}
type={toast.type} type={toast.type}
onClose={handleToastClose} onClose={handleToastClose}
duration={toast.duration}
/> />
</div> </div>

View File

@ -40,7 +40,9 @@ declare global {
interface Electron { interface Electron {
/* Torrenting */ /* Torrenting */
startGameDownload: (payload: StartGameDownloadPayload) => Promise<void>; startGameDownload: (
payload: StartGameDownloadPayload
) => Promise<{ ok: boolean; error?: string }>;
cancelGameDownload: (shop: GameShop, objectId: string) => Promise<void>; cancelGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
pauseGameDownload: (shop: GameShop, objectId: string) => Promise<void>; pauseGameDownload: (shop: GameShop, objectId: string) => Promise<void>;
resumeGameDownload: (shop: GameShop, objectId: string) => Promise<void>; resumeGameDownload: (shop: GameShop, objectId: string) => Promise<void>;

View File

@ -6,6 +6,7 @@ export interface ToastState {
title: string; title: string;
message?: string; message?: string;
type: ToastProps["type"]; type: ToastProps["type"];
duration?: number;
visible: boolean; visible: boolean;
} }
@ -13,6 +14,7 @@ const initialState: ToastState = {
title: "", title: "",
message: "", message: "",
type: "success", type: "success",
duration: 5000,
visible: false, visible: false,
}; };
@ -24,6 +26,7 @@ export const toastSlice = createSlice({
state.title = action.payload.title; state.title = action.payload.title;
state.message = action.payload.message; state.message = action.payload.message;
state.type = action.payload.type; state.type = action.payload.type;
state.duration = action.payload.duration ?? 5000;
state.visible = true; state.visible = true;
}, },
closeToast: (state) => { closeToast: (state) => {

View File

@ -29,10 +29,11 @@ export function useDownload() {
const startDownload = async (payload: StartGameDownloadPayload) => { const startDownload = async (payload: StartGameDownloadPayload) => {
dispatch(clearDownload()); dispatch(clearDownload());
const game = await window.electron.startGameDownload(payload); const response = await window.electron.startGameDownload(payload);
await updateLibrary(); if (response.ok) updateLibrary();
return game;
return response;
}; };
const pauseDownload = async (shop: GameShop, objectId: string) => { const pauseDownload = async (shop: GameShop, objectId: string) => {

View File

@ -6,12 +6,13 @@ export function useToast() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const showSuccessToast = useCallback( const showSuccessToast = useCallback(
(title: string, message?: string) => { (title: string, message?: string, duration?: number) => {
dispatch( dispatch(
showToast({ showToast({
title, title,
message, message,
type: "success", type: "success",
duration,
}) })
); );
}, },
@ -19,12 +20,13 @@ export function useToast() {
); );
const showErrorToast = useCallback( const showErrorToast = useCallback(
(title: string, message?: string) => { (title: string, message?: string, duration?: number) => {
dispatch( dispatch(
showToast({ showToast({
title, title,
message, message,
type: "error", type: "error",
duration,
}) })
); );
}, },
@ -32,12 +34,13 @@ export function useToast() {
); );
const showWarningToast = useCallback( const showWarningToast = useCallback(
(title: string, message?: string) => { (title: string, message?: string, duration?: number) => {
dispatch( dispatch(
showToast({ showToast({
title, title,
message, message,
type: "warning", type: "warning",
duration,
}) })
); );
}, },

View File

@ -8,7 +8,7 @@ import * as styles from "./downloads.css";
import { DeleteGameModal } from "./delete-game-modal"; import { DeleteGameModal } from "./delete-game-modal";
import { DownloadGroup } from "./download-group"; import { DownloadGroup } from "./download-group";
import type { GameShop, LibraryGame, SeedingStatus } from "@types"; import type { GameShop, LibraryGame, SeedingStatus } from "@types";
import { orderBy } from "lodash-es"; import { orderBy, sortBy } from "lodash-es";
import { ArrowDownIcon } from "@primer/octicons-react"; import { ArrowDownIcon } from "@primer/octicons-react";
export default function Downloads() { export default function Downloads() {
@ -58,21 +58,24 @@ export default function Downloads() {
complete: [], complete: [],
}; };
const result = library.reduce((prev, next) => { const result = sortBy(library, (game) => game.download?.timestamp).reduce(
/* Game has been manually added to the library or has been canceled */ (prev, next) => {
if (!next.download?.status || next.download?.status === "removed") /* Game has been manually added to the library or has been canceled */
return prev; if (!next.download?.status || next.download?.status === "removed")
return prev;
/* Is downloading */ /* Is downloading */
if (lastPacket?.gameId === next.id) if (lastPacket?.gameId === next.id)
return { ...prev, downloading: [...prev.downloading, next] }; return { ...prev, downloading: [...prev.downloading, next] };
/* Is either queued or paused */ /* Is either queued or paused */
if (next.download.queued || next.download?.status === "paused") if (next.download.queued || next.download?.status === "paused")
return { ...prev, queued: [...prev.queued, next] }; return { ...prev, queued: [...prev.queued, next] };
return { ...prev, complete: [...prev.complete, next] }; return { ...prev, complete: [...prev.complete, next] };
}, initialValue); },
initialValue
);
const queued = orderBy(result.queued, (game) => game.download?.timestamp, [ const queued = orderBy(result.queued, (game) => game.download?.timestamp, [
"desc", "desc",

View File

@ -102,19 +102,23 @@ export default function GameDetails() {
downloader: Downloader, downloader: Downloader,
downloadPath: string downloadPath: string
) => { ) => {
await startDownload({ const response = await startDownload({
repackId: repack.id, repackId: repack.id,
objectId: objectId!, objectId: objectId!,
title: gameTitle, title: gameTitle,
downloader, downloader,
shop: shop as GameShop, shop,
downloadPath, downloadPath,
uri: selectRepackUri(repack, downloader), uri: selectRepackUri(repack, downloader),
}); });
await updateGame(); if (response.ok) {
setShowRepacksModal(false); await updateGame();
setShowGameOptionsModal(false); setShowRepacksModal(false);
setShowGameOptionsModal(false);
}
return response;
}; };
const handleNSFWContentRefuse = () => { const handleNSFWContentRefuse = () => {
@ -123,10 +127,7 @@ export default function GameDetails() {
}; };
return ( return (
<CloudSyncContextProvider <CloudSyncContextProvider objectId={objectId!} shop={shop}>
objectId={objectId!}
shop={shop! as GameShop}
>
<CloudSyncContextConsumer> <CloudSyncContextConsumer>
{({ {({
showCloudSyncModal, showCloudSyncModal,

View File

@ -18,7 +18,7 @@ export interface DownloadSettingsModalProps {
repack: GameRepack, repack: GameRepack,
downloader: Downloader, downloader: Downloader,
downloadPath: string downloadPath: string
) => Promise<void>; ) => Promise<{ ok: boolean; error?: string }>;
repack: GameRepack | null; repack: GameRepack | null;
} }
@ -27,7 +27,7 @@ export function DownloadSettingsModal({
onClose, onClose,
startDownload, startDownload,
repack, repack,
}: DownloadSettingsModalProps) { }: Readonly<DownloadSettingsModalProps>) {
const { t } = useTranslation("game_details"); const { t } = useTranslation("game_details");
const { showErrorToast } = useToast(); const { showErrorToast } = useToast();
@ -117,20 +117,30 @@ export function DownloadSettingsModal({
} }
}; };
const handleStartClick = () => { const handleStartClick = async () => {
if (repack) { if (repack) {
setDownloadStarting(true); setDownloadStarting(true);
startDownload(repack, selectedDownloader!, selectedPath) try {
.then(() => { const response = await startDownload(
repack,
selectedDownloader!,
selectedPath
);
if (response.ok) {
onClose(); onClose();
}) return;
.catch((error) => { } else if (response.error) {
showErrorToast(t("download_error"), error.message); showErrorToast(t("download_error"), t(response.error), 4_000);
}) }
.finally(() => { } catch (error) {
setDownloadStarting(false); if (error instanceof Error) {
}); showErrorToast(t("download_error"), error.message, 4_000);
}
} finally {
setDownloadStarting(false);
}
} }
}; };

View File

@ -19,7 +19,7 @@ export interface RepacksModalProps {
repack: GameRepack, repack: GameRepack,
downloader: Downloader, downloader: Downloader,
downloadPath: string downloadPath: string
) => Promise<void>; ) => Promise<{ ok: boolean; error?: string }>;
onClose: () => void; onClose: () => void;
} }
@ -27,7 +27,7 @@ export function RepacksModal({
visible, visible,
startDownload, startDownload,
onClose, onClose,
}: RepacksModalProps) { }: Readonly<RepacksModalProps>) {
const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]); const [filteredRepacks, setFilteredRepacks] = useState<GameRepack[]>([]);
const [repack, setRepack] = useState<GameRepack | null>(null); const [repack, setRepack] = useState<GameRepack | null>(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false); const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
@ -111,7 +111,7 @@ export function RepacksModal({
<p style={{ fontSize: "12px" }}> <p style={{ fontSize: "12px" }}>
{repack.fileSize} - {repack.repacker} -{" "} {repack.fileSize} - {repack.repacker} -{" "}
{repack.uploadDate ? formatDate(repack.uploadDate!) : ""} {repack.uploadDate ? formatDate(repack.uploadDate) : ""}
</p> </p>
</Button> </Button>
); );

View File

@ -49,3 +49,10 @@ export enum AuthPage {
UpdateEmail = "/update-email", UpdateEmail = "/update-email",
UpdatePassword = "/update-password", UpdatePassword = "/update-password",
} }
export enum DownloadError {
NotCachedInRealDebrid = "download_error_not_cached_in_real_debrid",
NotCachedInTorbox = "download_error_not_cached_in_torbox",
GofileQuotaExceeded = "download_error_gofile_quota_exceeded",
RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized",
}