mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2025-12-19 17:52:59 +05:30
(feat): added speed rate limit option in settings, improved download error handling and improved remove from library dialog
This commit is contained in:
69
src/App.tsx
69
src/App.tsx
@@ -7,7 +7,7 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
import { arch, exeExtension } from "@tauri-apps/plugin-os";
|
||||||
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path";
|
||||||
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { determineFileType, generateDownloadId, generateSafeFilePath, generateVideoId, isObjEmpty, parseProgressLine, sanitizeFilename } from "@/utils";
|
import { determineFileType, generateDownloadId, generateSafeFilePath, generateVideoId, isObjEmpty, parseProgressLine, sanitizeFilename } from "@/utils";
|
||||||
import { Command } from "@tauri-apps/plugin-shell";
|
import { Command } from "@tauri-apps/plugin-shell";
|
||||||
import { RawVideoInfo } from "@/types/video";
|
import { RawVideoInfo } from "@/types/video";
|
||||||
@@ -25,8 +25,11 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
|
||||||
import useAppUpdater from "@/helpers/use-app-updater";
|
import useAppUpdater from "@/helpers/use-app-updater";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
export default function App({ children }: { children: React.ReactNode }) {
|
export default function App({ children }: { children: React.ReactNode }) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||||
const { data: settings, isSuccess: isSuccessFetchingSettings } = useFetchAllSettings();
|
const { data: settings, isSuccess: isSuccessFetchingSettings } = useFetchAllSettings();
|
||||||
const { data: kvPairs, isSuccess: isSuccessFetchingKvPairs } = useFetchAllkVPairs();
|
const { data: kvPairs, isSuccess: isSuccessFetchingKvPairs } = useFetchAllkVPairs();
|
||||||
@@ -58,6 +61,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const STRICT_DOWNLOADABILITY_CHECK = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
const STRICT_DOWNLOADABILITY_CHECK = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
||||||
const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
||||||
const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
||||||
|
const USE_RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.use_rate_limit);
|
||||||
|
const RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.rate_limit);
|
||||||
const VIDEO_FORMAT = useSettingsPageStatesStore(state => state.settings.video_format);
|
const VIDEO_FORMAT = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||||
const AUDIO_FORMAT = useSettingsPageStatesStore(state => state.settings.audio_format);
|
const AUDIO_FORMAT = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||||
const ALWAYS_REENCODE_VIDEO = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
const ALWAYS_REENCODE_VIDEO = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||||
@@ -65,6 +70,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const EMBED_AUDIO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
const EMBED_AUDIO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||||
const EMBED_AUDIO_THUMBNAIL = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
const EMBED_AUDIO_THUMBNAIL = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||||
|
|
||||||
|
const isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||||
|
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||||
|
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId);
|
||||||
|
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
|
||||||
|
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||||
|
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow()
|
const appWindow = getCurrentWebviewWindow()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { updateYtDlp } = useYtDlpUpdater();
|
const { updateYtDlp } = useYtDlpUpdater();
|
||||||
@@ -136,6 +148,11 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
||||||
|
// set error states to default
|
||||||
|
setIsErrored(false);
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
setErroredDownloadId(null);
|
||||||
|
|
||||||
console.log('Starting download:', { url, selectedFormat, selectedSubtitles, resumeState, playlistItems });
|
console.log('Starting download:', { url, selectedFormat, selectedSubtitles, resumeState, playlistItems });
|
||||||
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
||||||
console.error('FFmpeg or download paths not found');
|
console.error('FFmpeg or download paths not found');
|
||||||
@@ -147,6 +164,11 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined);
|
let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined);
|
||||||
if (!videoMetadata) {
|
if (!videoMetadata) {
|
||||||
console.error('Failed to fetch video metadata');
|
console.error('Failed to fetch video metadata');
|
||||||
|
toast({
|
||||||
|
title: 'Download Failed',
|
||||||
|
description: 'yt-dlp failed to fetch video metadata. Please try again later.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +255,20 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
args.push('--proxy', PROXY_URL);
|
args.push('--proxy', PROXY_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (USE_RATE_LIMIT && RATE_LIMIT) {
|
||||||
|
args.push('--limit-rate', `${RATE_LIMIT}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Starting download with args:', args);
|
console.log('Starting download with args:', args);
|
||||||
const command = Command.sidecar('binaries/yt-dlp', args);
|
const command = Command.sidecar('binaries/yt-dlp', args);
|
||||||
|
|
||||||
command.on('close', async data => {
|
command.on('close', async data => {
|
||||||
if (data.code !== 0) {
|
if (data.code !== 0) {
|
||||||
console.error(`Download failed with code ${data.code}`);
|
console.error(`Download failed with code ${data.code}`);
|
||||||
|
if (!isErrorExpected) {
|
||||||
|
setIsErrored(true);
|
||||||
|
setErroredDownloadId(downloadId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -430,6 +460,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const pauseDownload = async (downloadState: DownloadState) => {
|
const pauseDownload = async (downloadState: DownloadState) => {
|
||||||
try {
|
try {
|
||||||
|
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||||
console.log("Killing process with PID:", downloadState.process_id);
|
console.log("Killing process with PID:", downloadState.process_id);
|
||||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||||
downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, {
|
downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, {
|
||||||
@@ -474,6 +505,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const cancelDownload = async (downloadState: DownloadState) => {
|
const cancelDownload = async (downloadState: DownloadState) => {
|
||||||
try {
|
try {
|
||||||
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) {
|
||||||
|
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||||
console.log("Killing process with PID:", downloadState.process_id);
|
console.log("Killing process with PID:", downloadState.process_id);
|
||||||
await invoke('kill_all_process', { pid: downloadState.process_id });
|
await invoke('kill_all_process', { pid: downloadState.process_id });
|
||||||
}
|
}
|
||||||
@@ -807,6 +839,41 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
}, [processQueuedDownloads, ongoingDownloads, queuedDownloads]);
|
||||||
|
|
||||||
|
// show a toast and pause the download when yt-dlp exits unexpectedly
|
||||||
|
useEffect(() => {
|
||||||
|
if (isErrored && !isErrorExpected) {
|
||||||
|
toast({
|
||||||
|
title: "Download Failed",
|
||||||
|
description: "yt-dlp exited unexpectedly. Please try again later",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
if (erroredDownloadId) {
|
||||||
|
downloadStatusUpdater.mutate({ download_id: erroredDownloadId, download_status: 'paused' }, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
console.log("Download status updated successfully:", data);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to update download status:", error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setErroredDownloadId(null);
|
||||||
|
}
|
||||||
|
setIsErrored(false);
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
}
|
||||||
|
}, [isErrored, isErrorExpected, erroredDownloadId, setIsErrored, setIsErrorExpected, setErroredDownloadId]);
|
||||||
|
|
||||||
|
// auto reset isErrorExpected state after 3 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (isErrorExpected) {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setIsErrorExpected(false);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}, [isErrorExpected, setIsErrorExpected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>
|
<AppContext.Provider value={{ fetchVideoMetadata, startDownload, pauseDownload, resumeDownload, cancelDownload }}>
|
||||||
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme={APP_THEME || "system"} storageKey="vite-ui-theme">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAppContext } from "@/providers/appContextProvider";
|
import { useAppContext } from "@/providers/appContextProvider";
|
||||||
import { useDownloadActionStatesStore, useDownloadStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
import { useDownloadActionStatesStore, useDownloaderPageStatesStore, useDownloadStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
||||||
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils";
|
||||||
import { AudioLines, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Square, Trash2, Video, X } from "lucide-react";
|
import { AudioLines, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Square, Trash2, Video, X } from "lucide-react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@@ -33,6 +33,8 @@ export default function LibraryPage() {
|
|||||||
const setIsCancelingDownload = useDownloadActionStatesStore(state => state.setIsCancelingDownload);
|
const setIsCancelingDownload = useDownloadActionStatesStore(state => state.setIsCancelingDownload);
|
||||||
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
const setIsDeleteFileChecked = useDownloadActionStatesStore(state => state.setIsDeleteFileChecked);
|
||||||
|
|
||||||
|
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||||
|
|
||||||
const { pauseDownload, resumeDownload, cancelDownload } = useAppContext()
|
const { pauseDownload, resumeDownload, cancelDownload } = useAppContext()
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -106,6 +108,7 @@ export default function LibraryPage() {
|
|||||||
|
|
||||||
const stopOngoingDownloads = async () => {
|
const stopOngoingDownloads = async () => {
|
||||||
if (ongoingDownloads.length > 0) {
|
if (ongoingDownloads.length > 0) {
|
||||||
|
setIsErrorExpected(true); // Set error expected to true to handle UI state
|
||||||
try {
|
try {
|
||||||
await invoke('pause_ongoing_downloads').then(() => {
|
await invoke('pause_ongoing_downloads').then(() => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||||
@@ -275,9 +278,9 @@ export default function LibraryPage() {
|
|||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Remove from library?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone! it will permanently remove this from downloads.
|
Are you sure you want to remove this download from the library? You can also delete the downloaded file by cheking the box below. This action cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { SlidingButton } from "@/components/custom/slidingButton";
|
|||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import * as fs from "@tauri-apps/plugin-fs";
|
import * as fs from "@tauri-apps/plugin-fs";
|
||||||
import { join } from "@tauri-apps/api/path";
|
import { join } from "@tauri-apps/api/path";
|
||||||
|
import { formatSpeed } from "@/utils";
|
||||||
|
|
||||||
const websocketPortSchema = z.object({
|
const websocketPortSchema = z.object({
|
||||||
port: z.coerce.number({
|
port: z.coerce.number({
|
||||||
@@ -42,6 +43,17 @@ const proxyUrlSchema = z.object({
|
|||||||
url: z.string().min(1, { message: "Proxy URL is required" }).url({ message: "Invalid URL format" })
|
url: z.string().min(1, { message: "Proxy URL is required" }).url({ message: "Invalid URL format" })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rateLimitSchema = z.object({
|
||||||
|
rate_limit: z.coerce.number({
|
||||||
|
required_error: "Rate Limit is required",
|
||||||
|
invalid_type_error: "Rate Limit must be a valid number",
|
||||||
|
}).min(1024, {
|
||||||
|
message: "Rate Limit must be at least 1024 bytes/s (1 KB/s)"
|
||||||
|
}).max(104857600, {
|
||||||
|
message: "Rate Limit must be at most 104857600 bytes/s (100 MB/s)"
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
@@ -65,6 +77,8 @@ export default function SettingsPage() {
|
|||||||
const strictDownloadabilityCheck = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
const strictDownloadabilityCheck = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check);
|
||||||
const useProxy = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
const useProxy = useSettingsPageStatesStore(state => state.settings.use_proxy);
|
||||||
const proxyUrl = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
const proxyUrl = useSettingsPageStatesStore(state => state.settings.proxy_url);
|
||||||
|
const useRateLimit = useSettingsPageStatesStore(state => state.settings.use_rate_limit);
|
||||||
|
const rateLimit = useSettingsPageStatesStore(state => state.settings.rate_limit);
|
||||||
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||||
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
|
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||||
const alwaysReencodeVideo = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
const alwaysReencodeVideo = useSettingsPageStatesStore(state => state.settings.always_reencode_video);
|
||||||
@@ -169,6 +183,33 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rateLimitForm = useForm<z.infer<typeof rateLimitSchema>>({
|
||||||
|
resolver: zodResolver(rateLimitSchema),
|
||||||
|
defaultValues: {
|
||||||
|
rate_limit: rateLimit,
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const watchedRateLimit = rateLimitForm.watch("rate_limit");
|
||||||
|
const { errors: rateLimitFormErrors } = rateLimitForm.formState;
|
||||||
|
|
||||||
|
function handleRateLimitSubmit(values: z.infer<typeof rateLimitSchema>) {
|
||||||
|
try {
|
||||||
|
saveSettingsKey('rate_limit', values.rate_limit);
|
||||||
|
toast({
|
||||||
|
title: "Rate Limit updated",
|
||||||
|
description: `Rate Limit changed to ${values.rate_limit} bytes/s`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error changing rate limit:", error);
|
||||||
|
toast({
|
||||||
|
title: "Failed to change rate limit",
|
||||||
|
description: "Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
@@ -577,7 +618,6 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
<Label htmlFor="use-proxy">Use Proxy</Label>
|
<Label htmlFor="use-proxy">Use Proxy</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Form {...proxyUrlForm}>
|
<Form {...proxyUrlForm}>
|
||||||
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
<form onSubmit={proxyUrlForm.handleSubmit(handleProxyUrlSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -607,6 +647,45 @@ export default function SettingsPage() {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="rate-limit">
|
||||||
|
<h3 className="font-semibold">Rate Limit</h3>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Limit download speed to prevent network congestion. Rate limit is applied per-download basis (not in the whole app)</p>
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<Switch
|
||||||
|
id="use-rate-limit"
|
||||||
|
checked={useRateLimit}
|
||||||
|
onCheckedChange={(checked) => saveSettingsKey('use_rate_limit', checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="use-rate-limit">Use Rate Limit</Label>
|
||||||
|
</div>
|
||||||
|
<Form {...rateLimitForm}>
|
||||||
|
<form onSubmit={rateLimitForm.handleSubmit(handleRateLimitSubmit)} className="flex gap-4 w-full" autoComplete="off">
|
||||||
|
<FormField
|
||||||
|
control={rateLimitForm.control}
|
||||||
|
name="rate_limit"
|
||||||
|
disabled={!useRateLimit}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
className="focus-visible:ring-0"
|
||||||
|
placeholder="Enter rate limit in bytes/s"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Label htmlFor="rate_limit" className="text-xs text-muted-foreground">(Configured: {rateLimit ? `${rateLimit} = ${formatSpeed(rateLimit)}` : 'No'}, Status: {useRateLimit ? 'Enabled' : 'Disabled'}) (Default: 1048576, Range: 1024-104857600)</Label>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!watchedRateLimit || Number(watchedRateLimit) === rateLimit || Object.keys(rateLimitFormErrors).length > 0 || !useRateLimit}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,13 +49,19 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
|
|||||||
selectedCombinableAudioFormat: '',
|
selectedCombinableAudioFormat: '',
|
||||||
selectedSubtitles: [],
|
selectedSubtitles: [],
|
||||||
selectedPlaylistVideoIndex: '1',
|
selectedPlaylistVideoIndex: '1',
|
||||||
|
isErrored: false,
|
||||||
|
isErrorExpected: false,
|
||||||
|
erroredDownloadId: null,
|
||||||
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
||||||
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
||||||
setSelectedDownloadFormat: (format) => set(() => ({ selectedDownloadFormat: format })),
|
setSelectedDownloadFormat: (format) => set(() => ({ selectedDownloadFormat: format })),
|
||||||
setSelectedCombinableVideoFormat: (format) => set(() => ({ selectedCombinableVideoFormat: format })),
|
setSelectedCombinableVideoFormat: (format) => set(() => ({ selectedCombinableVideoFormat: format })),
|
||||||
setSelectedCombinableAudioFormat: (format) => set(() => ({ selectedCombinableAudioFormat: format })),
|
setSelectedCombinableAudioFormat: (format) => set(() => ({ selectedCombinableAudioFormat: format })),
|
||||||
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
||||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index }))
|
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index })),
|
||||||
|
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
|
||||||
|
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
||||||
|
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
|
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
|
||||||
@@ -122,6 +128,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
max_parallel_downloads: 2,
|
max_parallel_downloads: 2,
|
||||||
use_proxy: false,
|
use_proxy: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
|
use_rate_limit: false,
|
||||||
|
rate_limit: 1048576, // 1 MB/s
|
||||||
video_format: 'auto',
|
video_format: 'auto',
|
||||||
audio_format: 'auto',
|
audio_format: 'auto',
|
||||||
always_reencode_video: false,
|
always_reencode_video: false,
|
||||||
@@ -163,6 +171,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
max_parallel_downloads: 2,
|
max_parallel_downloads: 2,
|
||||||
use_proxy: false,
|
use_proxy: false,
|
||||||
proxy_url: '',
|
proxy_url: '',
|
||||||
|
use_rate_limit: false,
|
||||||
|
rate_limit: 1048576, // 1 MB/s
|
||||||
video_format: 'auto',
|
video_format: 'auto',
|
||||||
audio_format: 'auto',
|
audio_format: 'auto',
|
||||||
always_reencode_video: false,
|
always_reencode_video: false,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface Settings {
|
|||||||
strict_downloadablity_check: boolean;
|
strict_downloadablity_check: boolean;
|
||||||
use_proxy: boolean;
|
use_proxy: boolean;
|
||||||
proxy_url: string;
|
proxy_url: string;
|
||||||
|
use_rate_limit: boolean;
|
||||||
|
rate_limit: number;
|
||||||
video_format: string;
|
video_format: string;
|
||||||
audio_format: string;
|
audio_format: string;
|
||||||
always_reencode_video: boolean;
|
always_reencode_video: boolean;
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export interface DownloaderPageStatesStore {
|
|||||||
selectedCombinableAudioFormat: string;
|
selectedCombinableAudioFormat: string;
|
||||||
selectedSubtitles: string[];
|
selectedSubtitles: string[];
|
||||||
selectedPlaylistVideoIndex: string;
|
selectedPlaylistVideoIndex: string;
|
||||||
|
isErrored: boolean;
|
||||||
|
isErrorExpected: boolean;
|
||||||
|
erroredDownloadId: string | null;
|
||||||
setActiveDownloadModeTab: (tab: string) => void;
|
setActiveDownloadModeTab: (tab: string) => void;
|
||||||
setIsStartingDownload: (isStarting: boolean) => void;
|
setIsStartingDownload: (isStarting: boolean) => void;
|
||||||
setSelectedDownloadFormat: (format: string) => void;
|
setSelectedDownloadFormat: (format: string) => void;
|
||||||
@@ -45,6 +48,9 @@ export interface DownloaderPageStatesStore {
|
|||||||
setSelectedCombinableAudioFormat: (format: string) => void;
|
setSelectedCombinableAudioFormat: (format: string) => void;
|
||||||
setSelectedSubtitles: (subtitles: string[]) => void;
|
setSelectedSubtitles: (subtitles: string[]) => void;
|
||||||
setSelectedPlaylistVideoIndex: (index: string) => void;
|
setSelectedPlaylistVideoIndex: (index: string) => void;
|
||||||
|
setIsErrored: (isErrored: boolean) => void;
|
||||||
|
setIsErrorExpected: (isErrorExpected: boolean) => void;
|
||||||
|
setErroredDownloadId: (downloadId: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LibraryPageStatesStore {
|
export interface LibraryPageStatesStore {
|
||||||
|
|||||||
Reference in New Issue
Block a user