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, color_scheme: APP_COLOR_SCHEME,
} = useSettingsPageStatesStore(state => state.settings); } = useSettingsPageStatesStore(state => state.settings);
const isErrored = useDownloaderPageStatesStore((state) => state.isErrored); const erroredDownloadIds = useDownloaderPageStatesStore((state) => state.erroredDownloadIds);
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected); const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds);
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId); const removeErroredDownload = useDownloaderPageStatesStore((state) => state.removeErroredDownload);
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored); const removeExpectedErrorDownload = useDownloaderPageStatesStore((state) => state.removeExpectedErrorDownload);
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
const appWindow = getCurrentWebviewWindow() const appWindow = getCurrentWebviewWindow();
const navigate = useNavigate(); const navigate = useNavigate();
const LOG = useLogger(); const LOG = useLogger();
const currentPlatform = platform(); const currentPlatform = platform();
@@ -79,6 +77,7 @@ export default function App({ children }: { children: React.ReactNode }) {
const hasRunYtDlpAutoUpdateRef = useRef(false); const hasRunYtDlpAutoUpdateRef = useRef(false);
const hasRunAppUpdateCheckRef = useRef(false); const hasRunAppUpdateCheckRef = useRef(false);
const isRegisteredToMacOsRef = useRef(false); const isRegisteredToMacOsRef = useRef(false);
const pendingErrorUpdatesRef = useRef<Set<string>>(new Set());
const { fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload, processQueuedDownloads } = useDownloader(); 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 // show a toast and pause the download when yt-dlp exits unexpectedly
useEffect(() => { 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", { 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) => { onSuccess: (data) => {
console.log("Download status updated successfully:", data); console.log("Download status updated successfully:", data);
queryClient.invalidateQueries({ queryKey: ['download-states'] }); queryClient.invalidateQueries({ queryKey: ['download-states'] });
removeErroredDownload(downloadId);
pendingErrorUpdatesRef.current.delete(downloadId);
}, },
onError: (error) => { onError: (error) => {
console.error("Failed to update download status:", error); console.error("Failed to update download status:", error);
removeErroredDownload(downloadId);
pendingErrorUpdatesRef.current.delete(downloadId);
} }
}) });
setErroredDownloadId(null); }, 500);
} timeoutIds.push(timeoutId);
setIsErrored(false); });
setIsErrorExpected(false);
} return () => {
}, [isErrored, isErrorExpected, erroredDownloadId, setIsErrored, setIsErrorExpected, setErroredDownloadId]); timeoutIds.forEach(id => clearTimeout(id));
};
}, [erroredDownloadIds, expectedErrorDownloadIds]);
// auto reset error states after 3 seconds of expecting an error // auto reset error states after 3 seconds of expecting an error
useEffect(() => { useEffect(() => {
if (isErrorExpected) { if (expectedErrorDownloadIds.size > 0) {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
setIsErrored(false); expectedErrorDownloadIds.forEach((downloadId) => {
setIsErrorExpected(false); removeErroredDownload(downloadId);
setErroredDownloadId(null); removeExpectedErrorDownload(downloadId);
});
}, 3000); }, 3000);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
} }
}, [isErrorExpected, setIsErrorExpected]); }, [expectedErrorDownloadIds]);
return ( return (
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}> <AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>

View File

@@ -7,7 +7,7 @@ import { toast } from "sonner";
import { useAppContext } from "@/providers/appContextProvider"; import { useAppContext } from "@/providers/appContextProvider";
import { useDownloadActionStatesStore, useSettingsPageStatesStore } from "@/services/store"; import { useDownloadActionStatesStore, useSettingsPageStatesStore } from "@/services/store";
import { formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils"; 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 { DownloadState } from "@/types/download";
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
import { useNavigate } from "react-router-dom"; 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 === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && (
<IndeterminateProgress indeterminate={true} className="w-full" /> <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"> <div className="w-full flex items-center gap-2">
<span className="text-sm text-nowrap">{state.progress}%</span> <span className="text-sm text-nowrap">{state.progress}%</span>
<Progress value={state.progress} /> <Progress value={state.progress} />
@@ -84,7 +84,21 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
</div> </div>
)} )}
<div className="text-xs text-muted-foreground"> <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> </div>
<div className="w-full flex items-center gap-2 mt-2"> <div className="w-full flex items-center gap-2 mt-2">
@@ -102,7 +116,7 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
} catch (e) { } catch (e) {
console.error(e); console.error(e);
toast.error("Failed to Resume Download", { 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 { } finally {
setIsResumingDownload(state.download_id, false); setIsResumingDownload(state.download_id, false);
@@ -122,6 +136,37 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
</> </>
)} )}
</Button> </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 <Button
size="sm" size="sm"
@@ -136,7 +181,7 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
} catch (e) { } catch (e) {
console.error(e); console.error(e);
toast.error("Failed to Pause Download", { 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 { } finally {
setIsPausingDownload(state.download_id, false); setIsPausingDownload(state.download_id, false);
@@ -165,12 +210,12 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) {
try { try {
await cancelDownload(state) await cancelDownload(state)
toast.success("Canceled Download", { toast.success("Canceled Download", {
description: "Download canceled successfully.", description: `The download for "${state.title}" has been canceled.`,
}) })
} catch (e) { } catch (e) {
console.error(e); console.error(e);
toast.error("Failed to Cancel Download", { 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 { } finally {
setIsCancelingDownload(state.download_id, false); setIsCancelingDownload(state.download_id, false);

View File

@@ -63,10 +63,11 @@ export default function useDownloader() {
download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION
} = useSettingsPageStatesStore(state => state.settings); } = useSettingsPageStatesStore(state => state.settings);
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected); const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds);
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored); const addErroredDownload = useDownloaderPageStatesStore((state) => state.addErroredDownload);
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected); const removeErroredDownload = useDownloaderPageStatesStore((state) => state.removeErroredDownload);
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId); const addExpectedErrorDownload = useDownloaderPageStatesStore((state) => state.addExpectedErrorDownload);
const removeExpectedErrorDownload = useDownloaderPageStatesStore((state) => state.removeExpectedErrorDownload);
const LOG = useLogger(); const LOG = useLogger();
const currentPlatform = platform(); const currentPlatform = platform();
@@ -210,10 +211,6 @@ export default function useDownloader() {
const startDownload = async (params: StartDownloadParams) => { const startDownload = async (params: StartDownloadParams) => {
const { url, selectedFormat, downloadConfig, selectedSubtitles, resumeState, playlistItems } = params; const { url, selectedFormat, downloadConfig, selectedSubtitles, resumeState, playlistItems } = params;
LOG.info('NEODLP', `Initiating yt-dlp download for URL: ${url}`); 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 }); console.log('Starting download:', { url, selectedFormat, downloadConfig, selectedSubtitles, resumeState, playlistItems });
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) { 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 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 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)*/; 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 tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`);
// const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`); // 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}`)); // 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) { if (data.code !== 0) {
console.error(`Download failed with code ${data.code}`); 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)`); LOG.error(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code} (ignore if you manually paused or cancelled the download)`);
if (!isErrorExpected) { if (!expectedErrorDownloadIds.has(downloadId)) addErroredDownload(downloadId);
setIsErrored(true);
setErroredDownloadId(downloadId);
}
} else { } else {
LOG.info(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code}`); 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 => { command.on('error', error => {
console.error(`Error: ${error}`); console.error(`Error: ${error}`);
LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`); LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`);
setIsErrored(true); addErroredDownload(downloadId);
setErroredDownloadId(downloadId);
}); });
command.stdout.on('data', line => { command.stdout.on('data', line => {
@@ -681,7 +679,7 @@ export default function useDownloader() {
try { try {
LOG.info('NEODLP', `Pausing yt-dlp download with id: ${downloadState.download_id} (as per user request)`); 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)) { 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); console.log("Killing process with PID:", downloadState.process_id);
await invoke('kill_all_process', { pid: downloadState.process_id }); await invoke('kill_all_process', { pid: downloadState.process_id });
} }
@@ -706,12 +704,13 @@ export default function useDownloader() {
reject(error); reject(error);
} }
}); });
}, 1000); }, 500);
}); });
} catch (e) { } catch (e) {
console.error(`Failed to pause download: ${e}`); console.error(`Failed to pause download: ${e}`);
LOG.error('NEODLP', `Failed to pause download with id: ${downloadState.download_id} with error: ${e}`); LOG.error('NEODLP', `Failed to pause download with id: ${downloadState.download_id} with error: ${e}`);
isProcessingQueueRef.current = false; isProcessingQueueRef.current = false;
removeExpectedErrorDownload(downloadState.download_id);
throw e; throw e;
} }
}; };
@@ -726,6 +725,7 @@ export default function useDownloader() {
output_format: null, output_format: null,
embed_metadata: null, embed_metadata: null,
embed_thumbnail: null, embed_thumbnail: null,
square_crop_thumbnail: null,
sponsorblock: null, sponsorblock: null,
custom_command: null custom_command: null
}, },
@@ -744,31 +744,39 @@ export default function useDownloader() {
try { try {
LOG.info('NEODLP', `Cancelling yt-dlp download with id: ${downloadState.download_id} (as per user request)`); 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)) { 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); console.log("Killing process with PID:", downloadState.process_id);
await invoke('kill_all_process', { pid: downloadState.process_id }); await invoke('kill_all_process', { pid: downloadState.process_id });
} }
downloadStateDeleter.mutate(downloadState.download_id, {
onSuccess: (data) => {
console.log("Download State deleted successfully:", data);
queryClient.invalidateQueries({ queryKey: ['download-states'] });
// Reset processing flag and trigger queue processing
isProcessingQueueRef.current = false;
// Process the queue after a short delay return new Promise<void>((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
processQueuedDownloads(); downloadStateDeleter.mutate(downloadState.download_id, {
}, 1000); onSuccess: (data) => {
}, console.log("Download State deleted successfully:", data);
onError: (error) => { queryClient.invalidateQueries({ queryKey: ['download-states'] });
console.error("Failed to delete download state:", error); // Reset processing flag and trigger queue processing
isProcessingQueueRef.current = false; isProcessingQueueRef.current = false;
}
// Process the queue after a short delay
setTimeout(() => {
processQueuedDownloads();
}, 1000);
resolve();
},
onError: (error) => {
console.error("Failed to delete download state:", error);
isProcessingQueueRef.current = false;
reject(error);
}
});
}, 500);
}); });
return Promise.resolve();
} catch (e) { } catch (e) {
console.error(`Failed to cancel download: ${e}`); console.error(`Failed to cancel download: ${e}`);
LOG.error('NEODLP', `Failed to cancel download with id: ${downloadState.download_id} with error: ${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; throw e;
} }
} }
@@ -826,6 +834,7 @@ export default function useDownloader() {
output_format: null, output_format: null,
embed_metadata: null, embed_metadata: null,
embed_thumbnail: null, embed_thumbnail: null,
square_crop_thumbnail: null,
sponsorblock: null, sponsorblock: null,
custom_command: null custom_command: null
}, },

View File

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

View File

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

View File

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