refactor: added errored state and improved error detection

This commit is contained in:
2025-12-19 10:30:45 +05:30
parent c1c2384c78
commit da806b21e9
6 changed files with 177 additions and 83 deletions

View File

@@ -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<Set<string>>(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 (
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>

View File

@@ -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')) && (
<IndeterminateProgress indeterminate={true} className="w-full" />
)}
{(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' && (
<div className="w-full flex items-center gap-2">
<span className="text-sm text-nowrap">{state.progress}%</span>
<Progress value={state.progress} />
@@ -84,7 +84,21 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
</div>
)}
<div className="text-xs text-muted-foreground">
{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 ? <><span className="text-primary"></span> ID: {state.download_id.toUpperCase()}</> : ""} {state.download_status === 'downloading' && state.status !== 'finished' && state.speed ? <><span className="text-primary"></span> Speed: {formatSpeed(state.speed)}</> : ""} {state.download_status === 'downloading' && state.eta ? <><span className="text-primary"></span> ETA: {formatSecToTimeString(state.eta)}</> : ""}
{state.download_status && state.download_status === 'downloading' && state.status === 'finished' ? (
<span>Processing</span>
) : state.download_status && state.download_status === 'errored' ? (
<span className="text-destructive"><Info className="inline size-3 mb-1 mr-0.5" /> Errored</span>
) : (
<span>{state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)}</span>
)} {
(debugMode && state.download_id) || (state.download_status === 'errored' && state.download_id) && (
<><span className="text-primary"></span> ID: {state.download_id.toUpperCase()}</>
)} {
state.download_status === 'downloading' && state.status !== 'finished' && state.speed && (
<><span className="text-primary"></span> Speed: {formatSpeed(state.speed)}</>
)} {state.download_status === 'downloading' && state.eta && (
<><span className="text-primary"></span> ETA: {formatSecToTimeString(state.eta)}</>
)}
</div>
</div>
<div className="w-full flex items-center gap-2 mt-2">
@@ -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) {
</>
)}
</Button>
) : state.download_status === 'errored' ? (
<Button
size="sm"
className="w-fill"
onClick={async () => {
setIsResumingDownload(state.download_id, true);
try {
await resumeDownload(state);
} catch (e) {
console.error(e);
toast.error("Failed to Restart Download", {
description: `An error occurred while trying to restart the download for "${state.title}".`,
})
} finally {
setIsResumingDownload(state.download_id, false);
}
}}
disabled={itemActionStates.isResuming || itemActionStates.isCanceling}
>
{itemActionStates.isResuming ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Retrying
</>
) : (
<>
<RotateCw className="w-4 h-4" />
Retry
</>
)}
</Button>
) : (
<Button
size="sm"
@@ -136,7 +181,7 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
} catch (e) {
console.error(e);
toast.error("Failed to Pause Download", {
description: "An error occurred while trying to pause the download."
description: `An error occurred while trying to pause the download for "${state.title}".`,
})
} finally {
setIsPausingDownload(state.download_id, false);
@@ -165,12 +210,12 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
try {
await cancelDownload(state)
toast.success("Canceled Download", {
description: "Download canceled successfully.",
description: `The download for "${state.title}" has been canceled.`,
})
} catch (e) {
console.error(e);
toast.error("Failed to Cancel Download", {
description: "An error occurred while trying to cancel the download.",
description: `An error occurred while trying to cancel the download for "${state.title}".`,
})
} finally {
setIsCancelingDownload(state.download_id, false);

View File

@@ -63,10 +63,11 @@ export default function useDownloader() {
download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION
} = useSettingsPageStatesStore(state => state.settings);
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds);
const addErroredDownload = useDownloaderPageStatesStore((state) => state.addErroredDownload);
const removeErroredDownload = useDownloaderPageStatesStore((state) => state.removeErroredDownload);
const addExpectedErrorDownload = useDownloaderPageStatesStore((state) => state.addExpectedErrorDownload);
const removeExpectedErrorDownload = useDownloaderPageStatesStore((state) => state.removeExpectedErrorDownload);
const LOG = useLogger();
const currentPlatform = platform();
@@ -210,10 +211,6 @@ export default function useDownloader() {
const startDownload = async (params: StartDownloadParams) => {
const { url, selectedFormat, downloadConfig, selectedSubtitles, resumeState, playlistItems } = params;
LOG.info('NEODLP', `Initiating yt-dlp download for URL: ${url}`);
// set error states to default
setIsErrored(false);
setIsErrorExpected(false);
setErroredDownloadId(null);
console.log('Starting download:', { url, selectedFormat, downloadConfig, selectedSubtitles, resumeState, playlistItems });
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
@@ -258,6 +255,11 @@ export default function useDownloader() {
const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain);
const playlistId = isPlaylist ? (resumeState?.playlist_id || generateVideoId(videoMetadata.playlist_id, videoMetadata.webpage_url_domain)) : null;
const downloadId = resumeState?.download_id || ulid() /*generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain)*/;
// Clear any existing errored/expected error states for this download
removeErroredDownload(downloadId);
removeExpectedErrorDownload(downloadId);
// const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`);
// const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`);
// let downloadFilePath = resumeState?.filepath || await join(downloadDirPath, sanitizeFilename(`${videoMetadata.title}_${videoMetadata.resolution || 'unknown'}[${videoMetadata.id}].${videoMetadata.ext}`));
@@ -446,10 +448,7 @@ export default function useDownloader() {
if (data.code !== 0) {
console.error(`Download failed with code ${data.code}`);
LOG.error(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code} (ignore if you manually paused or cancelled the download)`);
if (!isErrorExpected) {
setIsErrored(true);
setErroredDownloadId(downloadId);
}
if (!expectedErrorDownloadIds.has(downloadId)) addErroredDownload(downloadId);
} else {
LOG.info(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code}`);
}
@@ -458,8 +457,7 @@ export default function useDownloader() {
command.on('error', error => {
console.error(`Error: ${error}`);
LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`);
setIsErrored(true);
setErroredDownloadId(downloadId);
addErroredDownload(downloadId);
});
command.stdout.on('data', line => {
@@ -681,7 +679,7 @@ export default function useDownloader() {
try {
LOG.info('NEODLP', `Pausing yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
setIsErrorExpected(true); // Set error expected to true to handle UI state
addExpectedErrorDownload(downloadState.download_id); // Mark as error expected to handle UI state
console.log("Killing process with PID:", downloadState.process_id);
await invoke('kill_all_process', { pid: downloadState.process_id });
}
@@ -706,12 +704,13 @@ export default function useDownloader() {
reject(error);
}
});
}, 1000);
}, 500);
});
} catch (e) {
console.error(`Failed to pause download: ${e}`);
LOG.error('NEODLP', `Failed to pause download with id: ${downloadState.download_id} with error: ${e}`);
isProcessingQueueRef.current = false;
removeExpectedErrorDownload(downloadState.download_id);
throw e;
}
};
@@ -726,6 +725,7 @@ export default function useDownloader() {
output_format: null,
embed_metadata: null,
embed_thumbnail: null,
square_crop_thumbnail: null,
sponsorblock: null,
custom_command: null
},
@@ -744,10 +744,13 @@ export default function useDownloader() {
try {
LOG.info('NEODLP', `Cancelling yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
setIsErrorExpected(true); // Set error expected to true to handle UI state
addExpectedErrorDownload(downloadState.download_id); // Mark as error expected to handle UI state
console.log("Killing process with PID:", downloadState.process_id);
await invoke('kill_all_process', { pid: downloadState.process_id });
}
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
downloadStateDeleter.mutate(downloadState.download_id, {
onSuccess: (data) => {
console.log("Download State deleted successfully:", data);
@@ -759,16 +762,21 @@ export default function useDownloader() {
setTimeout(() => {
processQueuedDownloads();
}, 1000);
resolve();
},
onError: (error) => {
console.error("Failed to delete download state:", error);
isProcessingQueueRef.current = false;
reject(error);
}
});
return Promise.resolve();
}, 500);
});
} catch (e) {
console.error(`Failed to cancel download: ${e}`);
LOG.error('NEODLP', `Failed to cancel download with id: ${downloadState.download_id} with error: ${e}`);
isProcessingQueueRef.current = false;
removeExpectedErrorDownload(downloadState.download_id);
throw e;
}
}
@@ -826,6 +834,7 @@ export default function useDownloader() {
output_format: null,
embed_metadata: null,
embed_thumbnail: null,
square_crop_thumbnail: null,
sponsorblock: null,
custom_command: null
},

View File

@@ -3,7 +3,16 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const TanstackProvider = ({children}: {children: React.ReactNode}) => {
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'always',
},
mutations: {
networkMode: 'always',
}
}
});
return (
<QueryClientProvider client={queryClient}>
{children}

View File

@@ -62,9 +62,8 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
sponsorblock: null,
custom_command: null
},
isErrored: false,
isErrorExpected: false,
erroredDownloadId: null,
erroredDownloadIds: new Set(),
expectedErrorDownloadIds: new Set(),
videoPanelSizes: [35, 65],
playlistPanelSizes: [45, 55],
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
@@ -92,9 +91,23 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
custom_command: null
}
})),
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
addErroredDownload: (downloadId) => set((state) => ({
erroredDownloadIds: new Set(state.erroredDownloadIds).add(downloadId)
})),
removeErroredDownload: (downloadId) => set((state) => {
const newSet = new Set(state.erroredDownloadIds);
newSet.delete(downloadId);
return { erroredDownloadIds: newSet };
}),
addExpectedErrorDownload: (downloadId) => set((state) => ({
expectedErrorDownloadIds: new Set(state.expectedErrorDownloadIds).add(downloadId)
})),
removeExpectedErrorDownload: (downloadId) => set((state) => {
const newSet = new Set(state.expectedErrorDownloadIds);
newSet.delete(downloadId);
return { expectedErrorDownloadIds: newSet };
}),
clearErrorStates: () => set({ erroredDownloadIds: new Set(), expectedErrorDownloadIds: new Set() }),
setVideoPanelSizes: (sizes) => set(() => ({ videoPanelSizes: sizes })),
setPlaylistPanelSizes: (sizes) => set(() => ({ playlistPanelSizes: sizes }))
}));

View File

@@ -45,9 +45,8 @@ export interface DownloaderPageStatesStore {
selectedSubtitles: string[];
selectedPlaylistVideoIndex: string;
downloadConfiguration: DownloadConfiguration;
isErrored: boolean;
isErrorExpected: boolean;
erroredDownloadId: string | null;
erroredDownloadIds: Set<string>;
expectedErrorDownloadIds: Set<string>;
videoPanelSizes: number[];
playlistPanelSizes: number[];
setActiveDownloadModeTab: (tab: string) => void;
@@ -61,9 +60,11 @@ export interface DownloaderPageStatesStore {
setDownloadConfigurationKey: (key: string, value: unknown) => void;
setDownloadConfiguration: (config: DownloadConfiguration) => void;
resetDownloadConfiguration: () => void;
setIsErrored: (isErrored: boolean) => void;
setIsErrorExpected: (isErrorExpected: boolean) => void;
setErroredDownloadId: (downloadId: string | null) => void;
addErroredDownload: (downloadId: string) => void;
removeErroredDownload: (downloadId: string) => void;
addExpectedErrorDownload: (downloadId: string) => void;
removeExpectedErrorDownload: (downloadId: string) => void;
clearErrorStates: () => void;
setVideoPanelSizes: (sizes: number[]) => void;
setPlaylistPanelSizes: (sizes: number[]) => void;
}