feat: added cancel search button and improved search error handling

This commit is contained in:
2025-07-21 19:27:39 +05:30
parent 161f7ad13c
commit 495b6f2129
4 changed files with 65 additions and 15 deletions

View File

@@ -43,6 +43,8 @@ export default function App({ children }: { children: React.ReactNode }) {
const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath); const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath);
const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath); const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath);
const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid);
// const isUsingDefaultSettings = useSettingsPageStatesStore((state) => state.isUsingDefaultSettings); // const isUsingDefaultSettings = useSettingsPageStatesStore((state) => state.isUsingDefaultSettings);
const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings); const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings);
const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey); const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey);
@@ -121,15 +123,20 @@ export default function App({ children }: { children: React.ReactNode }) {
jsonOutput += line; jsonOutput += line;
}); });
command.on('close', async () => { command.on('close', async (data) => {
if (data.code !== 0) {
console.error(`yt-dlp failed to fetch metadata with code ${data.code}`);
resolve(null);
} else {
try { try {
const data: RawVideoInfo = JSON.parse(jsonOutput); const parsedData: RawVideoInfo = JSON.parse(jsonOutput);
resolve(data); resolve(parsedData);
} }
catch (e) { catch (e) {
console.error(`Failed to parse JSON: ${e}`); console.error(`Failed to parse JSON: ${e}`);
resolve(null); resolve(null);
} }
}
}); });
command.on('error', error => { command.on('error', error => {
@@ -137,7 +144,9 @@ export default function App({ children }: { children: React.ReactNode }) {
resolve(null); resolve(null);
}); });
command.spawn().catch(e => { command.spawn().then(child => {
setSearchPid(child.pid);
}).catch(e => {
console.error(`Failed to spawn command: ${e}`); console.error(`Failed to spawn command: ${e}`);
resolve(null); resolve(null);
}); });
@@ -265,7 +274,7 @@ export default function App({ children }: { children: React.ReactNode }) {
console.log('Starting download with args:', args); console.log('Starting download with args:', args);
const command = Command.sidecar('binaries/yt-dlp', args); const command = Command.sidecar('binaries/yt-dlp', args);
command.on('close', async data => { command.on('close', async (data) => {
if (data.code !== 0) { if (data.code !== 0) {
console.error(`Download failed with code ${data.code}`); console.error(`Download failed with code ${data.code}`);
if (!isErrorExpected) { if (!isErrorExpected) {
@@ -303,6 +312,8 @@ export default function App({ children }: { children: React.ReactNode }) {
command.on('error', error => { command.on('error', error => {
console.error(`Error: ${error}`); console.error(`Error: ${error}`);
setIsErrored(true);
setErroredDownloadId(downloadId);
}); });
command.stdout.on('data', line => { command.stdout.on('data', line => {

View File

@@ -9,7 +9,7 @@ import { useToast } from "@/hooks/use-toast";
import { useAppContext } from "@/providers/appContextProvider"; import { useAppContext } from "@/providers/appContextProvider";
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store"; import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
import { determineFileType, fileFormatFilter, formatBitrate, formatDurationString, formatFileSize, formatReleaseDate, formatYtStyleCount, isObjEmpty, sortByBitrate } from "@/utils"; import { determineFileType, fileFormatFilter, formatBitrate, formatDurationString, formatFileSize, formatReleaseDate, formatYtStyleCount, isObjEmpty, sortByBitrate } from "@/utils";
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon } from "lucide-react"; import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon, X } from "lucide-react";
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup"; import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup"; import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
@@ -23,6 +23,7 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from "@/component
import { config } from "@/config"; import { config } from "@/config";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { invoke } from "@tauri-apps/api/core";
const searchFormSchema = z.object({ const searchFormSchema = z.object({
url: z.string().min(1, { message: "URL is required" }) url: z.string().min(1, { message: "URL is required" })
@@ -38,11 +39,14 @@ export default function DownloaderPage() {
const isMetadataLoading = useCurrentVideoMetadataStore((state) => state.isMetadataLoading); const isMetadataLoading = useCurrentVideoMetadataStore((state) => state.isMetadataLoading);
const requestedUrl = useCurrentVideoMetadataStore((state) => state.requestedUrl); const requestedUrl = useCurrentVideoMetadataStore((state) => state.requestedUrl);
const autoSubmitSearch = useCurrentVideoMetadataStore((state) => state.autoSubmitSearch); const autoSubmitSearch = useCurrentVideoMetadataStore((state) => state.autoSubmitSearch);
const searchPid = useCurrentVideoMetadataStore((state) => state.searchPid);
const setVideoUrl = useCurrentVideoMetadataStore((state) => state.setVideoUrl); const setVideoUrl = useCurrentVideoMetadataStore((state) => state.setVideoUrl);
const setVideoMetadata = useCurrentVideoMetadataStore((state) => state.setVideoMetadata); const setVideoMetadata = useCurrentVideoMetadataStore((state) => state.setVideoMetadata);
const setIsMetadataLoading = useCurrentVideoMetadataStore((state) => state.setIsMetadataLoading); const setIsMetadataLoading = useCurrentVideoMetadataStore((state) => state.setIsMetadataLoading);
const setRequestedUrl = useCurrentVideoMetadataStore((state) => state.setRequestedUrl); const setRequestedUrl = useCurrentVideoMetadataStore((state) => state.setRequestedUrl);
const setAutoSubmitSearch = useCurrentVideoMetadataStore((state) => state.setAutoSubmitSearch); const setAutoSubmitSearch = useCurrentVideoMetadataStore((state) => state.setAutoSubmitSearch);
const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid);
const setShowSearchError = useCurrentVideoMetadataStore((state) => state.setShowSearchError);
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab); const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload); const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
@@ -205,26 +209,42 @@ export default function DownloaderPage() {
function handleSearchSubmit(values: z.infer<typeof searchFormSchema>) { function handleSearchSubmit(values: z.infer<typeof searchFormSchema>) {
setVideoMetadata(null); setVideoMetadata(null);
setSearchPid(null);
setShowSearchError(true);
setIsMetadataLoading(true); setIsMetadataLoading(true);
setSelectedDownloadFormat('best'); setSelectedDownloadFormat('best');
setSelectedCombinableVideoFormat(''); setSelectedCombinableVideoFormat('');
setSelectedCombinableAudioFormat(''); setSelectedCombinableAudioFormat('');
setSelectedSubtitles([]); setSelectedSubtitles([]);
setSelectedPlaylistVideoIndex('1'); setSelectedPlaylistVideoIndex('1');
fetchVideoMetadata(values.url).then((metadata) => { fetchVideoMetadata(values.url).then((metadata) => {
if (!metadata || (metadata._type !== 'video' && metadata._type !== 'playlist') || (metadata && metadata._type === 'video' && metadata.formats.length <= 0) || (metadata && metadata._type === 'playlist' && metadata.entries.length <= 0)) { if (!metadata || (metadata._type !== 'video' && metadata._type !== 'playlist') || (metadata && metadata._type === 'video' && metadata.formats.length <= 0) || (metadata && metadata._type === 'playlist' && metadata.entries.length <= 0)) {
const showSearchError = useCurrentVideoMetadataStore.getState().showSearchError;
if (showSearchError) {
toast({ toast({
title: 'Opps! No results found', title: 'Oops! No results found',
description: 'The provided URL does not contain any downloadable content. Please check the URL and try again.', description: 'The provided URL does not contain any downloadable content or you are not connected to the internet. Please check the URL, your network connection and try again.',
variant: "destructive" variant: "destructive"
}); });
} }
}
if (metadata && (metadata._type === 'video' || metadata._type === 'playlist') && ((metadata._type === 'video' && metadata.formats.length > 0) || (metadata._type === 'playlist' && metadata.entries.length > 0))) setVideoMetadata(metadata); if (metadata && (metadata._type === 'video' || metadata._type === 'playlist') && ((metadata._type === 'video' && metadata.formats.length > 0) || (metadata._type === 'playlist' && metadata.entries.length > 0))) setVideoMetadata(metadata);
if (metadata) console.log(metadata); if (metadata) console.log(metadata);
setIsMetadataLoading(false); setIsMetadataLoading(false);
}); });
} }
const cancelSearch = async (pid: number | null) => {
setShowSearchError(false);
if (pid) {
console.log("Killing process with PID:", pid);
await invoke('kill_all_process', { pid: pid });
}
setVideoMetadata(null);
setIsMetadataLoading(false);
};
useEffect(() => { useEffect(() => {
const updateBottomBarWidth = (): void => { const updateBottomBarWidth = (): void => {
if (containerRef.current && bottomBarRef.current) { if (containerRef.current && bottomBarRef.current) {
@@ -338,6 +358,17 @@ export default function DownloaderPage() {
</FormItem> </FormItem>
)} )}
/> />
{isMetadataLoading && (
<Button
type="button"
variant="destructive"
size="icon"
disabled={!isMetadataLoading}
onClick={() => cancelSearch(searchPid)}
>
<X className="size-4" />
</Button>
)}
<Button <Button
type="submit" type="submit"
disabled={!videoUrl || isMetadataLoading} disabled={!videoUrl || isMetadataLoading}

View File

@@ -34,11 +34,15 @@ export const useCurrentVideoMetadataStore = create<CurrentVideoMetadataStore>((s
isMetadataLoading: false, isMetadataLoading: false,
requestedUrl: '', requestedUrl: '',
autoSubmitSearch: false, autoSubmitSearch: false,
searchPid: null,
showSearchError: true,
setVideoUrl: (url) => set(() => ({ videoUrl: url })), setVideoUrl: (url) => set(() => ({ videoUrl: url })),
setVideoMetadata: (metadata) => set(() => ({ videoMetadata: metadata })), setVideoMetadata: (metadata) => set(() => ({ videoMetadata: metadata })),
setIsMetadataLoading: (isLoading) => set(() => ({ isMetadataLoading: isLoading })), setIsMetadataLoading: (isLoading) => set(() => ({ isMetadataLoading: isLoading })),
setRequestedUrl: (url) => set(() => ({ requestedUrl: url })), setRequestedUrl: (url) => set(() => ({ requestedUrl: url })),
setAutoSubmitSearch: (autoSubmit) => set(() => ({ autoSubmitSearch: autoSubmit })), setAutoSubmitSearch: (autoSubmit) => set(() => ({ autoSubmitSearch: autoSubmit })),
setSearchPid: (pid) => set(() => ({ searchPid: pid })),
setShowSearchError: (showError) => set(() => ({ showSearchError: showError }))
})); }));
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({

View File

@@ -23,11 +23,15 @@ export interface CurrentVideoMetadataStore {
isMetadataLoading: boolean; isMetadataLoading: boolean;
requestedUrl: string; requestedUrl: string;
autoSubmitSearch: boolean; autoSubmitSearch: boolean;
searchPid: number | null;
showSearchError: boolean;
setVideoUrl: (url: string) => void; setVideoUrl: (url: string) => void;
setVideoMetadata: (metadata: RawVideoInfo | null) => void; setVideoMetadata: (metadata: RawVideoInfo | null) => void;
setIsMetadataLoading: (isLoading: boolean) => void; setIsMetadataLoading: (isLoading: boolean) => void;
setRequestedUrl: (url: string) => void; setRequestedUrl: (url: string) => void;
setAutoSubmitSearch: (autoSubmit: boolean) => void; setAutoSubmitSearch: (autoSubmit: boolean) => void;
setSearchPid: (pid: number | null) => void;
setShowSearchError: (showError: boolean) => void;
} }
export interface DownloaderPageStatesStore { export interface DownloaderPageStatesStore {