(feat): added speed rate limit option in settings, improved download error handling and improved remove from library dialog

This commit is contained in:
2025-07-14 13:39:59 +05:30
parent 21524d2b29
commit 097839d919
6 changed files with 201 additions and 34 deletions

View File

@@ -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">

View File

@@ -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)}} />

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 {