From 1eb23eb035bd6dd73f15f43e0d525c24d141a10a Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Thu, 15 Jan 2026 15:22:43 +0530 Subject: [PATCH] feat: added support for full-playlist/selective-batch downloading #9 --- src-tauri/src/migrations.rs | 15 +- .../custom/playlistSelectionGroup.tsx | 2 +- src/components/custom/playlistToggleGroup.tsx | 109 ++------ src/components/pages/downloader/bottomBar.tsx | 25 +- .../pages/downloader/playlistDownloader.tsx | 136 ++++++---- .../pages/downloader/videoDownloader.tsx | 54 ++-- .../pages/library/completedDownloads.tsx | 131 ++++++--- .../pages/library/incompleteDownloads.tsx | 36 ++- .../pages/settings/applicationSettings.tsx | 6 +- src/helpers/use-downloader.ts | 137 ++++++++-- src/pages/downloader.tsx | 47 +++- src/providers/appContextProvider.tsx | 5 +- src/services/database.ts | 83 ++++-- src/services/mutations.ts | 9 +- src/services/store.ts | 4 +- src/types/download.ts | 7 +- src/types/store.ts | 4 +- src/utils.ts | 248 +++++++++++++++++- 18 files changed, 768 insertions(+), 290 deletions(-) diff --git a/src-tauri/src/migrations.rs b/src-tauri/src/migrations.rs index cb81c53..fc2fb9e 100644 --- a/src-tauri/src/migrations.rs +++ b/src-tauri/src/migrations.rs @@ -163,7 +163,7 @@ pub fn get_migrations() -> Vec { subtitle_id TEXT, queue_index INTEGER, playlist_id TEXT, - playlist_index INTEGER, + playlist_indices TEXT, resolution TEXT, ext TEXT, abr REAL, @@ -173,6 +173,7 @@ pub fn get_migrations() -> Vec { dynamic_range TEXT, process_id INTEGER, status TEXT, + item TEXT, progress REAL, total INTEGER, downloaded INTEGER, @@ -199,13 +200,17 @@ pub fn get_migrations() -> Vec { -- Copy all data from original table to temporary table with default values for new columns INSERT INTO downloads_temp SELECT id, download_id, download_status, video_id, format_id, subtitle_id, - queue_index, playlist_id, playlist_index, resolution, ext, abr, vbr, - acodec, vcodec, dynamic_range, process_id, status, progress, total, - downloaded, speed, eta, filepath, filetype, filesize, + queue_index, playlist_id, + CAST(playlist_index AS TEXT), -- Convert INTEGER playlist_index to TEXT playlist_indices + resolution, ext, abr, vbr, + acodec, vcodec, dynamic_range, process_id, status, + CASE WHEN playlist_id IS NOT NULL THEN '1/1' ELSE NULL END, -- item + progress, total, downloaded, speed, eta, + filepath, filetype, filesize, output_format, embed_metadata, embed_thumbnail, - 0, -- square_crop_thumbnail + 0, -- square_crop_thumbnail sponsorblock_remove, sponsorblock_mark, use_aria2, custom_command, queue_config, created_at, updated_at FROM downloads; diff --git a/src/components/custom/playlistSelectionGroup.tsx b/src/components/custom/playlistSelectionGroup.tsx index f34efe0..8c9f1cd 100644 --- a/src/components/custom/playlistSelectionGroup.tsx +++ b/src/components/custom/playlistSelectionGroup.tsx @@ -36,7 +36,7 @@ const PlaylistSelectionGroupItem = React.forwardRef< ref={ref} className={cn( "relative w-full rounded-lg border-2 border-border bg-background p-2 shadow-sm transition-all", - "data-[state=checked]:border-primary data-[state=checked]:border-2 data-[state=checked]:bg-primary/10", + "data-[state=checked]:border-primary data-[state=checked]:bg-primary/10", "hover:bg-muted/70", "disabled:cursor-not-allowed disabled:opacity-50", className diff --git a/src/components/custom/playlistToggleGroup.tsx b/src/components/custom/playlistToggleGroup.tsx index 3cc89bf..17725e7 100644 --- a/src/components/custom/playlistToggleGroup.tsx +++ b/src/components/custom/playlistToggleGroup.tsx @@ -3,7 +3,6 @@ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; import { type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; import { toggleVariants } from "@/components/ui/toggle"; -import { Checkbox } from "@/components/ui/checkbox"; import { AspectRatio } from "@/components/ui/aspect-ratio"; import { ProxyImage } from "@/components/custom/proxyImage"; import { Clock } from "lucide-react"; @@ -11,7 +10,6 @@ import clsx from "clsx"; import { formatDurationString } from "@/utils"; import { RawVideoInfo } from "@/types/video"; -// Create a context to share toggle group props const PlaylistToggleGroupContext = React.createContext< VariantProps & { toggleType?: "single" | "multiple" } >({ @@ -20,19 +18,16 @@ const PlaylistToggleGroupContext = React.createContext< toggleType: "multiple", }); -// Helper type for the PlaylistToggleGroup -type PlaylistToggleGroupProps = - | (Omit, "type"> & +type PlaylistToggleGroupProps = + | (Omit, "type"> & VariantProps & { type: "single", value?: string, onValueChange?: (value: string) => void }) - | (Omit, "type"> & + | (Omit, "type"> & VariantProps & { type: "multiple", value?: string[], onValueChange?: (value: string[]) => void }); -// Main PlaylistToggleGroup component with proper type handling export const PlaylistToggleGroup = React.forwardRef< - React.ElementRef, + React.ComponentRef, PlaylistToggleGroupProps >(({ className, variant, size, children, type = "multiple", ...props }, ref) => { - // Pass props based on the type if (type === "single") { return ( ); } - + return ( , + React.ComponentRef, React.ComponentPropsWithoutRef & VariantProps & { video: RawVideoInfo; } >(({ className, children, variant, size, video, value, ...props }, ref) => { - const [isHovered, setIsHovered] = React.useState(false); - const [checked, setChecked] = React.useState(false); - - // Instead of a ref + useEffect approach - const [itemElement, setItemElement] = React.useState(null); - - // Handle checkbox click separately by simulating a click on the parent item - const handleCheckboxClick = (e: React.MouseEvent) => { - e.stopPropagation(); - - // Manually trigger the item's click to toggle selection - if (itemElement) { - // This simulates a click on the toggle item itself - itemElement.click(); - } - }; - - // Use an effect that triggers when itemElement changes - React.useEffect(() => { - if (itemElement) { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === 'data-state') { - setChecked(itemElement.getAttribute('data-state') === 'on'); - } - }); - }); - - setChecked(itemElement.getAttribute('data-state') === 'on'); - observer.observe(itemElement, { attributes: true }); - - return () => observer.disconnect(); - } - }, [itemElement]); - return ( { - // Handle both our ref and the forwarded ref - if (typeof ref === 'function') { - ref(el); - } else if (ref) { - ref.current = el; - } - setItemElement(el); - }} + ref={ref} className={cn( - "flex w-full p-2 rounded-md transition-colors border-2 border-border", - "hover:bg-muted/50 data-[state=on]:bg-muted/70", + "flex w-full p-2 rounded-lg transition-colors border-2 border-border", + "hover:bg-muted/70 data-[state=on]:bg-primary/10", "data-[state=on]:border-primary", className )} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} value={value} {...props} > -
-
- -
-
- + -
- -
-

{video.title}

-

{video.channel || video.uploader || 'unknown'}

+ +
+

{video.title}

+

{video.creator || video.channel || video.uploader || 'unknown'}

@@ -174,4 +111,4 @@ export const PlaylistToggleGroupItem = React.forwardRef< ); }); -PlaylistToggleGroupItem.displayName = "PlaylistToggleGroupItem"; \ No newline at end of file +PlaylistToggleGroupItem.displayName = "PlaylistToggleGroupItem"; diff --git a/src/components/pages/downloader/bottomBar.tsx b/src/components/pages/downloader/bottomBar.tsx index 8707c46..cbe04bf 100644 --- a/src/components/pages/downloader/bottomBar.tsx +++ b/src/components/pages/downloader/bottomBar.tsx @@ -6,7 +6,6 @@ import { formatBitrate, formatFileSize } from "@/utils"; import { Loader2, Music, Video, File, AlertCircleIcon, Settings2 } from "lucide-react"; import { useEffect, useRef } from "react"; import { RawVideoInfo, VideoFormat } from "@/types/video"; -// import { PlaylistToggleGroup, PlaylistToggleGroupItem } from "@/components/custom/playlistToggleGroup"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; @@ -282,17 +281,21 @@ export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileTyp 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 selectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideos); const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration); const setActiveDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadConfigurationTab); const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload); const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format); const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format); + const useSponsorblock = useSettingsPageStatesStore(state => state.settings.use_sponsorblock); const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); const bottomBarRef = useRef(null); + const isPlaylist = videoMetadata._type === 'playlist'; + const isMultiplePlaylistItems = isPlaylist && selectedPlaylistVideos.length > 1; + let selectedFormatExtensionMsg = 'Auto - unknown'; if (activeDownloadModeTab === 'combine') { if (downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') { @@ -327,8 +330,8 @@ export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileTyp 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 = selectedVideoFormat?.dynamic_range && selectedVideoFormat.dynamic_range !== 'SDR' && selectedVideoFormat.dynamic_range !== 'auto' ? selectedVideoFormat.dynamic_range : ''; + } else if (selectedFormat?.dynamic_range && selectedFormat.dynamic_range !== 'SDR' && selectedFormat.dynamic_range !== 'auto') { selectedFormatDynamicRangeMsg = selectedFormat.dynamic_range; } @@ -342,13 +345,13 @@ export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileTyp let selectedFormatFinalMsg = ''; if (activeDownloadModeTab === 'combine') { if (selectedCombinableVideoFormat && selectedCombinableAudioFormat) { - selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} • ${selectedFormatFileSizeMsg}`; + selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} ${useSponsorblock || (downloadConfiguration.sponsorblock && downloadConfiguration.sponsorblock !== 'auto') ? `• SPBLOCK` : ''} • ${selectedFormatFileSizeMsg}`; } else { selectedFormatFinalMsg = `Choose a video and audio stream to combine`; } } else { if (selectedFormat) { - selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} • ${selectedFormatFileSizeMsg}`; + selectedFormatFinalMsg = `${selectedFormatExtensionMsg} (${selectedFormatResolutionMsg}) ${selectedFormatDynamicRangeMsg} ${selectedSubtitles.length > 0 ? `• ESUB` : ''} ${useSponsorblock || (downloadConfiguration.sponsorblock && downloadConfiguration.sponsorblock !== 'auto') ? `• SPBLOCK` : ''} • ${selectedFormatFileSizeMsg}`; } else { selectedFormatFinalMsg = `Choose a stream to download`; } @@ -397,7 +400,7 @@ export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileTyp )}
- {videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].title : 'Unknown' } + {videoMetadata._type === 'video' ? videoMetadata.title : videoMetadata._type === 'playlist' ? selectedPlaylistVideos.length === 1 ? videoMetadata.entries[Number(selectedPlaylistVideos[0]) - 1].title : `${selectedPlaylistVideos.length} Items` : 'Unknown' } {selectedFormatFinalMsg}
@@ -410,10 +413,14 @@ export function BottomBar({ videoMetadata, selectedFormat, selectedFormatFileTyp if (videoMetadata._type === 'playlist') { await startDownload({ url: videoMetadata.original_url, - selectedFormat: activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat, + selectedFormat: activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat, downloadConfig: downloadConfiguration, selectedSubtitles: selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null, - playlistItems: selectedPlaylistVideoIndex + playlistItems: selectedPlaylistVideos.sort((a, b) => Number(a) - Number(b)).join(','), + overrideOptions: isMultiplePlaylistItems ? { + filesize: activeDownloadModeTab === 'combine' ? (selectedVideoFormat?.filesize_approx && selectedAudioFormat?.filesize_approx ? selectedVideoFormat.filesize_approx + selectedAudioFormat.filesize_approx : undefined) : selectedFormat?.filesize_approx ? selectedFormat.filesize_approx : undefined, + tbr: activeDownloadModeTab === 'combine' ? (selectedVideoFormat?.tbr && selectedAudioFormat?.tbr ? selectedVideoFormat.tbr + selectedAudioFormat.tbr : undefined) : selectedFormat?.tbr ? selectedFormat.tbr : undefined, + } : undefined }); } else if (videoMetadata._type === 'video') { await startDownload({ diff --git a/src/components/pages/downloader/playlistDownloader.tsx b/src/components/pages/downloader/playlistDownloader.tsx index 300ab7b..692b5d2 100644 --- a/src/components/pages/downloader/playlistDownloader.tsx +++ b/src/components/pages/downloader/playlistDownloader.tsx @@ -3,11 +3,12 @@ import { DownloadCloud, Info, ListVideo, AlertCircleIcon } from "lucide-react"; import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { RawVideoInfo, VideoFormat } from "@/types/video"; -// import { PlaylistToggleGroup, PlaylistToggleGroupItem } from "@/components/custom/playlistToggleGroup"; -import { PlaylistSelectionGroup, PlaylistSelectionGroupItem } from "@/components/custom/playlistSelectionGroup"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { PlaylistToggleGroup, PlaylistToggleGroupItem } from "@/components/custom/playlistToggleGroup"; +import { getMergedBestFormat } from "@/utils"; +import { Switch } from "@/components/ui/switch"; interface PlaylistPreviewSelectionProps { videoMetadata: RawVideoInfo; @@ -38,28 +39,61 @@ interface PlaylistDownloaderProps { } function PlaylistPreviewSelection({ videoMetadata }: PlaylistPreviewSelectionProps) { - const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex); + const selectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideos); 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); + const setSelectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideos); const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration); + const totalVideos = videoMetadata.entries.filter((entry) => entry).length; + const allVideoIndices = videoMetadata.entries.filter((entry) => entry).map((entry) => entry.playlist_index.toString()); + return (
-

- - Playlist ({videoMetadata.entries[0].n_entries}) -

+
+

+ + Playlist ({videoMetadata.entries[0].n_entries}) +

+
+ 0} + onCheckedChange={(checked) => { + if (checked) { + setSelectedPlaylistVideos(allVideoIndices); + } else { + setSelectedPlaylistVideos(["1"]); + } + setSelectedDownloadFormat('best'); + setSelectedSubtitles([]); + setSelectedCombinableVideoFormat(''); + setSelectedCombinableAudioFormat(''); + resetDownloadConfiguration(); + }} + disabled={totalVideos <= 1} + /> +
+

{videoMetadata.entries[0].playlist_title ? videoMetadata.entries[0].playlist_title : 'UNTITLED'}

{videoMetadata.entries[0].playlist_creator || videoMetadata.entries[0].playlist_channel || videoMetadata.entries[0].playlist_uploader || 'unknown'}

- {/* { + if (value.length > 0) { + setSelectedPlaylistVideos(value); + setSelectedDownloadFormat('best'); + setSelectedSubtitles([]); + setSelectedCombinableVideoFormat(''); + setSelectedCombinableAudioFormat(''); + resetDownloadConfiguration(); + } + }} > {videoMetadata.entries.map((entry) => entry ? ( ) : null)} - */} - { - setSelectedPlaylistVideoIndex(value); - setSelectedDownloadFormat('best'); - setSelectedSubtitles([]); - setSelectedCombinableVideoFormat(''); - setSelectedCombinableAudioFormat(''); - resetDownloadConfiguration(); - }} - > - {videoMetadata.entries.map((entry) => entry ? ( - - ) : null)} - +
Extracted from {videoMetadata.entries[0].extractor ? videoMetadata.entries[0].extractor.charAt(0).toUpperCase() + videoMetadata.entries[0].extractor.slice(1) : 'Unknown'} @@ -102,7 +116,7 @@ function PlaylistPreviewSelection({ videoMetadata }: PlaylistPreviewSelectionPro function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyFormats, combinedFormats, qualityPresetFormats, subtitleLanguages }: SelectivePlaylistDownloadProps) { const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat); const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles); - const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex); + const selectedPlaylistVideos = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideos); const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat); const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles); const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration); @@ -120,16 +134,23 @@ function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyF >

Subtitle Languages

- {subtitleLanguages.map((lang) => ( - - {lang.lang} - - ))} + {subtitleLanguages.map((lang) => { + const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig')); + const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig')); + const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig')); + + return ( + + {lang.lang} + + ); + })}
)} @@ -149,7 +170,7 @@ function SelectivePlaylistDownload({ videoMetadata, audioOnlyFormats, videoOnlyF
{qualityPresetFormats && qualityPresetFormats.length > 0 && ( @@ -235,16 +256,23 @@ function CombinedPlaylistDownload({ audioOnlyFormats, videoOnlyFormats, subtitle >

Subtitle Languages

- {subtitleLanguages.map((lang) => ( - - {lang.lang} - - ))} + {subtitleLanguages.map((lang) => { + const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig')); + const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig')); + const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig')); + + return ( + + {lang.lang} + + ); + })}
)} diff --git a/src/components/pages/downloader/videoDownloader.tsx b/src/components/pages/downloader/videoDownloader.tsx index 4437ba5..b77d14f 100644 --- a/src/components/pages/downloader/videoDownloader.tsx +++ b/src/components/pages/downloader/videoDownloader.tsx @@ -112,16 +112,23 @@ function SelectiveVideoDownload({ videoMetadata, audioOnlyFormats, videoOnlyForm >

Subtitle Languages

- {subtitleLanguages.map((lang) => ( - - {lang.lang} - - ))} + {subtitleLanguages.map((lang) => { + const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig')); + const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig')); + const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig')); + + return ( + + {lang.lang} + + ); + })}
)} @@ -227,16 +234,23 @@ function CombinedVideoDownload({ audioOnlyFormats, videoOnlyFormats, subtitleLan >

Subtitle Languages

- {subtitleLanguages.map((lang) => ( - - {lang.lang} - - ))} + {subtitleLanguages.map((lang) => { + const hasAutoSubSelected = selectedSubtitles.some(code => code.endsWith('-orig')); + const hasNormalSubSelected = selectedSubtitles.some(code => !code.endsWith('-orig')); + const isDisabled = (hasAutoSubSelected && !lang.code.endsWith('-orig')) || (hasNormalSubSelected && lang.code.endsWith('-orig')); + + return ( + + {lang.lang} + + ); + })}
)} diff --git a/src/components/pages/library/completedDownloads.tsx b/src/components/pages/library/completedDownloads.tsx index 2249dd2..6dcf819 100644 --- a/src/components/pages/library/completedDownloads.tsx +++ b/src/components/pages/library/completedDownloads.tsx @@ -9,6 +9,7 @@ import { formatBitrate, formatCodec, formatDurationString, formatFileSize, pagin import { ArrowUpRightIcon, AudioLines, CircleArrowDown, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Music, Play, Search, Trash2, Video } from "lucide-react"; import { invoke } from "@tauri-apps/api/core"; import * as fs from "@tauri-apps/plugin-fs"; +import { dirname } from "@tauri-apps/api/path"; import { DownloadState } from "@/types/download"; import { useQueryClient } from "@tanstack/react-query"; import { useDeleteDownloadState } from "@/services/mutations"; @@ -60,14 +61,31 @@ export function CompletedDownload({ state }: CompletedDownloadProps) { const removeFromDownloads = async (downloadState: DownloadState, delete_file: boolean) => { if (delete_file && downloadState.filepath) { - try { - if (await fs.exists(downloadState.filepath)) { - await fs.remove(downloadState.filepath); - } else { - console.error(`File not found: "${downloadState.filepath}"`); + const isMutilplePlaylistItems = downloadState.playlist_id !== null && + downloadState.playlist_indices !== null && + downloadState.playlist_indices.includes(','); + + if (isMutilplePlaylistItems) { + const dirPath = await dirname(downloadState.filepath); + try { + if (await fs.exists(dirPath)) { + await fs.remove(dirPath, { recursive: true }); + } else { + console.error(`Directory not found: "${dirPath}"`); + } + } catch (e) { + console.error(e); + } + } else { + try { + if (await fs.exists(downloadState.filepath)) { + await fs.remove(downloadState.filepath); + } else { + console.error(`File not found: "${downloadState.filepath}"`); + } + } catch (e) { + console.error(e); } - } catch (e) { - console.error(e); } } @@ -77,11 +95,11 @@ export function CompletedDownload({ state }: CompletedDownloadProps) { queryClient.invalidateQueries({ queryKey: ['download-states'] }); if (delete_file && downloadState.filepath) { toast.success("Deleted from downloads", { - description: `The download for "${downloadState.title}" has been deleted successfully.`, + description: `The download for ${isMutilplePlaylistItems ? 'playlist ' : ''}"${isMutilplePlaylistItems ? downloadState.playlist_title : downloadState.title}" has been deleted successfully.`, }); } else { toast.success("Removed from downloads", { - description: `The download for "${downloadState.title}" has been removed successfully.`, + description: `The download for ${isMutilplePlaylistItems ? 'playlist ' : ''}"${isMutilplePlaylistItems ? downloadState.playlist_title : downloadState.title}" has been removed successfully.`, }); } }, @@ -89,11 +107,11 @@ export function CompletedDownload({ state }: CompletedDownloadProps) { console.error("Failed to delete download state:", error); if (delete_file && downloadState.filepath) { toast.error("Failed to delete download", { - description: `An error occurred while trying to delete the download for "${downloadState.title}".`, + description: `An error occurred while trying to delete the download for ${isMutilplePlaylistItems ? 'playlist ' : ''}"${isMutilplePlaylistItems ? downloadState.playlist_title : downloadState.title}".`, }); } else { toast.error("Failed to remove download", { - description: `An error occurred while trying to remove the download for "${downloadState.title}".`, + description: `An error occurred while trying to remove the download for ${isMutilplePlaylistItems ? 'playlist ' : ''}"${isMutilplePlaylistItems ? downloadState.playlist_title : downloadState.title}".`, }); } } @@ -125,31 +143,60 @@ export function CompletedDownload({ state }: CompletedDownloadProps) { isDeleteFileChecked: false, }; + const isPlaylist = state.playlist_id !== null && state.playlist_indices !== null; + const isMutilplePlaylistItems = isPlaylist && state.playlist_indices && state.playlist_indices.includes(','); + return (
- - - - - {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( - + {isMutilplePlaylistItems ? ( +
+ + + +
+ +
+
+ +
+
+ ) : ( + + + + )} + {isMutilplePlaylistItems ? ( + + Playlist ({state.playlist_indices?.split(',').length}) + + ) : ( + + {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( + + )}
-

{state.title}

-

{state.channel ? state.channel : 'unknown'} {state.host ? <> {state.host} : 'unknown'}

+

{isMutilplePlaylistItems ? state.playlist_title : state.title}

+

{isMutilplePlaylistItems ? state.playlist_channel ?? 'unknown' : state.channel ?? 'unknown'} {state.host ? <> {state.host} : 'unknown'}

- {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'} + + {isMutilplePlaylistItems ? ( + <> {state.playlist_n_entries ?? 'unknown'} + ) : ( + <> {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'} + )} + {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( @@ -177,21 +224,21 @@ export function CompletedDownload({ state }: CompletedDownloadProps) {
- {state.playlist_id && state.playlist_index && ( + {state.playlist_id && state.playlist_indices && !isMutilplePlaylistItems && ( - Playlist ({state.playlist_index} of {state.playlist_n_entries}) + Playlist ({state.playlist_indices} of {state.playlist_n_entries}) )} - {state.vcodec && ( + {state.vcodec && !isMutilplePlaylistItems && ( {formatCodec(state.vcodec)} )} - {state.acodec && ( + {state.acodec && !isMutilplePlaylistItems && ( {formatCodec(state.acodec)} )} - {state.dynamic_range && state.dynamic_range !== 'SDR' && ( + {state.dynamic_range && state.dynamic_range !== 'SDR' && !isMutilplePlaylistItems && ( {state.dynamic_range} )} {state.subtitle_id && ( @@ -202,6 +249,22 @@ export function CompletedDownload({ state }: CompletedDownloadProps) { ESUB )} + {state.sponsorblock_mark && ( + + SPBLOCK(M) + + )} + {state.sponsorblock_remove && ( + + SPBLOCK(R) + + )}
diff --git a/src/components/pages/library/incompleteDownloads.tsx b/src/components/pages/library/incompleteDownloads.tsx index 74d8d66..588ae66 100644 --- a/src/components/pages/library/incompleteDownloads.tsx +++ b/src/components/pages/library/incompleteDownloads.tsx @@ -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, Info, Loader2, Music, Pause, Play, RotateCw, Video, X } from "lucide-react"; +import { ArrowUpRightIcon, CircleCheck, File, Info, ListVideo, 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"; @@ -37,13 +37,34 @@ export function IncompleteDownload({ state }: IncompleteDownloadProps) { isDeleteFileChecked: false, }; + const isPlaylist = state.playlist_id !== null && state.playlist_indices !== null; + const isMutilplePlaylistItems = isPlaylist && state.playlist_indices && state.playlist_indices.includes(','); + return (
- - - - {state.ext ? ( + {isMutilplePlaylistItems ? ( +
+ + + +
+ +
+
+ +
+
+ ) : ( + + + + )} + {isMutilplePlaylistItems ? ( + + Playlist ({state.playlist_indices?.split(',').length}) + + ) : state.ext ? ( {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
-

{state.title}

+

{isMutilplePlaylistItems ? state.playlist_title : state.title}

{((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && ( )} {(state.download_status === 'downloading' || state.download_status === 'paused' || state.download_status === 'errored') && state.progress && state.status !== 'finished' && (
+ {isMutilplePlaylistItems && state.item ? ( + ({state.item}) + ) : null} {state.progress}% { diff --git a/src/components/pages/settings/applicationSettings.tsx b/src/components/pages/settings/applicationSettings.tsx index 3434dfd..9925e69 100644 --- a/src/components/pages/settings/applicationSettings.tsx +++ b/src/components/pages/settings/applicationSettings.tsx @@ -362,7 +362,7 @@ function AppFolderSettings() {

Filename Template

-

Set the template for naming downloaded files (download id and file extension will be auto-appended at the end, changing template may cause paused downloads to re-start from begining)

+

Set the template for naming downloaded files (download id, file extension and playlist index will be auto-appended, changing template may cause paused downloads to re-start from begining)