(feat): introduced combine download mode and other tweaks and improvements

This commit is contained in:
2025-07-12 23:55:16 +05:30
parent f2589d78b7
commit ad6f1938de
6 changed files with 536 additions and 253 deletions

View File

@@ -9,7 +9,7 @@ import { useToast } from "@/hooks/use-toast";
import { useAppContext } from "@/providers/appContextProvider";
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
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 } from "lucide-react";
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon } from "lucide-react";
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
import { useEffect, useRef } from "react";
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
@@ -21,6 +21,8 @@ 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const searchFormSchema = z.object({
url: z.string().min(1, { message: "URL is required" })
@@ -42,12 +44,18 @@ export default function DownloaderPage() {
const setRequestedUrl = useCurrentVideoMetadataStore((state) => state.setRequestedUrl);
const setAutoSubmitSearch = useCurrentVideoMetadataStore((state) => state.setAutoSubmitSearch);
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
const selctedDownloadFormat = useDownloaderPageStatesStore((state) => state.selctedDownloadFormat);
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
const selectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormat);
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
const setSelctedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelctedDownloadFormat);
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
const setSelectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormat);
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
const setSelectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideoIndex);
@@ -82,22 +90,44 @@ export default function DownloaderPage() {
const allFilteredFormats = [...(audioOnlyFormats || []), ...(videoOnlyFormats || []), ...(combinedFormats || []), ...(qualityPresetFormats || [])];
const selectedFormat = (() => {
if (videoMetadata?._type === 'video') {
if (selctedDownloadFormat === 'best') {
if (selectedDownloadFormat === 'best') {
return videoMetadata?.requested_downloads[0];
}
return allFilteredFormats.find(
(format) => format.format_id === selctedDownloadFormat
(format) => format.format_id === selectedDownloadFormat
);
} else if (videoMetadata?._type === 'playlist') {
if (selctedDownloadFormat === 'best') {
if (selectedDownloadFormat === 'best') {
return videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0];
}
return allFilteredFormats.find(
(format) => format.format_id === selctedDownloadFormat
(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 selectedAudioFormat = (() => {
if (videoMetadata?._type === 'video') {
return allFilteredFormats.find(
(format) => format.format_id === selectedCombinableAudioFormat
);
} else if (videoMetadata?._type === 'playlist') {
return allFilteredFormats.find(
(format) => format.format_id === selectedCombinableAudioFormat
);
}
})();
const subtitles = videoMetadata?._type === 'video' ? (videoMetadata?.subtitles || {}) : videoMetadata?._type === 'playlist' ? (videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].subtitles || {}) : {};
const subtitleLanguages = Object.keys(subtitles).map(langCode => ({
@@ -109,7 +139,16 @@ export default function DownloaderPage() {
const bottomBarRef = useRef<HTMLDivElement>(null);
let selectedFormatExtensionMsg = 'Auto - unknown';
if (selectedFormat?.ext) {
if (activeDownloadModeTab === 'combine') {
if (videoFormat !== 'auto') {
selectedFormatExtensionMsg = `Combined - ${videoFormat.toUpperCase()}`;
}
else if (selectedAudioFormat?.ext && selectedVideoFormat?.ext) {
selectedFormatExtensionMsg = `Combined - ${selectedVideoFormat.ext.toUpperCase()} + ${selectedAudioFormat.ext.toUpperCase()}`;
} else {
selectedFormatExtensionMsg = `Combined - unknown`;
}
} else if (selectedFormat?.ext) {
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && videoFormat !== 'auto') {
selectedFormatExtensionMsg = `Forced - ${videoFormat.toUpperCase()}`;
} else if (selectedFormatFileType === 'audio' && audioFormat !== 'auto') {
@@ -119,6 +158,42 @@ export default function DownloaderPage() {
}
}
let selectedFormatResolutionMsg = 'unknown';
if (activeDownloadModeTab === 'combine') {
selectedFormatResolutionMsg = `${selectedVideoFormat?.resolution ?? 'unknown'} + ${selectedAudioFormat?.tbr ? formatBitrate(selectedAudioFormat.tbr) : 'unknown'}`;
} else if (selectedFormat?.resolution) {
selectedFormatResolutionMsg = selectedFormat.resolution;
}
let selectedFormatDynamicRangeMsg = '';
if (activeDownloadModeTab === 'combine') {
selectedFormatDynamicRangeMsg = selectedVideoFormat?.dynamic_range && selectedVideoFormat.dynamic_range !== 'SDR' ? selectedVideoFormat.dynamic_range : '';
} else if (selectedFormat?.dynamic_range && selectedFormat.dynamic_range !== 'SDR') {
selectedFormatDynamicRangeMsg = selectedFormat.dynamic_range;
}
let selectedFormatFileSizeMsg = 'unknown filesize';
if (activeDownloadModeTab === 'combine') {
selectedFormatFileSizeMsg = selectedVideoFormat?.filesize_approx && selectedAudioFormat?.filesize_approx ? formatFileSize(selectedVideoFormat.filesize_approx + selectedAudioFormat.filesize_approx) : 'unknown filesize';
} else if (selectedFormat?.filesize_approx) {
selectedFormatFileSizeMsg = formatFileSize(selectedFormat.filesize_approx);
}
let selectedFormatFinalMsg = '';
if (activeDownloadModeTab === 'combine') {
if (selectedCombinableVideoFormat && selectedCombinableAudioFormat) {
selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''}${selectedFormatFileSizeMsg}`;
} else {
selectedFormatFinalMsg = `Choose a video and audio stream to combine`;
}
} else {
if (selectedFormat) {
selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''}${selectedFormatFileSizeMsg}`;
} else {
selectedFormatFinalMsg = `Choose a stream to download`;
}
}
const searchForm = useForm<z.infer<typeof searchFormSchema>>({
resolver: zodResolver(searchFormSchema),
defaultValues: {
@@ -131,7 +206,9 @@ export default function DownloaderPage() {
function handleSearchSubmit(values: z.infer<typeof searchFormSchema>) {
setVideoMetadata(null);
setIsMetadataLoading(true);
setSelctedDownloadFormat('best');
setSelectedDownloadFormat('best');
setSelectedCombinableVideoFormat('');
setSelectedCombinableAudioFormat('');
setSelectedSubtitles([]);
setSelectedPlaylistVideoIndex('1');
fetchVideoMetadata(values.url).then((metadata) => {
@@ -281,11 +358,11 @@ export default function DownloaderPage() {
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'video' && ( // === Single Video ===
<div className="flex">
<div className="flex flex-col w-[55%] border-r border-border pr-4">
<h3 className="text-sm mb-4 flex items-center gap-2">
<h3 className="text-sm mb-4 mt-2 flex items-center gap-2">
<Info className="w-4 h-4" />
<span>Metadata</span>
</h3>
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
<AspectRatio ratio={16 / 9} className={clsx("w-full rounded-lg overflow-hidden mb-2 border border-border", videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "relative")}>
<ProxyImage src={videoMetadata.thumbnail} alt="thumbnail" className={clsx(videoMetadata.aspect_ratio && videoMetadata.aspect_ratio === 0.56 && "absolute h-full w-auto top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2")} />
</AspectRatio>
@@ -323,127 +400,222 @@ export default function DownloaderPage() {
<Info className="w-3 h-3 mr-2" />
<span className="text-xs">Extracted from {videoMetadata.extractor ? videoMetadata.extractor.charAt(0).toUpperCase() + videoMetadata.extractor.slice(1) : 'Unknown'}</span>
</div>
<div className="spacer mb-14"></div>
<div className="spacer mb-10"></div>
</div>
</div>
<div className="flex flex-col w-full pl-4">
<h3 className="text-sm mb-4 flex items-center gap-2">
<DownloadCloud className="w-4 h-4" />
<span>Download Options</span>
</h3>
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
{subtitles && !isObjEmpty(subtitles) && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}>
{lang.lang}
</ToggleGroupItem>
))}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
value={selctedDownloadFormat}
onValueChange={(value) => {
setSelctedDownloadFormat(value);
const currentlySelectedFormat = value === 'best' ? videoMetadata?.requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
setSelectedSubtitles([]);
}
}}
>
<p className="text-xs">Suggested</p>
<div className="">
<FormatSelectionGroupItem
key="best"
value="best"
format={videoMetadata.requested_downloads[0]}
/>
<Tabs
className=""
value={activeDownloadModeTab}
onValueChange={(tab) => setActiveDownloadModeTab(tab)}
>
<div className="flex items-center justify-between">
<h3 className="text-sm flex items-center gap-2">
<DownloadCloud className="w-4 h-4" />
<span>Download Options</span>
</h3>
<TabsList>
<TabsTrigger value="selective">Selective</TabsTrigger>
<TabsTrigger value="combine">Combine</TabsTrigger>
</TabsList>
</div>
<TabsContent value="selective">
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
{subtitles && !isObjEmpty(subtitles) && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}>
{lang.lang}
</ToggleGroupItem>
))}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
value={selectedDownloadFormat}
onValueChange={(value) => {
setSelectedDownloadFormat(value);
const currentlySelectedFormat = value === 'best' ? videoMetadata?.requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
setSelectedSubtitles([]);
}
}}
>
<p className="text-xs">Suggested</p>
<div className="">
<FormatSelectionGroupItem
key="best"
value="best"
format={videoMetadata.requested_downloads[0]}
/>
</div>
{qualityPresetFormats && qualityPresetFormats.length > 0 && (
<>
<p className="text-xs">Quality Presets</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{qualityPresetFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video {videoOnlyFormats.every(format => format.acodec === 'none') ? '(no audio)' : ''}</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{combinedFormats && combinedFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{combinedFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<div className="spacer mb-10"></div>
</div>
{qualityPresetFormats && qualityPresetFormats.length > 0 && (
<>
<p className="text-xs">Quality Presets</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{qualityPresetFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video {videoOnlyFormats.every(format => format.acodec === 'none') ? '(no audio)' : ''}</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{combinedFormats && combinedFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{combinedFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<div className="spacer mb-14"></div>
</div>
</TabsContent>
<TabsContent value="combine">
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && subtitles && !isObjEmpty(subtitles) && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}>
{lang.lang}
</ToggleGroupItem>
))}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
className="mb-2"
value={selectedCombinableAudioFormat}
onValueChange={(value) => {
setSelectedCombinableAudioFormat(value);
}}
>
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<FormatSelectionGroup
value={selectedCombinableVideoFormat}
onValueChange={(value) => {
setSelectedCombinableVideoFormat(value);
}}
>
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
{(!videoOnlyFormats || videoOnlyFormats.length === 0 || !audioOnlyFormats || audioOnlyFormats.length === 0) && (
<Alert>
<AlertCircleIcon />
<AlertTitle>Unable to use Combine Mode!</AlertTitle>
<AlertDescription>
Cannot use combine mode for this video as it does not have both audio and video formats available. Use Selective Mode or try another video.
</AlertDescription>
</Alert>
)}
<div className="spacer mb-10"></div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
)}
{!isMetadataLoading && videoMetadata && videoMetadata._type === 'playlist' && ( // === Playlists ===
<div className="flex">
<div className="flex flex-col w-[55%] border-r border-border pr-4">
<h3 className="text-sm mb-4 flex items-center gap-2">
<h3 className="text-sm mb-4 mt-2 flex items-center gap-2">
<ListVideo className="w-4 h-4" />
<span>Playlist ({videoMetadata.entries[0].n_entries})</span>
</h3>
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
<h2 className="mb-1">{videoMetadata.entries[0].playlist_title ? videoMetadata.entries[0].playlist_title : 'UNTITLED'}</h2>
<p className="text-muted-foreground text-xs mb-4">{videoMetadata.entries[0].playlist_channel || videoMetadata.entries[0].playlist_uploader || 'unknown'}</p>
{/* <PlaylistToggleGroup
@@ -465,8 +637,10 @@ export default function DownloaderPage() {
value={selectedPlaylistVideoIndex}
onValueChange={(value) => {
setSelectedPlaylistVideoIndex(value);
setSelctedDownloadFormat('best');
setSelectedDownloadFormat('best');
setSelectedSubtitles([]);
setSelectedCombinableVideoFormat('');
setSelectedCombinableAudioFormat('');
}}
>
{videoMetadata.entries.map((entry) => entry ? (
@@ -481,120 +655,215 @@ export default function DownloaderPage() {
<Info className="w-3 h-3 mr-2" />
<span className="text-xs">Extracted from {videoMetadata.entries[0].extractor ? videoMetadata.entries[0].extractor.charAt(0).toUpperCase() + videoMetadata.entries[0].extractor.slice(1) : 'Unknown'}</span>
</div>
<div className="spacer mb-14"></div>
<div className="spacer mb-10"></div>
</div>
</div>
<div className="flex flex-col w-full pl-4">
<h3 className="text-sm mb-4 flex items-center gap-2">
<DownloadCloud className="w-4 h-4" />
<span>Download Options</span>
</h3>
<div className="flex flex-col overflow-y-scroll max-h-[53vh] no-scrollbar">
{subtitles && !isObjEmpty(subtitles) && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}>
{lang.lang}
</ToggleGroupItem>
))}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
value={selctedDownloadFormat}
onValueChange={(value) => {
setSelctedDownloadFormat(value);
const currentlySelectedFormat = value === 'best' ? videoMetadata?.entries[Number(value) - 1].requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
setSelectedSubtitles([]);
}
}}
>
<p className="text-xs">Suggested</p>
<div className="">
<FormatSelectionGroupItem
key="best"
value="best"
format={videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0]}
/>
<Tabs
className=""
value={activeDownloadModeTab}
onValueChange={(tab) => setActiveDownloadModeTab(tab)}
>
<div className="flex items-center justify-between">
<h3 className="text-sm flex items-center gap-2">
<DownloadCloud className="w-4 h-4" />
<span>Download Options</span>
</h3>
<TabsList>
<TabsTrigger value="selective">Selective</TabsTrigger>
<TabsTrigger value="combine">Combine</TabsTrigger>
</TabsList>
</div>
<TabsContent value="selective">
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
{subtitles && !isObjEmpty(subtitles) && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}>
{lang.lang}
</ToggleGroupItem>
))}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
value={selectedDownloadFormat}
onValueChange={(value) => {
setSelectedDownloadFormat(value);
const currentlySelectedFormat = value === 'best' ? videoMetadata?.entries[Number(value) - 1].requested_downloads[0] : allFilteredFormats.find((format) => format.format_id === value);
if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
setSelectedSubtitles([]);
}
}}
>
<p className="text-xs">Suggested</p>
<div className="">
<FormatSelectionGroupItem
key="best"
value="best"
format={videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0]}
/>
</div>
{qualityPresetFormats && qualityPresetFormats.length > 0 && (
<>
<p className="text-xs">Quality Presets</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{qualityPresetFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video {videoOnlyFormats.every(format => format.acodec === 'none') ? '(no audio)' : ''}</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{combinedFormats && combinedFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{combinedFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<div className="spacer mb-10"></div>
</div>
{qualityPresetFormats && qualityPresetFormats.length > 0 && (
<>
<p className="text-xs">Quality Presets</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{qualityPresetFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video {videoOnlyFormats.every(format => format.acodec === 'none') ? '(no audio)' : ''}</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
{combinedFormats && combinedFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{combinedFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<div className="spacer mb-14"></div>
</div>
</TabsContent>
<TabsContent value="combine">
<div className="flex flex-col overflow-y-scroll max-h-[50vh] no-scrollbar">
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && subtitles && !isObjEmpty(subtitles) && (
<ToggleGroup
type="multiple"
variant="outline"
className="flex flex-col items-start gap-2 mb-2"
value={selectedSubtitles}
onValueChange={(value) => setSelectedSubtitles(value)}
disabled={selectedFormat?.ext !== 'mp4' && selectedFormat?.ext !== 'mkv' && selectedFormat?.ext !== 'webm'}
>
<p className="text-xs">Subtitle Languages</p>
<div className="flex gap-2 flex-wrap items-center">
{subtitleLanguages.map((lang) => (
<ToggleGroupItem
className="text-xs text-nowrap border-2 data-[state=on]:border-2 data-[state=on]:border-primary data-[state=on]:bg-muted/70 hover:bg-muted/70"
value={lang.code}
size="sm"
aria-label={lang.lang}
key={lang.code}>
{lang.lang}
</ToggleGroupItem>
))}
</div>
</ToggleGroup>
)}
<FormatSelectionGroup
className="mb-2"
value={selectedCombinableAudioFormat}
onValueChange={(value) => {
setSelectedCombinableAudioFormat(value);
}}
>
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
<>
<p className="text-xs">Audio</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{audioOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
<FormatSelectionGroup
value={selectedCombinableVideoFormat}
onValueChange={(value) => {
setSelectedCombinableVideoFormat(value);
}}
>
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
<>
<p className="text-xs">Video</p>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
{videoOnlyFormats.map((format) => (
<FormatSelectionGroupItem
key={format.format_id}
value={format.format_id}
format={format}
/>
))}
</div>
</>
)}
</FormatSelectionGroup>
{(!videoOnlyFormats || videoOnlyFormats.length === 0 || !audioOnlyFormats || audioOnlyFormats.length === 0) && (
<Alert>
<AlertCircleIcon />
<AlertTitle>Unable to use Combine Mode!</AlertTitle>
<AlertDescription>
Cannot use combine mode for this video as it does not have both audio and video formats available. Use Selective Mode or try another video.
</AlertDescription>
</Alert>
)}
<div className="spacer mb-10"></div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
)}
{!isMetadataLoading && videoMetadata && selctedDownloadFormat && ( // === Bottom Bar ===
{!isMetadataLoading && videoMetadata && selectedDownloadFormat && ( // === Bottom Bar ===
<div className="flex justify-between items-center gap-2 fixed bottom-0 right-0 p-4 w-full bg-background rounded-t-lg border-t border-border z-20" ref={bottomBarRef}>
<div className="flex items-center gap-4">
<div className="flex justify-center items-center p-3 rounded-md border border-border">
@@ -610,7 +879,7 @@ export default function DownloaderPage() {
</div>
<div className="flex flex-col gap-1">
<span className="text-sm text-nowrap max-w-[30rem] xl:max-w-[50rem] overflow-hidden text-ellipsis">{videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].title : 'Unknown' }</span>
<span className="text-xs text-muted-foreground">{selectedFormatExtensionMsg} ({selectedFormat?.resolution ? selectedFormat.resolution : 'unknown'}) {selectedFormat?.dynamic_range && selectedFormat.dynamic_range !== 'SDR' ? selectedFormat.dynamic_range : null } {selectedSubtitles.length > 0 ? `• ESUB` : null} {selectedFormat?.filesize_approx ? formatFileSize(selectedFormat?.filesize_approx) : 'unknown filesize'}</span>
<span className="text-xs text-muted-foreground">{selectedFormatFinalMsg}</span>
</div>
</div>
<Button
@@ -620,7 +889,7 @@ export default function DownloaderPage() {
if (videoMetadata._type === 'playlist') {
await startDownload(
videoMetadata.original_url,
selctedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selctedDownloadFormat,
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat,
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
undefined,
selectedPlaylistVideoIndex
@@ -628,7 +897,7 @@ export default function DownloaderPage() {
} else if (videoMetadata._type === 'video') {
await startDownload(
videoMetadata.webpage_url,
selctedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selctedDownloadFormat,
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
);
}
@@ -647,12 +916,12 @@ export default function DownloaderPage() {
setIsStartingDownload(false);
}
}}
disabled={isStartingDownload}
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat))}
>
{isStartingDownload ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Starting
Starting Download
</>
) : (
'Start Download'