From 1292758b1e2ac359c1474574c463390da6f1dba1 Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Sun, 15 Feb 2026 13:35:44 +0530 Subject: [PATCH] feat: added delay configuration settings #12 --- src/components/custom/numberInput.tsx | 110 ++++++++ .../pages/library/completedDownloads.tsx | 2 +- .../pages/library/incompleteDownloads.tsx | 2 +- .../pages/settings/applicationSettings.tsx | 267 +++++++++++++++++- .../pages/settings/extensionSettings.tsx | 7 +- src/helpers/use-downloader.ts | 60 +++- src/services/store.ts | 14 + src/types/settings.ts | 7 + src/utils.ts | 4 +- 9 files changed, 452 insertions(+), 21 deletions(-) create mode 100644 src/components/custom/numberInput.tsx diff --git a/src/components/custom/numberInput.tsx b/src/components/custom/numberInput.tsx new file mode 100644 index 0000000..105956c --- /dev/null +++ b/src/components/custom/numberInput.tsx @@ -0,0 +1,110 @@ +import * as React from "react" +import { Minus, Plus } from "lucide-react" +import { Input } from "@/components/ui/input" +import { cn } from "@/lib/utils" + +interface NumberInputProps + extends Omit, "type" | "onChange" | "value"> { + value?: number + defaultValue?: number + min?: number + max?: number + step?: number + onChange?: (value: number) => void +} + +const NumberInput = React.forwardRef( + ( + { + className, + value: controlledValue, + defaultValue = 0, + min = -Infinity, + max = Infinity, + step = 1, + onChange, + disabled, + readOnly, + ...props + }, + ref + ) => { + const [internalValue, setInternalValue] = React.useState(defaultValue) + const isControlled = controlledValue !== undefined + const currentValue = isControlled ? controlledValue : internalValue + + const updateValue = (newValue: number) => { + const clamped = Math.min(max, Math.max(min, newValue)) + if (!isControlled) { + setInternalValue(clamped) + } + onChange?.(clamped) + } + + const handleIncrement = () => { + if (!disabled && !readOnly) { + updateValue(currentValue + step) + } + } + + const handleDecrement = () => { + if (!disabled && !readOnly) { + updateValue(currentValue - step) + } + } + + const handleInputChange = (e: React.ChangeEvent) => { + const parsed = parseFloat(e.target.value) + if (!isNaN(parsed)) { + updateValue(parsed) + } else if (e.target.value === "" || e.target.value === "-") { + if (!isControlled) { + setInternalValue(0) + } + } + } + + return ( +
+ +
+ + +
+
+ ) + } +) +NumberInput.displayName = "NumberInput" + +export { NumberInput } diff --git a/src/components/pages/library/completedDownloads.tsx b/src/components/pages/library/completedDownloads.tsx index 0d70f95..5c9bfd3 100644 --- a/src/components/pages/library/completedDownloads.tsx +++ b/src/components/pages/library/completedDownloads.tsx @@ -352,7 +352,7 @@ export function CompletedDownloads({ downloads }: CompletedDownloadsProps) { - + No Completed Downloads diff --git a/src/components/pages/library/incompleteDownloads.tsx b/src/components/pages/library/incompleteDownloads.tsx index 811d030..d7add66 100644 --- a/src/components/pages/library/incompleteDownloads.tsx +++ b/src/components/pages/library/incompleteDownloads.tsx @@ -280,7 +280,7 @@ export function IncompleteDownloads({ downloads }: IncompleteDownloadsProps) { - + No Incomplete Downloads diff --git a/src/components/pages/settings/applicationSettings.tsx b/src/components/pages/settings/applicationSettings.tsx index d72c7e4..ac9394e 100644 --- a/src/components/pages/settings/applicationSettings.tsx +++ b/src/components/pages/settings/applicationSettings.tsx @@ -7,7 +7,7 @@ import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; -import { BadgeCheck, BellRing, BrushCleaning, Bug, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react"; +import { BadgeCheck, BellRing, BrushCleaning, Bug, Cookie, ExternalLink, FilePen, FileVideo, Folder, FolderOpen, Github, Globe, Heart, Info, Loader2, LucideIcon, Mail, Monitor, Moon, Package, Scale, ShieldMinus, SquareTerminal, Sun, Terminal, Timer, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react"; import { cn } from "@/lib/utils"; import { Slider } from "@/components/ui/slider"; import { Input } from "@/components/ui/input"; @@ -34,6 +34,7 @@ import { config } from "@/config"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import neosubhamoyImage from "@/assets/images/neosubhamoy.jpg"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { NumberInput } from "@/components/custom/numberInput"; const proxyUrlSchema = z.object({ url: z.url({ @@ -66,6 +67,46 @@ const filenameTemplateShcema = z.object({ template: z.string().min(1, { message: "Filename Template is required" }), }); +const minMaxSleepIntervalSchema = z.object({ + min_sleep_interval: z.coerce.number({ + error: (issue) => issue.input === undefined || issue.input === null || issue.input === "" + ? "Minimum Sleep Interval is required" + : "Minimum Sleep Interval must be a valid number" + }).int({ + message: "Minimum Sleep Interval must be an integer" + }).min(1, { + message: "Minimum Sleep Interval must be at least 1 second" + }).max(3600, { + message: "Minimum Sleep Interval must be at most 3600 seconds (1 hour)" + }), + max_sleep_interval: z.coerce.number({ + error: (issue) => issue.input === undefined || issue.input === null || issue.input === "" + ? "Maximum Sleep Interval is required" + : "Maximum Sleep Interval must be a valid number" + }).int({ + message: "Maximum Sleep Interval must be an integer" + }).min(1, { + message: "Maximum Sleep Interval must be at least 1 second" + }).max(3600, { + message: "Maximum Sleep Interval must be at most 3600 seconds (1 hour)" + }), +}) + +const requestSleepIntervalSchema = z.object({ + request_sleep_interval: z.coerce.number({ + error: (issue) => issue.input === undefined || issue.input === null || issue.input === "" + ? "Request Sleep Interval is required" + : "Request Sleep Interval must be a valid number" + }).int({ + message: "Request Sleep Interval must be an integer" + }).min(1, { + message: "Request Sleep Interval must be at least 1 second" + }).max(3600, { + message: "Request Sleep Interval must be at most 3600 seconds (1 hour)" + }), +}) + + function AppGeneralSettings() { const { saveSettingsKey } = useSettings(); @@ -668,9 +709,10 @@ function AppNetworkSettings() { render={({ field }) => ( - @@ -853,7 +895,7 @@ function AppSponsorblockSettings() { return ( <>
-

Sponsor Block

+

Sponsorblock

Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)

state.formResetTrigger); + const acknowledgeFormReset = useSettingsPageStatesStore(state => state.acknowledgeFormReset); + + const useDelay = useSettingsPageStatesStore(state => state.settings.use_delay); + const useSearchDelay = useSettingsPageStatesStore(state => state.settings.use_search_delay); + const delayMode = useSettingsPageStatesStore(state => state.settings.delay_mode); + const minSleepInterval = useSettingsPageStatesStore(state => state.settings.min_sleep_interval); + const maxSleepInterval = useSettingsPageStatesStore(state => state.settings.max_sleep_interval); + const requestSleepInterval = useSettingsPageStatesStore(state => state.settings.request_sleep_interval); + const delayPlaylistOnly = useSettingsPageStatesStore(state => state.settings.delay_playlist_only); + const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands); + + const minMaxSleepIntervalForm = useForm>({ + resolver: zodResolver(minMaxSleepIntervalSchema), + defaultValues: { + min_sleep_interval: minSleepInterval, + max_sleep_interval: maxSleepInterval, + }, + mode: "onChange", + }); + const watchedMinSleepInterval = minMaxSleepIntervalForm.watch("min_sleep_interval"); + const watchedMaxSleepInterval = minMaxSleepIntervalForm.watch("max_sleep_interval"); + const { errors: minMaxSleepIntervalFormErrors } = minMaxSleepIntervalForm.formState; + + function handleMinMaxSleepIntervalSubmit(values: z.infer) { + try { + saveSettingsKey('min_sleep_interval', values.min_sleep_interval); + saveSettingsKey('max_sleep_interval', values.max_sleep_interval); + toast.success("Sleep Intervals updated", { + description: `Minimum Sleep Interval changed to ${values.min_sleep_interval} seconds, Maximum Sleep Interval changed to ${values.max_sleep_interval} seconds`, + }); + } catch (error) { + console.error("Error changing sleep intervals:", error); + toast.error("Failed to change sleep intervals", { + description: "An error occurred while trying to change the sleep intervals. Please try again.", + }); + } + } + + const requestSleepIntervalForm = useForm>({ + resolver: zodResolver(requestSleepIntervalSchema), + defaultValues: { + request_sleep_interval: requestSleepInterval, + }, + mode: "onChange", + }); + const watchedRequestSleepInterval = requestSleepIntervalForm.watch("request_sleep_interval"); + const { errors: requestSleepIntervalFormErrors } = requestSleepIntervalForm.formState; + + function handleRequestSleepIntervalSubmit(values: z.infer) { + try { + saveSettingsKey('request_sleep_interval', values.request_sleep_interval); + toast.success("Request Sleep Interval updated", { + description: `Request Sleep Interval changed to ${values.request_sleep_interval} seconds`, + }); + } catch (error) { + console.error("Error changing request sleep interval:", error); + toast.error("Failed to change request sleep interval", { + description: "An error occurred while trying to change the request sleep interval. Please try again.", + }); + } + } + + useEffect(() => { + if (formResetTrigger > 0) { + minMaxSleepIntervalForm.reset(); + requestSleepIntervalForm.reset(); + acknowledgeFormReset(); + } + }, [formResetTrigger]); + + return ( + <> +
+

Delay

+

Use delay to prevent potential issues with some sites (bypass rate-limit, temporary ban, etc.)

+
+ saveSettingsKey('use_delay', checked)} + disabled={useCustomCommands} + /> + +
+
+ saveSettingsKey('use_search_delay', checked)} + disabled={useCustomCommands} + /> + +
+ saveSettingsKey('delay_mode', value)} + disabled={(!useDelay && !useSearchDelay) || useCustomCommands} + > +
+ + +
+
+ + +
+
+
+ +
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + + + +
+
+ +
+ + ( + + + + + + + )} + /> + + + +
+ +
+
+

Delay Playlist Only

+

Only apply delay for playlist/batch downloads, single video downloads will not be affected (recommended)

+
+ saveSettingsKey('delay_playlist_only', checked)} + disabled={!useDelay || useCustomCommands} + /> +
+
+ + ); +} + function AppNotificationSettings() { const { saveSettingsKey } = useSettings(); @@ -1468,6 +1724,7 @@ export function ApplicationSettings() { { key: 'network', label: 'Network', icon: Wifi, component: }, { key: 'cookies', label: 'Cookies', icon: Cookie, component: }, { key: 'sponsorblock', label: 'Sponsorblock', icon: ShieldMinus, component: }, + { key: 'delay', label: 'Delay', icon: Timer, component: }, { key: 'notifications', label: 'Notifications', icon: BellRing, component: }, { key: 'commands', label: 'Commands', icon: SquareTerminal, component: }, { key: 'debug', label: 'Debug', icon: Bug, component: }, @@ -1553,7 +1810,7 @@ export function ApplicationSettings() {
{tabsList.map((tab) => ( - + {tab.component} ))} diff --git a/src/components/pages/settings/extensionSettings.tsx b/src/components/pages/settings/extensionSettings.tsx index 4c7b7ee..6b17b8b 100644 --- a/src/components/pages/settings/extensionSettings.tsx +++ b/src/components/pages/settings/extensionSettings.tsx @@ -7,7 +7,6 @@ import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { ArrowDownToLine, ArrowRight, EthernetPort, Loader2, Radio, RotateCw } from "lucide-react"; import { useSettings } from "@/helpers/use-settings"; -import { Input } from "@/components/ui/input"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod" @@ -15,6 +14,7 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from "@/component import { invoke } from "@tauri-apps/api/core"; import { SlidingButton } from "@/components/custom/slidingButton"; import clsx from "clsx"; +import { NumberInput } from "@/components/custom/numberInput"; const websocketPortSchema = z.object({ port: z.coerce.number({ @@ -167,9 +167,10 @@ function ExtPortSettings() { render={({ field }) => ( - diff --git a/src/helpers/use-downloader.ts b/src/helpers/use-downloader.ts index 2b8657d..88480d0 100644 --- a/src/helpers/use-downloader.ts +++ b/src/helpers/use-downloader.ts @@ -61,7 +61,14 @@ export default function useDownloader() { log_verbose: LOG_VERBOSE, log_progress: LOG_PROGRESS, enable_notifications: ENABLE_NOTIFICATIONS, - download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION + download_completion_notification: DOWNLOAD_COMPLETION_NOTIFICATION, + use_delay: USE_DELAY, + use_search_delay: USE_SEARCH_DELAY, + delay_mode: DELAY_MODE, + min_sleep_interval: MIN_SLEEP_INTERVAL, + max_sleep_interval: MAX_SLEEP_INTERVAL, + request_sleep_interval: REQUEST_SLEEP_INTERVAL, + delay_playlist_only: DELAY_PLAYLIST_ONLY, } = useSettingsPageStatesStore(state => state.settings); const expectedErrorDownloadIds = useDownloaderPageStatesStore((state) => state.expectedErrorDownloadIds); @@ -167,6 +174,14 @@ export default function useDownloader() { args.push('--sponsorblock-mark', sponsorblockMark); } }; + if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_SEARCH_DELAY) { + if (DELAY_MODE === 'auto') { + args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20'); + } else if (DELAY_MODE === 'custom') { + args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString()); + } + } + const command = Command.sidecar('binaries/yt-dlp', args); let jsonOutput = ''; @@ -319,14 +334,41 @@ export default function useDownloader() { args.push('--output', `${FILENAME_TEMPLATE}[${downloadId}].%(ext)s`); } - if (isMultiplePlaylistItems) { - const playlistLength = playlistIndices.split(',').length; - if (playlistLength > 5 && playlistLength < 100) { - args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '15'); - } else if (playlistLength >= 100 && playlistLength < 500) { - args.push('--sleep-requests', '1.5', '--sleep-interval', '10', '--max-sleep-interval', '40'); - } else if (playlistLength >= 500) { - args.push('--sleep-requests', '2.5', '--sleep-interval', '20', '--max-sleep-interval', '60'); + if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_DELAY) { + if (!DELAY_PLAYLIST_ONLY) { + if (DELAY_MODE === 'auto') { + if (isMultiplePlaylistItems) { + const playlistLength = playlistIndices.split(',').length; + if (playlistLength <= 5) { + args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '10'); + } else if (playlistLength > 5 && playlistLength < 100) { + args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20'); + } else if (playlistLength >= 100 && playlistLength < 500) { + args.push('--sleep-requests', '2', '--sleep-interval', '20', '--max-sleep-interval', '40'); + } else if (playlistLength >= 500) { + args.push('--sleep-requests', '2', '--sleep-interval', '40', '--max-sleep-interval', '60'); + } + } else { + args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20'); + } + } else if (DELAY_MODE === 'custom') { + args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString()); + } + } else if (DELAY_PLAYLIST_ONLY && isMultiplePlaylistItems) { + if (DELAY_MODE === 'auto') { + const playlistLength = playlistIndices.split(',').length; + if (playlistLength <= 5) { + args.push('--sleep-requests', '1', '--sleep-interval', '5', '--max-sleep-interval', '10'); + } else if (playlistLength > 5 && playlistLength < 100) { + args.push('--sleep-requests', '1', '--sleep-interval', '10', '--max-sleep-interval', '20'); + } else if (playlistLength >= 100 && playlistLength < 500) { + args.push('--sleep-requests', '2', '--sleep-interval', '20', '--max-sleep-interval', '40'); + } else if (playlistLength >= 500) { + args.push('--sleep-requests', '2', '--sleep-interval', '40', '--max-sleep-interval', '60'); + } + } else if (DELAY_MODE === 'custom') { + args.push('--sleep-requests', REQUEST_SLEEP_INTERVAL.toString(), '--sleep-interval', MIN_SLEEP_INTERVAL.toString(), '--max-sleep-interval', MAX_SLEEP_INTERVAL.toString()); + } } } diff --git a/src/services/store.ts b/src/services/store.ts index 4ed3156..1b0ca14 100644 --- a/src/services/store.ts +++ b/src/services/store.ts @@ -211,6 +211,13 @@ export const useSettingsPageStatesStore = create((set) enable_notifications: false, update_notification: true, download_completion_notification: false, + use_delay: true, + use_search_delay: false, + delay_mode: 'auto', + min_sleep_interval: 10, + max_sleep_interval: 20, + request_sleep_interval: 1, + delay_playlist_only: true, // extension settings websocket_port: 53511 }, @@ -282,6 +289,13 @@ export const useSettingsPageStatesStore = create((set) enable_notifications: false, update_notification: true, download_completion_notification: false, + use_delay: true, + use_search_delay: false, + delay_mode: 'auto', + min_sleep_interval: 10, + max_sleep_interval: 20, + request_sleep_interval: 1, + delay_playlist_only: true, // extension settings websocket_port: 53511 }, diff --git a/src/types/settings.ts b/src/types/settings.ts index 0b1b648..7dd2889 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -54,6 +54,13 @@ export interface Settings { enable_notifications: boolean; update_notification: boolean; download_completion_notification: boolean; + use_delay: boolean; + use_search_delay: boolean; + delay_mode: string; + min_sleep_interval: number; + max_sleep_interval: number; + request_sleep_interval: number; + delay_playlist_only: boolean; // extension settings websocket_port: number; } diff --git a/src/utils.ts b/src/utils.ts index a6b96eb..8a92fda 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -348,7 +348,7 @@ export const determineFileType = ( const audioCodec = (acodec || '').toLowerCase(); const isNone = (str: string): boolean => { - return ['none', 'n/a', '-', ''].includes(str); + return ['none', 'auto', 'n/a', '-', ''].includes(str); }; const hasVideo = !isNone(videoCodec); @@ -591,7 +591,7 @@ export const getMergedBestFormat = ( } else { return { ...baseFormat, - format: 'Best Video (Automatic)', + format: 'Best Quality (Auto)', format_id: 'best', format_note: 'auto', ext: 'auto',