1
1
mirror of https://github.com/neosubhamoy/neodlp.git synced 2026-03-22 16:05:50 +05:30
Files
neodlp/src/pages/downloader.tsx

401 lines
20 KiB
TypeScript

import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { useAppContext } from "@/providers/appContextProvider";
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
import { determineFileType, fileFormatFilter, getCommonFormats, getCommonSubtitles, getCommonAutoSubtitles, sortByBitrate, getMergedBestFormat } from "@/utils";
import { Loader2, PackageSearch, X, Clipboard } from "lucide-react";
import { useEffect, useRef } from "react";
import { VideoFormat } from "@/types/video";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
import { config } from "@/config";
import { invoke } from "@tauri-apps/api/core";
import { readText } from '@tauri-apps/plugin-clipboard-manager';
import { useTheme } from "@/providers/themeProvider";
import { VideoDownloader } from "@/components/pages/downloader/videoDownloader";
import { PlaylistDownloader } from "@/components/pages/downloader/playlistDownloader";
import { BottomBar } from "@/components/pages/downloader/bottomBar";
const searchFormSchema = z.object({
url: z.url({
error: (issue) => issue.input === undefined || issue.input === null || issue.input === ""
? "URL is required"
: "Invalid URL format"
}),
});
export default function DownloaderPage() {
const { fetchVideoMetadata } = useAppContext();
const { setTheme } = useTheme();
const videoUrl = useCurrentVideoMetadataStore((state) => state.videoUrl);
const videoMetadata = useCurrentVideoMetadataStore((state) => state.videoMetadata);
const isMetadataLoading = useCurrentVideoMetadataStore((state) => state.isMetadataLoading);
const requestedUrl = useCurrentVideoMetadataStore((state) => state.requestedUrl);
const autoSubmitSearch = useCurrentVideoMetadataStore((state) => state.autoSubmitSearch);
const searchPid = useCurrentVideoMetadataStore((state) => state.searchPid);
const setVideoUrl = useCurrentVideoMetadataStore((state) => state.setVideoUrl);
const setVideoMetadata = useCurrentVideoMetadataStore((state) => state.setVideoMetadata);
const setIsMetadataLoading = useCurrentVideoMetadataStore((state) => state.setIsMetadataLoading);
const setRequestedUrl = useCurrentVideoMetadataStore((state) => state.setRequestedUrl);
const setAutoSubmitSearch = useCurrentVideoMetadataStore((state) => state.setAutoSubmitSearch);
const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid);
const setShowSearchError = useCurrentVideoMetadataStore((state) => state.setShowSearchError);
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
const selectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormats);
const selectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideos);
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
const setSelectedCombinableAudioFormats = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormats);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const setSelectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideos);
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
const appTheme = useSettingsPageStatesStore(state => state.settings.theme);
const appColorScheme = useSettingsPageStatesStore(state => state.settings.color_scheme);
const containerRef = useRef<HTMLDivElement>(null);
const commonFormats = (() => {
if (videoMetadata?._type === 'video') {
return videoMetadata?.formats;
} else if (videoMetadata?._type === 'playlist') {
return getCommonFormats(videoMetadata.entries, selectedPlaylistVideos);
}
return [];
})();
const audioOnlyFormats = sortByBitrate(commonFormats.filter(fileFormatFilter('audio')));
const videoOnlyFormats = sortByBitrate(commonFormats.filter(fileFormatFilter('video')));
const combinedFormats = sortByBitrate(commonFormats.filter(fileFormatFilter('video+audio')));
const av1VideoFormats = videoMetadata?.webpage_url_domain === 'youtube.com' ? sortByBitrate(commonFormats.filter((format) => format.vcodec?.startsWith('av01'))) : [];
const opusAudioFormats = videoMetadata?.webpage_url_domain === 'youtube.com' ? sortByBitrate(commonFormats.filter((format) => format.acodec?.startsWith('opus'))) : [];
const qualityPresetFormats: VideoFormat[] | undefined = videoMetadata?.webpage_url_domain === 'youtube.com' ?
av1VideoFormats && opusAudioFormats ?
av1VideoFormats.map((av1Format) => {
const opusFormat = av1Format.format_note.startsWith('144p') || av1Format.format_note.startsWith('240p') ? opusAudioFormats[opusAudioFormats.length - 1] : opusAudioFormats[0]
return {
...av1Format,
format: `${av1Format.format}+${opusFormat?.format}`,
format_id: `${av1Format.format_id}+${opusFormat?.format_id}`,
format_note: `${av1Format.format_note}+${opusFormat?.format_note}`,
filesize_approx: av1Format.filesize_approx && opusFormat.filesize_approx ? av1Format.filesize_approx + opusFormat.filesize_approx : null,
acodec: opusFormat?.acodec,
audio_ext: opusFormat.audio_ext,
ext: 'webm',
tbr: av1Format.tbr && opusFormat.tbr ? av1Format.tbr + opusFormat.tbr : null,
};
})
: []
: [];
const allFilteredFormats = [...(audioOnlyFormats || []), ...(videoOnlyFormats || []), ...(combinedFormats || []), ...(qualityPresetFormats || [])];
const selectedFormat = (() => {
if (videoMetadata?._type === 'video') {
if (selectedDownloadFormat === 'best') {
return videoMetadata?.requested_downloads[0];
}
return allFilteredFormats.find(
(format) => format.format_id === selectedDownloadFormat
);
} else if (videoMetadata?._type === 'playlist') {
if (selectedDownloadFormat === 'best') {
return getMergedBestFormat(videoMetadata.entries, selectedPlaylistVideos);
}
return allFilteredFormats.find(
(format) => format.format_id === selectedDownloadFormat
);
}
})();
const selectedFormatFileType = determineFileType(selectedFormat?.vcodec, selectedFormat?.acodec);
const selectedVideoFormat = (() => {
if (videoMetadata?._type === 'video') {
return allFilteredFormats.find(
(format) => format.format_id === selectedCombinableVideoFormat
);
} else if (videoMetadata?._type === 'playlist') {
return allFilteredFormats.find(
(format) => format.format_id === selectedCombinableVideoFormat
);
}
})();
const selectedAudioFormats = (() => {
if (videoMetadata?._type === 'video') {
return allFilteredFormats.filter(
(format) => selectedCombinableAudioFormats.includes(format.format_id)
);
} else if (videoMetadata?._type === 'playlist') {
return allFilteredFormats.filter(
(format) => selectedCombinableAudioFormats.includes(format.format_id)
);
}
})();
const subtitles = (() => {
if (videoMetadata?._type === 'video') {
return videoMetadata?.subtitles || {};
} else if (videoMetadata?._type === 'playlist') {
return getCommonSubtitles(videoMetadata.entries, selectedPlaylistVideos);
}
return {};
})();
const filteredSubtitles = Object.fromEntries(Object.entries(subtitles).filter(([key]) => key !== 'live_chat'));
const autoSubtitles = (() => {
if (videoMetadata?._type === 'video') {
return videoMetadata?.automatic_captions || {};
} else if (videoMetadata?._type === 'playlist') {
return getCommonAutoSubtitles(videoMetadata.entries, selectedPlaylistVideos);
}
return {};
})();
const originalAutoSubtitles = Object.fromEntries(Object.entries(autoSubtitles).filter(([key]) => key.endsWith('-orig')));
const subtitleLanguages = Object.keys(filteredSubtitles).map(langCode => ({
code: langCode,
lang: filteredSubtitles[langCode][0].name || langCode
}));
const autoSubtitleLanguages = Object.keys(originalAutoSubtitles).map(langCode => ({
code: langCode,
lang: originalAutoSubtitles[langCode][0].name + ' [auto-generated]' || langCode + ' [auto-generated]'
}));
const allSubtitleLanguages = Array.from(new Set([...subtitleLanguages, ...autoSubtitleLanguages].map(lang => lang.code))).map(code => {
return [...subtitleLanguages, ...autoSubtitleLanguages].find(lang => lang.code === code)!;
});
const searchForm = useForm<z.infer<typeof searchFormSchema>>({
resolver: zodResolver(searchFormSchema),
defaultValues: {
url: videoUrl,
},
mode: "onChange",
})
const watchedUrl = searchForm.watch("url");
const { errors: searchFormErrors } = searchForm.formState;
function handleSearchSubmit(values: z.infer<typeof searchFormSchema>) {
setVideoMetadata(null);
setSearchPid(null);
setShowSearchError(true);
setIsMetadataLoading(true);
setSelectedDownloadFormat('best');
setSelectedCombinableVideoFormat('');
setSelectedCombinableAudioFormats([]);
setSelectedSubtitles([]);
setSelectedPlaylistVideos(["1"]);
resetDownloadConfiguration();
fetchVideoMetadata({ url: 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)) {
const showSearchError = useCurrentVideoMetadataStore.getState().showSearchError;
if (showSearchError) {
toast.error("Oops! No results found", {
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.",
});
}
}
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);
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(() => {
if (watchedUrl !== videoUrl) {
setVideoUrl(watchedUrl);
}
}, [watchedUrl, videoUrl, setVideoUrl]);
useEffect(() => {
const handleAutoSubmitRequest = async () => {
// Update form and state when requestedUrl changes
if (requestedUrl && requestedUrl !== searchForm.getValues("url") && !isMetadataLoading) {
searchForm.setValue("url", requestedUrl);
setVideoUrl(requestedUrl);
}
// Auto-submit the form if the flag is set
if (autoSubmitSearch && requestedUrl) {
if (!isMetadataLoading) {
// trigger a validation check on the URL field first then get the result
await searchForm.trigger("url");
const isValidUrl = !searchForm.getFieldState("url").invalid;
if (isValidUrl) {
// Reset the flag first to prevent loops
setAutoSubmitSearch(false);
// Submit the form with a small delay to ensure UI is ready
setTimeout(() => {
handleSearchSubmit({ url: requestedUrl });
setRequestedUrl('');
}, 300);
} else {
// If URL is invalid, just reset the flag
setAutoSubmitSearch(false);
setRequestedUrl('');
toast.error("Invalid URL", {
description: "The provided URL is not valid.",
});
}
} else {
// If metadata is loading, just reset the flag
setAutoSubmitSearch(false);
setRequestedUrl('');
toast.info("Search in progress", {
description: "There's a search in progress, Please try again later.",
});
}
} else {
// If auto-submit is not set, reset the flag
setAutoSubmitSearch(false);
setRequestedUrl('');
}
}
handleAutoSubmitRequest();
}, [requestedUrl, autoSubmitSearch, isMetadataLoading]);
useEffect(() => {
const updateTheme = async () => {
setTheme(appTheme, appColorScheme);
}
updateTheme().catch(console.error);
}, [appTheme, appColorScheme]);
return (
<div className="container mx-auto p-4 space-y-4 relative" ref={containerRef}>
<Card className="gap-4">
<CardHeader>
<CardTitle className="flex items-center"><PackageSearch className="size-5 mr-3 stroke-primary" />{config.appName} Search</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Form {...searchForm}>
<form onSubmit={searchForm.handleSubmit(handleSearchSubmit)} className="flex gap-2 w-full" autoComplete="off">
<FormField
control={searchForm.control}
name="url"
disabled={isMetadataLoading}
render={({ field }) => (
<FormItem className="grow">
<FormControl>
<Input
className="focus-visible:ring-0"
placeholder="Enter Video/Playlist URL to Download"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isMetadataLoading && (
<Button
type="button"
variant="destructive"
size="icon"
disabled={!isMetadataLoading}
onClick={() => cancelSearch(searchPid)}
>
<X className="size-4" />
</Button>
)}
{!isMetadataLoading && !videoUrl && (
<Button
type="button"
variant="ghost"
size="icon"
className="border border-input"
disabled={isMetadataLoading}
onClick={async () => {
const text = await readText();
if (text) {
searchForm.setValue("url", text);
setVideoUrl(text);
}
}}
>
<Clipboard className="size-4" />
</Button>
)}
{!isMetadataLoading && videoUrl && (
<Button
type="button"
variant="ghost"
size="icon"
className="border border-input"
disabled={isMetadataLoading}
onClick={() => {
searchForm.setValue("url", '');
setVideoUrl('');
}}
>
<X className="size-4" />
</Button>
)}
<Button
type="submit"
disabled={!videoUrl || Object.keys(searchFormErrors).length > 0 || isMetadataLoading}
>
{isMetadataLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Searching
</>
) : (
'Search'
)}
</Button>
</form>
</Form>
</CardContent>
</Card>
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'video' && (
<VideoDownloader
videoMetadata={videoMetadata}
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
combinedFormats={combinedFormats}
qualityPresetFormats={qualityPresetFormats}
subtitleLanguages={allSubtitleLanguages}
/>
)}
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'playlist' && (
<PlaylistDownloader
videoMetadata={videoMetadata}
audioOnlyFormats={audioOnlyFormats}
videoOnlyFormats={videoOnlyFormats}
combinedFormats={combinedFormats}
qualityPresetFormats={qualityPresetFormats}
subtitleLanguages={allSubtitleLanguages}
/>
)}
{!isMetadataLoading && videoMetadata && selectedDownloadFormat && (
<BottomBar
videoMetadata={videoMetadata}
selectedFormat={selectedFormat}
selectedFormatFileType={selectedFormatFileType}
selectedVideoFormat={selectedVideoFormat}
selectedAudioFormats={selectedAudioFormats}
containerRef={containerRef}
/>
)}
</div>
);
}