From da806b21e949c3d86445ae2a38dba5f598295f33 Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Fri, 19 Dec 2025 10:30:45 +0530 Subject: [PATCH] refactor: added errored state and improved error detection --- src/App.tsx | 63 ++++++++++------ .../pages/library/incompleteDownloads.tsx | 59 +++++++++++++-- src/helpers/use-downloader.ts | 75 +++++++++++-------- src/providers/tanstackProvider.tsx | 25 +++++-- src/services/store.ts | 25 +++++-- src/types/store.ts | 13 ++-- 6 files changed, 177 insertions(+), 83 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 59844da..789f754 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,14 +52,12 @@ export default function App({ children }: { children: React.ReactNode }) { color_scheme: APP_COLOR_SCHEME, } = useSettingsPageStatesStore(state => state.settings); - const isErrored = useDownloaderPageStatesStore((state) => state.isErrored); - const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected); - const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId); - const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored); - const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected); - const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId); + const erroredDownloadIds = useDownloaderPageStatesStore((state) => state.erroredDownloadIds); + const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds); + const removeErroredDownload = useDownloaderPageStatesStore((state) => state.removeErroredDownload); + const removeExpectedErrorDownload = useDownloaderPageStatesStore((state) => state.removeExpectedErrorDownload); - const appWindow = getCurrentWebviewWindow() + const appWindow = getCurrentWebviewWindow(); const navigate = useNavigate(); const LOG = useLogger(); const currentPlatform = platform(); @@ -79,6 +77,7 @@ export default function App({ children }: { children: React.ReactNode }) { const hasRunYtDlpAutoUpdateRef = useRef(false); const hasRunAppUpdateCheckRef = useRef(false); const isRegisteredToMacOsRef = useRef(false); + const pendingErrorUpdatesRef = useRef>(new Set()); const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader(); @@ -328,38 +327,56 @@ export default function App({ children }: { children: React.ReactNode }) { // show a toast and pause the download when yt-dlp exits unexpectedly useEffect(() => { - if (isErrored && !isErrorExpected) { + const unexpectedErrors = Array.from(erroredDownloadIds).filter(id => !expectedErrorDownloadIds.has(id)); + const processedUnexpectedErrors = unexpectedErrors.filter(id => !pendingErrorUpdatesRef.current.has(id)); + if (unexpectedErrors.length === 0) return; + + processedUnexpectedErrors.forEach((downloadId) => { + const downloadState = globalDownloadStates.find(d => d.download_id === downloadId); toast.error("Download Failed", { - description: "yt-dlp exited unexpectedly. Please try again later", + description: `The download for "${downloadState?.title}" failed because yt-dlp exited unexpectedly. Please try again later.`, }); - if (erroredDownloadId) { - downloadStatusUpdater.mutate({ download_id: erroredDownloadId, download_status: 'paused' }, { + }); + + const timeoutIds: NodeJS.Timeout[] = []; + unexpectedErrors.forEach((downloadId) => { + pendingErrorUpdatesRef.current.add(downloadId); + + const timeoutId = setTimeout(() => { + downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'errored' }, { onSuccess: (data) => { console.log("Download status updated successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); + removeErroredDownload(downloadId); + pendingErrorUpdatesRef.current.delete(downloadId); }, onError: (error) => { console.error("Failed to update download status:", error); + removeErroredDownload(downloadId); + pendingErrorUpdatesRef.current.delete(downloadId); } - }) - setErroredDownloadId(null); - } - setIsErrored(false); - setIsErrorExpected(false); - } - }, [isErrored, isErrorExpected, erroredDownloadId, setIsErrored, setIsErrorExpected, setErroredDownloadId]); + }); + }, 500); + timeoutIds.push(timeoutId); + }); + + return () => { + timeoutIds.forEach(id => clearTimeout(id)); + }; + }, [erroredDownloadIds, expectedErrorDownloadIds]); // auto reset error states after 3 seconds of expecting an error useEffect(() => { - if (isErrorExpected) { + if (expectedErrorDownloadIds.size > 0) { const timeoutId = setTimeout(() => { - setIsErrored(false); - setIsErrorExpected(false); - setErroredDownloadId(null); + expectedErrorDownloadIds.forEach((downloadId) => { + removeErroredDownload(downloadId); + removeExpectedErrorDownload(downloadId); + }); }, 3000); return () => clearTimeout(timeoutId); } - }, [isErrorExpected, setIsErrorExpected]); + }, [expectedErrorDownloadIds]); return ( diff --git a/src/components/pages/library/incompleteDownloads.tsx b/src/components/pages/library/incompleteDownloads.tsx index c0b0a83..74d8d66 100644 --- a/src/components/pages/library/incompleteDownloads.tsx +++ b/src/components/pages/library/incompleteDownloads.tsx @@ -7,7 +7,7 @@ import { toast } from "sonner"; import { useAppContext } from "@/providers/appContextProvider"; import { useDownloadActionStatesStore, useSettingsPageStatesStore } from "@/services/store"; import { formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils"; -import { ArrowUpRightIcon, CircleCheck, File, Loader2, Music, Pause, Play, Video, X } from "lucide-react"; +import { ArrowUpRightIcon, CircleCheck, File, Info, Loader2, Music, Pause, Play, RotateCw, Video, X } from "lucide-react"; import { DownloadState } from "@/types/download"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; import { useNavigate } from "react-router-dom"; @@ -72,7 +72,7 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) { {((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && ( )} - {(state.download_status === 'downloading' || state.download_status === 'paused') && state.progress && state.status !== 'finished' && ( + {(state.download_status === 'downloading' || state.download_status === 'paused' || state.download_status === 'errored') && state.progress && state.status !== 'finished' && (
{state.progress}% @@ -84,7 +84,21 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
)}
- {state.download_status && state.download_status === 'downloading' && state.status === 'finished' ? 'Processing' : state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)} {debugMode && state.download_id ? <> ID: {state.download_id.toUpperCase()} : ""} {state.download_status === 'downloading' && state.status !== 'finished' && state.speed ? <> Speed: {formatSpeed(state.speed)} : ""} {state.download_status === 'downloading' && state.eta ? <> ETA: {formatSecToTimeString(state.eta)} : ""} + {state.download_status && state.download_status === 'downloading' && state.status === 'finished' ? ( + Processing + ) : state.download_status && state.download_status === 'errored' ? ( + Errored + ) : ( + {state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)} + )} { + (debugMode && state.download_id) || (state.download_status === 'errored' && state.download_id) && ( + <> ID: {state.download_id.toUpperCase()} + )} { + state.download_status === 'downloading' && state.status !== 'finished' && state.speed && ( + <> Speed: {formatSpeed(state.speed)} + )} {state.download_status === 'downloading' && state.eta && ( + <> ETA: {formatSecToTimeString(state.eta)} + )}
@@ -102,7 +116,7 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) { } catch (e) { console.error(e); toast.error("Failed to Resume Download", { - description: "An error occurred while trying to resume the download.", + description: `An error occurred while trying to resume the download for "${state.title}".`, }) } finally { setIsResumingDownload(state.download_id, false); @@ -122,6 +136,37 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) { )} + ) : state.download_status === 'errored' ? ( + ) : (