mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2025-12-19 23:39:33 +05:30
feat: added custom commands, per-download configs and other minor improvements
This commit is contained in:
@@ -180,5 +180,80 @@ pub fn get_migrations() -> Vec<Migration> {
|
||||
END;
|
||||
",
|
||||
kind: MigrationKind::Up,
|
||||
},
|
||||
Migration {
|
||||
version: 4,
|
||||
description: "add_custom_command_column_to_downloads",
|
||||
sql: "
|
||||
-- Create temporary table with the new column in the correct position
|
||||
CREATE TABLE downloads_temp (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
download_id TEXT UNIQUE NOT NULL,
|
||||
download_status TEXT NOT NULL,
|
||||
video_id TEXT NOT NULL,
|
||||
format_id TEXT NOT NULL,
|
||||
subtitle_id TEXT,
|
||||
queue_index INTEGER,
|
||||
playlist_id TEXT,
|
||||
playlist_index INTEGER,
|
||||
resolution TEXT,
|
||||
ext TEXT,
|
||||
abr REAL,
|
||||
vbr REAL,
|
||||
acodec TEXT,
|
||||
vcodec TEXT,
|
||||
dynamic_range TEXT,
|
||||
process_id INTEGER,
|
||||
status TEXT,
|
||||
progress REAL,
|
||||
total INTEGER,
|
||||
downloaded INTEGER,
|
||||
speed REAL,
|
||||
eta INTEGER,
|
||||
filepath TEXT,
|
||||
filetype TEXT,
|
||||
filesize INTEGER,
|
||||
output_format TEXT,
|
||||
embed_metadata INTEGER NOT NULL DEFAULT 0,
|
||||
embed_thumbnail INTEGER NOT NULL DEFAULT 0,
|
||||
sponsorblock_remove TEXT,
|
||||
sponsorblock_mark TEXT,
|
||||
use_aria2 INTEGER NOT NULL DEFAULT 0,
|
||||
custom_command TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (video_id) REFERENCES video_info (video_id),
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlist_info (playlist_id)
|
||||
);
|
||||
|
||||
-- Copy all data from original table to temporary table
|
||||
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, output_format,
|
||||
embed_metadata, embed_thumbnail, sponsorblock_remove, sponsorblock_mark,
|
||||
use_aria2, NULL, -- custom_command default value
|
||||
created_at, updated_at
|
||||
FROM downloads;
|
||||
|
||||
-- Drop existing triggers for the original table
|
||||
DROP TRIGGER IF EXISTS update_downloads_updated_at;
|
||||
|
||||
-- Drop the original table
|
||||
DROP TABLE downloads;
|
||||
|
||||
-- Rename temporary table to original name
|
||||
ALTER TABLE downloads_temp RENAME TO downloads;
|
||||
|
||||
-- Re-Create the update trigger
|
||||
CREATE TRIGGER IF NOT EXISTS update_downloads_updated_at
|
||||
AFTER UPDATE ON downloads
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE downloads SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END;
|
||||
",
|
||||
kind: MigrationKind::Up,
|
||||
}]
|
||||
}
|
||||
|
||||
123
src/App.tsx
123
src/App.tsx
@@ -27,6 +27,7 @@ import useAppUpdater from "@/helpers/use-app-updater";
|
||||
import { Toaster as Sonner } from "@/components/ui/sonner";
|
||||
import { toast } from "sonner";
|
||||
import { useLogger } from "@/helpers/use-logger";
|
||||
import { DownloadConfiguration } from "./types/settings";
|
||||
|
||||
export default function App({ children }: { children: React.ReactNode }) {
|
||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
||||
@@ -84,10 +85,13 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const USE_ARIA2 = useSettingsPageStatesStore(state => state.settings.use_aria2);
|
||||
const USE_FORCE_INTERNET_PROTOCOL = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol);
|
||||
const FORCE_INTERNET_PROTOCOL = useSettingsPageStatesStore(state => state.settings.force_internet_protocol);
|
||||
const USE_CUSTOM_COMMANDS = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const CUSTOM_COMMANDS = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||
|
||||
const isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId);
|
||||
const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration);
|
||||
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
|
||||
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
|
||||
@@ -118,23 +122,25 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
||||
const isRegisteredToMacOsRef = useRef(false);
|
||||
|
||||
const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string): Promise<RawVideoInfo | null> => {
|
||||
const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState): Promise<RawVideoInfo | null> => {
|
||||
try {
|
||||
const args = [url, '--dump-single-json', '--no-warnings'];
|
||||
if (formatId) args.push('-f', formatId);
|
||||
if (selectedSubtitles) args.push('--embed-subs', '--sub-lang', selectedSubtitles);
|
||||
if (playlistIndex) args.push('--playlist-items', playlistIndex);
|
||||
if (PREFER_VIDEO_OVER_PLAYLIST) args.push('--no-playlist');
|
||||
if (PREFER_VIDEO_OVER_PLAYLIST && !playlistIndex) args.push('--no-playlist');
|
||||
if (STRICT_DOWNLOADABILITY_CHECK && !formatId) args.push('--check-all-formats');
|
||||
if (STRICT_DOWNLOADABILITY_CHECK && formatId) args.push('--check-formats');
|
||||
if (USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL);
|
||||
if (USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) {
|
||||
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL);
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) {
|
||||
if (FORCE_INTERNET_PROTOCOL === 'ipv4') {
|
||||
args.push('--force-ipv4');
|
||||
} else if (FORCE_INTERNET_PROTOCOL === 'ipv6') {
|
||||
args.push('--force-ipv6');
|
||||
}
|
||||
}
|
||||
if (USE_COOKIES) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_COOKIES) {
|
||||
if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) {
|
||||
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
||||
} else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) {
|
||||
@@ -197,14 +203,14 @@ 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, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
|
||||
LOG.info('NEODLP', `Initiating yt-dlp download for URL: ${url}`);
|
||||
// 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, downloadConfig, selectedSubtitles, resumeState, playlistItems });
|
||||
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
||||
console.error('FFmpeg or download paths not found');
|
||||
return;
|
||||
@@ -212,7 +218,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const isPlaylist = (playlistItems && typeof playlistItems === 'string') || (resumeState?.playlist_id && resumeState?.playlist_index) ? true : false;
|
||||
const playlistIndex = isPlaylist ? (resumeState?.playlist_index?.toString() || playlistItems) : null;
|
||||
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, selectedSubtitles, resumeState);
|
||||
if (!videoMetadata) {
|
||||
console.error('Failed to fetch video metadata');
|
||||
toast.error("Download Failed", {
|
||||
@@ -231,6 +237,11 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
if (AUDIO_FORMAT !== 'auto' && fileType === 'audio') videoMetadata.ext = AUDIO_FORMAT;
|
||||
}
|
||||
|
||||
let configOutputFormat = null;
|
||||
if (downloadConfig.output_format && downloadConfig.output_format !== 'auto') {
|
||||
videoMetadata.ext = downloadConfig.output_format;
|
||||
configOutputFormat = downloadConfig.output_format;
|
||||
}
|
||||
if (resumeState && resumeState.output_format) videoMetadata.ext = resumeState.output_format;
|
||||
|
||||
const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
||||
@@ -265,55 +276,82 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
args.push('--playlist-items', playlistIndex);
|
||||
}
|
||||
|
||||
let customCommandArgs = null;
|
||||
if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfiguration.custom_command) || resumeState?.custom_command) {
|
||||
if (resumeState?.custom_command) {
|
||||
customCommandArgs = resumeState.custom_command;
|
||||
} else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfiguration.custom_command)) {
|
||||
let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfiguration.custom_command);
|
||||
customCommandArgs = customCommand ? customCommand.args : '';
|
||||
}
|
||||
|
||||
if (customCommandArgs && customCommandArgs.trim() !== '') args.push(...customCommandArgs.split(' '));
|
||||
}
|
||||
|
||||
let outputFormat = null;
|
||||
if (fileType !== 'unknown' && ((VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto') || resumeState?.output_format)) {
|
||||
outputFormat = resumeState?.output_format || (fileType === 'video+audio' && VIDEO_FORMAT !== 'auto' ? VIDEO_FORMAT : (fileType === 'video' && VIDEO_FORMAT !== 'auto' ? VIDEO_FORMAT : (fileType === 'audio' && AUDIO_FORMAT !== 'auto' ? AUDIO_FORMAT : null)));
|
||||
if ((VIDEO_FORMAT !== 'auto' && fileType === 'video+audio') || (resumeState?.output_format && fileType === 'video+audio')) {
|
||||
if (ALWAYS_REENCODE_VIDEO) {
|
||||
args.push('--recode-video', resumeState?.output_format || VIDEO_FORMAT);
|
||||
} else {
|
||||
args.push('--merge-output-format', resumeState?.output_format || VIDEO_FORMAT);
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && ((fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) || configOutputFormat || resumeState?.output_format)) {
|
||||
const format = resumeState?.output_format || configOutputFormat;
|
||||
|
||||
if (format) {
|
||||
outputFormat = format;
|
||||
} else if (fileType === 'audio' && AUDIO_FORMAT !== 'auto') {
|
||||
outputFormat = AUDIO_FORMAT;
|
||||
} else if ((fileType === 'video' || fileType === 'video+audio') && VIDEO_FORMAT !== 'auto') {
|
||||
outputFormat = VIDEO_FORMAT;
|
||||
}
|
||||
|
||||
const recodeOrRemux = ALWAYS_REENCODE_VIDEO ? '--recode-video' : '--remux-video';
|
||||
const formatToUse = format || VIDEO_FORMAT;
|
||||
|
||||
// Handle video+audio
|
||||
if (fileType === 'video+audio' && (VIDEO_FORMAT !== 'auto' || format)) {
|
||||
args.push(ALWAYS_REENCODE_VIDEO ? '--recode-video' : '--merge-output-format', formatToUse);
|
||||
}
|
||||
if ((VIDEO_FORMAT !== 'auto' && fileType === 'video') || (resumeState?.output_format && fileType === 'video')) {
|
||||
if (ALWAYS_REENCODE_VIDEO) {
|
||||
args.push('--recode-video', resumeState?.output_format || VIDEO_FORMAT);
|
||||
} else {
|
||||
args.push('--remux-video', resumeState?.output_format || VIDEO_FORMAT);
|
||||
// Handle video only
|
||||
else if (fileType === 'video' && (VIDEO_FORMAT !== 'auto' || format)) {
|
||||
args.push(recodeOrRemux, formatToUse);
|
||||
}
|
||||
// Handle audio only
|
||||
else if (fileType === 'audio' && (AUDIO_FORMAT !== 'auto' || format)) {
|
||||
args.push('--extract-audio', '--audio-format', format || AUDIO_FORMAT);
|
||||
}
|
||||
// Handle unknown filetype
|
||||
else if (fileType === 'unknown' && format) {
|
||||
if (['mkv', 'mp4', 'webm'].includes(format)) {
|
||||
args.push(recodeOrRemux, formatToUse);
|
||||
} else if (['mp3', 'm4a', 'opus'].includes(format)) {
|
||||
args.push('--extract-audio', '--audio-format', format);
|
||||
}
|
||||
if ((AUDIO_FORMAT !== 'auto' && fileType === 'audio') || (resumeState?.output_format && fileType === 'audio')) {
|
||||
args.push('--extract-audio', '--audio-format', resumeState?.output_format || AUDIO_FORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
let embedMetadata = 0;
|
||||
if (fileType !== 'unknown' && ((EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA) || resumeState?.embed_metadata)) {
|
||||
if ((EMBED_VIDEO_METADATA || resumeState?.embed_metadata) && (fileType === 'video+audio' || fileType === 'video')) {
|
||||
embedMetadata = 1;
|
||||
args.push('--embed-metadata');
|
||||
}
|
||||
if ((EMBED_AUDIO_METADATA || resumeState?.embed_metadata) && fileType === 'audio') {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) {
|
||||
const shouldEmbedForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || (EMBED_VIDEO_METADATA && downloadConfiguration.embed_metadata === null));
|
||||
const shouldEmbedForAudio = fileType === 'audio' && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || (EMBED_AUDIO_METADATA && downloadConfiguration.embed_metadata === null));
|
||||
const shouldEmbedForUnknown = fileType === 'unknown' && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata);
|
||||
|
||||
if (shouldEmbedForUnknown || shouldEmbedForVideo || shouldEmbedForAudio) {
|
||||
embedMetadata = 1;
|
||||
args.push('--embed-metadata');
|
||||
}
|
||||
}
|
||||
|
||||
let embedThumbnail = 0;
|
||||
if (fileType === 'audio' && (EMBED_AUDIO_THUMBNAIL || resumeState?.embed_thumbnail)) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfiguration.embed_thumbnail || resumeState?.embed_thumbnail || (fileType === 'audio' && EMBED_AUDIO_THUMBNAIL && downloadConfiguration.embed_thumbnail === null))) {
|
||||
embedThumbnail = 1;
|
||||
args.push('--embed-thumbnail');
|
||||
}
|
||||
|
||||
if (USE_PROXY && PROXY_URL) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) {
|
||||
args.push('--proxy', PROXY_URL);
|
||||
}
|
||||
|
||||
if (USE_RATE_LIMIT && RATE_LIMIT) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_RATE_LIMIT && RATE_LIMIT) {
|
||||
args.push('--limit-rate', `${RATE_LIMIT}`);
|
||||
}
|
||||
|
||||
if (USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) {
|
||||
if (FORCE_INTERNET_PROTOCOL === 'ipv4') {
|
||||
args.push('--force-ipv4');
|
||||
} else if (FORCE_INTERNET_PROTOCOL === 'ipv6') {
|
||||
@@ -321,7 +359,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
if (USE_COOKIES) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_COOKIES) {
|
||||
if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) {
|
||||
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
||||
} else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) {
|
||||
@@ -331,7 +369,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
|
||||
let sponsorblockRemove = null;
|
||||
let sponsorblockMark = null;
|
||||
if (USE_SPONSORBLOCK || (resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark)) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (USE_SPONSORBLOCK || (resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark))) {
|
||||
if (SPONSORBLOCK_MODE === 'remove' || resumeState?.sponsorblock_remove) {
|
||||
sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? (
|
||||
SPONSORBLOCK_REMOVE_CATEGORIES.length > 0 ? SPONSORBLOCK_REMOVE_CATEGORIES.join(',') : 'default'
|
||||
@@ -346,7 +384,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
let useAria2 = 0;
|
||||
if (USE_ARIA2 || resumeState?.use_aria2) {
|
||||
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (USE_ARIA2 || resumeState?.use_aria2)) {
|
||||
useAria2 = 1;
|
||||
args.push(
|
||||
'--downloader', 'aria2c',
|
||||
@@ -356,7 +394,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
LOG.warning('NEODLP', `Looks like you are using aria2 for this yt-dlp download: ${downloadId}. Make sure aria2 is installed on your system if you are on macOS for this to work. Also, pause/resume might not work as expected especially on windows (using aria2 is not recommended for most downloads).`);
|
||||
}
|
||||
|
||||
if (resumeState || USE_ARIA2) {
|
||||
if (resumeState || (!USE_CUSTOM_COMMANDS && USE_ARIA2)) {
|
||||
args.push('--continue');
|
||||
} else {
|
||||
args.push('--no-continue');
|
||||
@@ -459,7 +497,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
embed_thumbnail: embedThumbnail,
|
||||
sponsorblock_remove: sponsorblockRemove,
|
||||
sponsorblock_mark: sponsorblockMark,
|
||||
use_aria2: useAria2
|
||||
use_aria2: useAria2,
|
||||
custom_command: customCommandArgs,
|
||||
};
|
||||
downloadStateSaver.mutate(state, {
|
||||
onSuccess: (data) => {
|
||||
@@ -551,7 +590,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
embed_thumbnail: resumeState?.embed_thumbnail || 0,
|
||||
sponsorblock_remove: resumeState?.sponsorblock_remove || null,
|
||||
sponsorblock_mark: resumeState?.sponsorblock_mark || null,
|
||||
use_aria2: resumeState?.use_aria2 || 0
|
||||
use_aria2: resumeState?.use_aria2 || 0,
|
||||
custom_command: resumeState?.custom_command || null
|
||||
}
|
||||
downloadStateSaver.mutate(state, {
|
||||
onSuccess: (data) => {
|
||||
@@ -643,6 +683,12 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
await startDownload(
|
||||
downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url,
|
||||
downloadState.format_id,
|
||||
{
|
||||
output_format: null,
|
||||
embed_metadata: null,
|
||||
embed_thumbnail: null,
|
||||
custom_command: null
|
||||
},
|
||||
downloadState.subtitle_id,
|
||||
downloadState
|
||||
);
|
||||
@@ -739,6 +785,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
await startDownload(
|
||||
downloadToStart.url,
|
||||
downloadToStart.format_id,
|
||||
downloadConfiguration,
|
||||
downloadToStart.subtitle_id,
|
||||
downloadToStart
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function Navbar() {
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Log Viewer</DialogTitle>
|
||||
<DialogDescription>Monitor real-time neodlp logs for the current session</DialogDescription>
|
||||
<DialogDescription>Monitor real-time app session logs (latest on top)</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2 p-2 max-h-[300px] overflow-y-scroll overflow-x-hidden bg-muted">
|
||||
{logs.length === 0 ? (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { toast } from "sonner";
|
||||
import { useAppContext } from "@/providers/appContextProvider";
|
||||
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { determineFileType, fileFormatFilter, formatBitrate, formatDurationString, formatFileSize, formatReleaseDate, formatYtStyleCount, isObjEmpty, sortByBitrate } from "@/utils";
|
||||
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon, X } from "lucide-react";
|
||||
import { Calendar, Clock, DownloadCloud, Eye, Info, Loader2, Music, ThumbsUp, Video, File, ListVideo, PackageSearch, AlertCircleIcon, X, Settings2 } from "lucide-react";
|
||||
import { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
||||
@@ -24,6 +24,11 @@ import { config } from "@/config";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
const searchFormSchema = z.object({
|
||||
url: z.url({
|
||||
@@ -51,22 +56,32 @@ export default function DownloaderPage() {
|
||||
const setShowSearchError = useCurrentVideoMetadataStore((state) => state.setShowSearchError);
|
||||
|
||||
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
|
||||
const activeDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.activeDownloadConfigurationTab);
|
||||
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
||||
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
|
||||
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
|
||||
const selectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormat);
|
||||
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
|
||||
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
|
||||
const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration);
|
||||
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
|
||||
const setActiveDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadConfigurationTab);
|
||||
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
|
||||
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 setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
|
||||
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
|
||||
|
||||
const videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_format);
|
||||
const embedVideoMetadata = useSettingsPageStatesStore(state => state.settings.embed_video_metadata);
|
||||
const embedAudioMetadata = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata);
|
||||
const embedAudioThumbnail = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail);
|
||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||
|
||||
const audioOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('audio'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('audio'))) : [];
|
||||
const videoOnlyFormats = videoMetadata?._type === 'video' ? sortByBitrate(videoMetadata?.formats.filter(fileFormatFilter('video'))) : videoMetadata?._type === 'playlist' ? sortByBitrate(videoMetadata?.entries[Number(selectedPlaylistVideoIndex) - 1].formats.filter(fileFormatFilter('video'))) : [];
|
||||
@@ -146,7 +161,10 @@ export default function DownloaderPage() {
|
||||
|
||||
let selectedFormatExtensionMsg = 'Auto - unknown';
|
||||
if (activeDownloadModeTab === 'combine') {
|
||||
if (videoFormat !== 'auto') {
|
||||
if (downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Combined - ${downloadConfiguration.output_format.toUpperCase()}`;
|
||||
}
|
||||
else if (videoFormat !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Combined - ${videoFormat.toUpperCase()}`;
|
||||
}
|
||||
else if (selectedAudioFormat?.ext && selectedVideoFormat?.ext) {
|
||||
@@ -155,10 +173,12 @@ export default function DownloaderPage() {
|
||||
selectedFormatExtensionMsg = `Combined - unknown`;
|
||||
}
|
||||
} else if (selectedFormat?.ext) {
|
||||
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && videoFormat !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Forced - ${videoFormat.toUpperCase()}`;
|
||||
} else if (selectedFormatFileType === 'audio' && audioFormat !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Forced - ${audioFormat.toUpperCase()}`;
|
||||
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || videoFormat !== 'auto')) {
|
||||
selectedFormatExtensionMsg = `Forced - ${(downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') ? downloadConfiguration.output_format.toUpperCase() : videoFormat.toUpperCase()}`;
|
||||
} else if (selectedFormatFileType === 'audio' && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || audioFormat !== 'auto')) {
|
||||
selectedFormatExtensionMsg = `Forced - ${(downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') ? downloadConfiguration.output_format.toUpperCase() : audioFormat.toUpperCase()}`;
|
||||
} else if (selectedFormatFileType === 'unknown' && downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') {
|
||||
selectedFormatExtensionMsg = `Forced - ${downloadConfiguration.output_format.toUpperCase()}`;
|
||||
} else {
|
||||
selectedFormatExtensionMsg = `Auto - ${selectedFormat.ext.toUpperCase()}`;
|
||||
}
|
||||
@@ -220,6 +240,7 @@ export default function DownloaderPage() {
|
||||
setSelectedCombinableAudioFormat('');
|
||||
setSelectedSubtitles([]);
|
||||
setSelectedPlaylistVideoIndex('1');
|
||||
resetDownloadConfiguration();
|
||||
|
||||
fetchVideoMetadata(values.url).then((metadata) => {
|
||||
if (!metadata || (metadata._type !== 'video' && metadata._type !== 'playlist') || (metadata && metadata._type === 'video' && metadata.formats.length <= 0) || (metadata && metadata._type === 'playlist' && metadata.entries.length <= 0)) {
|
||||
@@ -270,6 +291,10 @@ export default function DownloaderPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useCustomCommands ? setActiveDownloadConfigurationTab('commands') : setActiveDownloadConfigurationTab('options');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (watchedUrl !== videoUrl) {
|
||||
setVideoUrl(watchedUrl);
|
||||
@@ -435,7 +460,12 @@ export default function DownloaderPage() {
|
||||
<Tabs
|
||||
className=""
|
||||
value={activeDownloadModeTab}
|
||||
onValueChange={(tab) => setActiveDownloadModeTab(tab)}
|
||||
onValueChange={(tab) => {
|
||||
setActiveDownloadModeTab(tab)
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm flex items-center gap-2">
|
||||
@@ -481,6 +511,9 @@ export default function DownloaderPage() {
|
||||
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
|
||||
// setSelectedSubtitles([]);
|
||||
// }
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
}}
|
||||
>
|
||||
<p className="text-xs">Suggested</p>
|
||||
@@ -581,6 +614,9 @@ export default function DownloaderPage() {
|
||||
value={selectedCombinableAudioFormat}
|
||||
onValueChange={(value) => {
|
||||
setSelectedCombinableAudioFormat(value);
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
}}
|
||||
>
|
||||
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
|
||||
@@ -602,6 +638,9 @@ export default function DownloaderPage() {
|
||||
value={selectedCombinableVideoFormat}
|
||||
onValueChange={(value) => {
|
||||
setSelectedCombinableVideoFormat(value);
|
||||
setDownloadConfigurationKey('output_format', null);
|
||||
setDownloadConfigurationKey('embed_metadata', null);
|
||||
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||
}}
|
||||
>
|
||||
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
|
||||
@@ -668,6 +707,7 @@ export default function DownloaderPage() {
|
||||
setSelectedSubtitles([]);
|
||||
setSelectedCombinableVideoFormat('');
|
||||
setSelectedCombinableAudioFormat('');
|
||||
resetDownloadConfiguration();
|
||||
}}
|
||||
>
|
||||
{videoMetadata.entries.map((entry) => entry ? (
|
||||
@@ -909,6 +949,198 @@ export default function DownloaderPage() {
|
||||
<span className="text-xs text-muted-foreground">{selectedFormatFinalMsg}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={!selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat))}
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Configurations</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DialogContent className="sm:max-w-[450px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configurations</DialogTitle>
|
||||
<DialogDescription>Tweak this download's configurations</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2 max-h-[300px] overflow-y-scroll overflow-x-hidden no-scrollbar">
|
||||
<Tabs
|
||||
className=""
|
||||
value={activeDownloadConfigurationTab}
|
||||
onValueChange={(tab) => setActiveDownloadConfigurationTab(tab)}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="options">Options</TabsTrigger>
|
||||
<TabsTrigger value="commands">Commands</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="options">
|
||||
{useCustomCommands ? (
|
||||
<Alert className="mt-2 mb-3">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle className="text-sm">Options Unavailable!</AlertTitle>
|
||||
<AlertDescription className="text-xs">
|
||||
You cannot use these options when custom commands are enabled. To use these options, disable custom commands from Settings.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="video-format">
|
||||
<Label className="text-xs mb-3 mt-2">Output Format ({(selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? 'Video' : selectedFormatFileType && selectedFormatFileType === 'audio' ? 'Audio' : 'Unknown'})</Label>
|
||||
{(selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? (
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 flex-wrap"
|
||||
value={downloadConfiguration.output_format ?? 'auto'}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="v-auto" />
|
||||
<Label htmlFor="v-auto">Follow Settings</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp4" id="v-mp4" />
|
||||
<Label htmlFor="v-mp4">MP4</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="webm" id="v-webm" />
|
||||
<Label htmlFor="v-webm">WEBM</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mkv" id="v-mkv" />
|
||||
<Label htmlFor="v-mkv">MKV</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
) : selectedFormatFileType && selectedFormatFileType === 'audio' ? (
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 flex-wrap"
|
||||
value={downloadConfiguration.output_format ?? 'auto'}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="a-auto" />
|
||||
<Label htmlFor="a-auto">Follow Settings</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="m4a" id="a-m4a" />
|
||||
<Label htmlFor="a-m4a">M4A</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="opus" id="a-opus" />
|
||||
<Label htmlFor="a-opus">OPUS</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp3" id="a-mp3" />
|
||||
<Label htmlFor="a-mp3">MP3</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
) : (
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
className="flex items-center gap-4 flex-wrap"
|
||||
value={downloadConfiguration.output_format ?? 'auto'}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('output_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="u-auto" />
|
||||
<Label htmlFor="u-auto">Follow Settings</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp4" id="u-mp4" />
|
||||
<Label htmlFor="u-mp4">MP4</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="webm" id="u-webm" />
|
||||
<Label htmlFor="u-webm">WEBM</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mkv" id="u-mkv" />
|
||||
<Label htmlFor="u-mkv">MKV</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="m4a" id="u-m4a" />
|
||||
<Label htmlFor="u-m4a">M4A</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="opus" id="u-opus" />
|
||||
<Label htmlFor="u-opus">OPUS</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="mp3" id="u-mp3" />
|
||||
<Label htmlFor="u-mp3">MP3</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
<div className="embeding-options">
|
||||
<Label className="text-xs my-3">Embeding Options</Label>
|
||||
<div className="flex items-center space-x-2 mt-3">
|
||||
<Switch
|
||||
id="embed-metadata"
|
||||
checked={downloadConfiguration.embed_metadata !== null ? downloadConfiguration.embed_metadata : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? embedVideoMetadata : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioMetadata : false}
|
||||
onCheckedChange={(checked) => setDownloadConfigurationKey('embed_metadata', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-metadata">Embed Metadata</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-3">
|
||||
<Switch
|
||||
id="embed-thumbnail"
|
||||
checked={downloadConfiguration.embed_thumbnail !== null ? downloadConfiguration.embed_thumbnail : (selectedFormatFileType && (selectedFormatFileType === 'video' || selectedFormatFileType === 'video+audio')) || activeDownloadModeTab === 'combine' ? false : selectedFormatFileType && selectedFormatFileType === 'audio' ? embedAudioThumbnail : false}
|
||||
onCheckedChange={(checked) => setDownloadConfigurationKey('embed_thumbnail', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-thumbnail">Embed Thumbnail</Label>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="commands">
|
||||
{!useCustomCommands ? (
|
||||
<Alert className="mt-2 mb-3">
|
||||
<AlertCircleIcon />
|
||||
<AlertTitle className="text-sm">Enable Custom Commands!</AlertTitle>
|
||||
<AlertDescription className="text-xs">
|
||||
To run custom commands for downloads, please enable it from the Settings.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="custom-commands">
|
||||
<Label className="text-xs mb-3 mt-2">Run Custom Command</Label>
|
||||
{customCommands.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">NO CUSTOM COMMAND TEMPLATE ADDED YET!</p>
|
||||
) : (
|
||||
<RadioGroup
|
||||
orientation="vertical"
|
||||
className="flex flex-col gap-2"
|
||||
disabled={!useCustomCommands}
|
||||
value={downloadConfiguration.custom_command}
|
||||
onValueChange={(value) => setDownloadConfigurationKey('custom_command', value)}
|
||||
>
|
||||
{customCommands.map((command) => (
|
||||
<div className="flex items-center gap-3" key={command.id}>
|
||||
<RadioGroupItem value={command.id} id={`cmd-${command.id}`} />
|
||||
<Label htmlFor={`cmd-${command.id}`}>{command.label}</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setIsStartingDownload(true);
|
||||
@@ -917,6 +1149,7 @@ export default function DownloaderPage() {
|
||||
await startDownload(
|
||||
videoMetadata.original_url,
|
||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat,
|
||||
downloadConfiguration,
|
||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
|
||||
undefined,
|
||||
selectedPlaylistVideoIndex
|
||||
@@ -925,6 +1158,7 @@ export default function DownloaderPage() {
|
||||
await startDownload(
|
||||
videoMetadata.webpage_url,
|
||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
|
||||
downloadConfiguration,
|
||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
|
||||
);
|
||||
}
|
||||
@@ -941,7 +1175,7 @@ export default function DownloaderPage() {
|
||||
setIsStartingDownload(false);
|
||||
}
|
||||
}}
|
||||
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat))}
|
||||
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat)) || (useCustomCommands && !downloadConfiguration.custom_command)}
|
||||
>
|
||||
{isStartingDownload ? (
|
||||
<>
|
||||
@@ -953,6 +1187,7 @@ export default function DownloaderPage() {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Heading from "@/components/heading";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useBasePathsStore, useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { useBasePathsStore, useDownloaderPageStatesStore, useDownloadStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { ArrowDownToLine, ArrowRight, BrushCleaning, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, Sun, Terminal, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { ArrowDownToLine, ArrowRight, BrushCleaning, Cookie, EthernetPort, ExternalLink, FileVideo, Folder, FolderOpen, Info, Loader2, LucideIcon, Monitor, Moon, Radio, RotateCcw, RotateCw, ShieldMinus, SquareTerminal, Sun, Terminal, Trash, TriangleAlert, WandSparkles, Wifi, Wrench } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "@/providers/themeProvider";
|
||||
@@ -26,8 +26,10 @@ import { SlidingButton } from "@/components/custom/slidingButton";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import * as fs from "@tauri-apps/plugin-fs";
|
||||
import { join } from "@tauri-apps/api/path";
|
||||
import { formatSpeed } from "@/utils";
|
||||
import { formatSpeed, generateID } from "@/utils";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
const websocketPortSchema = z.object({
|
||||
port: z.coerce.number<number>({
|
||||
@@ -65,6 +67,11 @@ const rateLimitSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const addCustomCommandSchema = z.object({
|
||||
label: z.string().min(1, { message: "Label is required" }),
|
||||
args: z.string().min(1, { message: "Arguments are required" }),
|
||||
});
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
@@ -109,6 +116,8 @@ export default function SettingsPage() {
|
||||
const useAria2 = useSettingsPageStatesStore(state => state.settings.use_aria2);
|
||||
const useForceInternetProtocol = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol);
|
||||
const forceInternetProtocol = useSettingsPageStatesStore(state => state.settings.force_internet_protocol);
|
||||
const useCustomCommands = useSettingsPageStatesStore(state => state.settings.use_custom_commands);
|
||||
const customCommands = useSettingsPageStatesStore(state => state.settings.custom_commands);
|
||||
|
||||
const websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port);
|
||||
const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort);
|
||||
@@ -116,6 +125,9 @@ export default function SettingsPage() {
|
||||
const isRestartingWebSocketServer = useSettingsPageStatesStore(state => state.isRestartingWebSocketServer);
|
||||
const setIsRestartingWebSocketServer = useSettingsPageStatesStore(state => state.setIsRestartingWebSocketServer);
|
||||
|
||||
const setDownloadConfigurationKey = useDownloaderPageStatesStore((state) => state.setDownloadConfigurationKey);
|
||||
const resetDownloadConfiguration = useDownloaderPageStatesStore((state) => state.resetDownloadConfiguration);
|
||||
|
||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||
const ongoingDownloads = downloadStates.filter(state =>
|
||||
['starting', 'downloading', 'queued'].includes(state.download_status)
|
||||
@@ -235,6 +247,56 @@ export default function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const addCustomCommandForm = useForm<z.infer<typeof addCustomCommandSchema>>({
|
||||
resolver: zodResolver(addCustomCommandSchema),
|
||||
defaultValues: {
|
||||
label: '',
|
||||
args: '',
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
const watchedLabel = addCustomCommandForm.watch("label");
|
||||
const watchedArgs = addCustomCommandForm.watch("args");
|
||||
const { errors: addCustomCommandFormErrors } = addCustomCommandForm.formState;
|
||||
|
||||
function handleAddCustomCommandSubmit(values: z.infer<typeof addCustomCommandSchema>) {
|
||||
try {
|
||||
const newCommand = {
|
||||
id: generateID(),
|
||||
label: values.label,
|
||||
args: values.args,
|
||||
};
|
||||
const updatedCommands = [...customCommands, newCommand];
|
||||
saveSettingsKey('custom_commands', updatedCommands);
|
||||
toast.success("Custom Command added", {
|
||||
description: `Custom Command "${values.label}" added.`,
|
||||
});
|
||||
addCustomCommandForm.reset();
|
||||
} catch (error) {
|
||||
console.error("Error adding custom command:", error);
|
||||
toast.error("Failed to add custom command", {
|
||||
description: "An error occurred while trying to add the custom command. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveCustomCommandSubmit(commandId: string) {
|
||||
try {
|
||||
const removedCommand = customCommands.find(command => command.id === commandId);
|
||||
const updatedCommands = customCommands.filter(command => command.id !== commandId);
|
||||
saveSettingsKey('custom_commands', updatedCommands);
|
||||
setDownloadConfigurationKey('custom_command', null);
|
||||
toast.success("Custom Command removed", {
|
||||
description: `Custom Command "${removedCommand?.label}" removed.`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error removing custom command:", error);
|
||||
toast.error("Failed to remove custom command", {
|
||||
description: "An error occurred while trying to remove the custom command. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface Config {
|
||||
port: number;
|
||||
}
|
||||
@@ -422,9 +484,14 @@ export default function SettingsPage() {
|
||||
value="sponsorblock"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><ShieldMinus className="size-4" /> Sponsorblock</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="commands"
|
||||
value="commands"
|
||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
||||
><SquareTerminal className="size-4" /> Commands</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="min-h-full flex flex-col max-w-[55%] w-full border-l border-border pl-4">
|
||||
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="general" value="general" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="max-parallel-downloads">
|
||||
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Set maximum number of allowed parallel downloads</p>
|
||||
@@ -476,10 +543,11 @@ export default function SettingsPage() {
|
||||
id="aria2"
|
||||
checked={useAria2}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_aria2', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="appearance" value="appearance" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="app-theme">
|
||||
<h3 className="font-semibold">Theme</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Choose app interface theme</p>
|
||||
@@ -502,7 +570,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="folders" value="folders" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="download-dir">
|
||||
<h3 className="font-semibold">Download Folder</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Set default download folder (directory)</p>
|
||||
@@ -562,7 +630,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="formats" value="formats" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="formats" value="formats" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="video-format">
|
||||
<h3 className="font-semibold">Video Format</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Choose in which format the final video file will be saved</p>
|
||||
@@ -571,6 +639,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={videoFormat}
|
||||
onValueChange={(value) => saveSettingsKey('video_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="v-auto" />
|
||||
@@ -598,6 +667,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={audioFormat}
|
||||
onValueChange={(value) => saveSettingsKey('audio_format', value)}
|
||||
disabled={useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="a-auto" />
|
||||
@@ -624,10 +694,11 @@ export default function SettingsPage() {
|
||||
id="always-reencode-video"
|
||||
checked={alwaysReencodeVideo}
|
||||
onCheckedChange={(checked) => saveSettingsKey('always_reencode_video', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="metadata" value="metadata" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="metadata" value="metadata" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="embed-video-metadata">
|
||||
<h3 className="font-semibold">Embed Metadata</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Wheather to embed metadata in video/audio files (info, chapters)</p>
|
||||
@@ -636,6 +707,7 @@ export default function SettingsPage() {
|
||||
id="embed-video-metadata"
|
||||
checked={embedVideoMetadata}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_video_metadata', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-video-metadata">Video</Label>
|
||||
</div>
|
||||
@@ -644,6 +716,7 @@ export default function SettingsPage() {
|
||||
id="embed-audio-metadata"
|
||||
checked={embedAudioMetadata}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_audio_metadata', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="embed-audio-metadata">Audio</Label>
|
||||
</div>
|
||||
@@ -655,10 +728,11 @@ export default function SettingsPage() {
|
||||
id="embed-audio-thumbnail"
|
||||
checked={embedAudioThumbnail}
|
||||
onCheckedChange={(checked) => saveSettingsKey('embed_audio_thumbnail', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="network" value="network" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="proxy">
|
||||
<h3 className="font-semibold">Proxy</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use proxy for downloads, Unblocks blocked sites in your region (download speed may affect, some sites may not work)</p>
|
||||
@@ -667,6 +741,7 @@ export default function SettingsPage() {
|
||||
id="use-proxy"
|
||||
checked={useProxy}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_proxy', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-proxy">Use Proxy</Label>
|
||||
</div>
|
||||
@@ -682,10 +757,11 @@ export default function SettingsPage() {
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter proxy URL"
|
||||
readOnly={useCustomCommands}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy ? 'Enabled' : 'Disabled'})</Label>
|
||||
<Label htmlFor="url" className="text-xs text-muted-foreground">(Configured: {proxyUrl ? 'Yes' : 'No'}, Status: {useProxy && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -707,6 +783,7 @@ export default function SettingsPage() {
|
||||
id="use-rate-limit"
|
||||
checked={useRateLimit}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_rate_limit', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-rate-limit">Use Rate Limit</Label>
|
||||
</div>
|
||||
@@ -722,10 +799,11 @@ export default function SettingsPage() {
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter rate limit in bytes/s"
|
||||
readOnly={useCustomCommands}
|
||||
{...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>
|
||||
<Label htmlFor="rate_limit" className="text-xs text-muted-foreground">(Configured: {rateLimit ? `${rateLimit} = ${formatSpeed(rateLimit)}` : 'No'}, Status: {useRateLimit && !useCustomCommands ? 'Enabled' : 'Disabled'}) (Default: 1048576, Range: 1024-104857600)</Label>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -741,12 +819,13 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
<div className="force-internet-protocol">
|
||||
<h3 className="font-semibold">Force Internet Protocol</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Force using a specific internet protocol (ipv4/ipv6) for all downloads, useful if your network supports only one (some sites may not work)</p>
|
||||
<p className="text-xs text-muted-foreground mb-3">Force use a specific internet protocol (ipv4/ipv6) for all downloads, useful if your network supports only one (some sites may not work)</p>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-force-internet-protocol"
|
||||
checked={useForceInternetProtocol}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_force_internet_protocol', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-force-internet-protocol">Force</Label>
|
||||
</div>
|
||||
@@ -755,7 +834,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4 mb-2"
|
||||
value={forceInternetProtocol}
|
||||
onValueChange={(value) => saveSettingsKey('force_internet_protocol', value)}
|
||||
disabled={!useForceInternetProtocol}
|
||||
disabled={!useForceInternetProtocol || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="ipv4" id="force-ipv4" />
|
||||
@@ -766,10 +845,10 @@ export default function SettingsPage() {
|
||||
<Label htmlFor="force-ipv6">Use IPv6 Only</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Label className="text-xs text-muted-foreground">(Forced: {forceInternetProtocol === "ipv4" ? 'IPv4' : 'IPv6'}, Status: {useForceInternetProtocol ? 'Enabled' : 'Disabled'})</Label>
|
||||
<Label className="text-xs text-muted-foreground">(Forced: {forceInternetProtocol === "ipv4" ? 'IPv4' : 'IPv6'}, Status: {useForceInternetProtocol && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="cookies" value="cookies" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="cookies" value="cookies" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="cookies">
|
||||
<h3 className="font-semibold">Cookies</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use cookies to access exclusive/private (login-protected) contents from sites (use wisely, over-use can even block/ban your account)</p>
|
||||
@@ -778,6 +857,7 @@ export default function SettingsPage() {
|
||||
id="use-cookies"
|
||||
checked={useCookies}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_cookies', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-cookies">Use Cookies</Label>
|
||||
</div>
|
||||
@@ -786,7 +866,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={importCookiesFrom}
|
||||
onValueChange={(value) => saveSettingsKey('import_cookies_from', value)}
|
||||
disabled={!useCookies}
|
||||
disabled={!useCookies || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="browser" id="cookies-browser" />
|
||||
@@ -802,7 +882,7 @@ export default function SettingsPage() {
|
||||
<Select
|
||||
value={cookiesBrowser}
|
||||
onValueChange={(value) => saveSettingsKey('cookies_browser', value)}
|
||||
disabled={importCookiesFrom !== "browser" || !useCookies}
|
||||
disabled={importCookiesFrom !== "browser" || !useCookies || useCustomCommands}
|
||||
>
|
||||
<SelectTrigger className="w-[230px] ring-0 focus:ring-0">
|
||||
<SelectValue placeholder="Select browser to import cookies" />
|
||||
@@ -829,7 +909,7 @@ export default function SettingsPage() {
|
||||
<Input className="focus-visible:ring-0" type="text" placeholder="Select cookies text file" value={cookiesFile ?? ''} disabled={importCookiesFrom !== "file" || !useCookies} readOnly/>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={importCookiesFrom !== "file" || !useCookies}
|
||||
disabled={importCookiesFrom !== "file" || !useCookies || useCustomCommands}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const file = await open({
|
||||
@@ -854,10 +934,10 @@ export default function SettingsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {importCookiesFrom === "browser" ? 'Yes' : cookiesFile ? 'Yes' : 'No'}, From: {importCookiesFrom === "browser" ? 'Browser' : 'Text'}, Status: {useCookies ? 'Enabled' : 'Disabled'})</Label>
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {importCookiesFrom === "browser" ? 'Yes' : cookiesFile ? 'Yes' : 'No'}, From: {importCookiesFrom === "browser" ? 'Browser' : 'Text'}, Status: {useCookies && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="sponsorblock" value="sponsorblock" className="flex flex-col gap-4 min-h-[310px]">
|
||||
<TabsContent key="sponsorblock" value="sponsorblock" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="sponsorblock">
|
||||
<h3 className="font-semibold">Sponsor Block</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3">Use sponsorblock to remove/mark unwanted segments in videos (sponsorships, intros, outros, etc.)</p>
|
||||
@@ -866,6 +946,7 @@ export default function SettingsPage() {
|
||||
id="use-sponsorblock"
|
||||
checked={useSponsorblock}
|
||||
onCheckedChange={(checked) => saveSettingsKey('use_sponsorblock', checked)}
|
||||
disabled={useCustomCommands}
|
||||
/>
|
||||
<Label htmlFor="use-sponsorblock">Use Sponsorblock</Label>
|
||||
</div>
|
||||
@@ -874,7 +955,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={sponsorblockMode}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mode', value)}
|
||||
disabled={!useSponsorblock}
|
||||
disabled={!useSponsorblock || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="remove" id="sponsorblock-remove" />
|
||||
@@ -892,7 +973,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={sponsorblockRemove}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_remove', value)}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "remove"}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "remove" || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="default" id="sponsorblock-remove-default" />
|
||||
@@ -913,7 +994,7 @@ export default function SettingsPage() {
|
||||
className="flex flex-col items-start gap-2 mt-1"
|
||||
value={sponsorblockRemove === "custom" ? sponsorblockRemoveCategories : sponsorblockRemove === "default" ? sponsorblockCategories.filter((cat) => cat.code !== 'poi_highlight' && cat.code !== 'filler').map((cat) => cat.code) : sponsorblockRemove === "all" ? sponsorblockCategories.filter((cat) => cat.code !== 'poi_highlight').map((cat) => cat.code) : []}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_remove_categories', value)}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "remove" || sponsorblockRemove !== "custom"}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "remove" || sponsorblockRemove !== "custom" || useCustomCommands}
|
||||
>
|
||||
<div className="flex gap-2 flex-wrap items-center">
|
||||
{sponsorblockCategories.map((category) => (
|
||||
@@ -939,7 +1020,7 @@ export default function SettingsPage() {
|
||||
className="flex items-center gap-4"
|
||||
value={sponsorblockMark}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mark', value)}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "mark"}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "mark" || useCustomCommands}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="default" id="sponsorblock-mark-default" />
|
||||
@@ -960,7 +1041,7 @@ export default function SettingsPage() {
|
||||
className="flex flex-col items-start gap-2 mt-1 mb-2"
|
||||
value={sponsorblockMark === "custom" ? sponsorblockMarkCategories : sponsorblockMark === "default" ? sponsorblockCategories.map((cat) => cat.code) : sponsorblockMark === "all" ? sponsorblockCategories.map((cat) => cat.code) : []}
|
||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mark_categories', value)}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "mark" || sponsorblockMark !== "custom"}
|
||||
disabled={!useSponsorblock || sponsorblockMode !== "mark" || sponsorblockMark !== "custom" || useCustomCommands}
|
||||
>
|
||||
<div className="flex gap-2 flex-wrap items-center">
|
||||
{sponsorblockCategories.map((category) => (
|
||||
@@ -977,7 +1058,109 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {sponsorblockMode === "remove" && sponsorblockRemove === "custom" && sponsorblockRemoveCategories.length <= 0 ? 'No' : sponsorblockMode === "mark" && sponsorblockMark === "custom" && sponsorblockMarkCategories.length <= 0 ? 'No' : 'Yes'}, Mode: {sponsorblockMode === "remove" ? 'Remove' : 'Mark'}, Status: {useSponsorblock ? 'Enabled' : 'Disabled'})</Label>
|
||||
<Label className="text-xs text-muted-foreground">(Configured: {sponsorblockMode === "remove" && sponsorblockRemove === "custom" && sponsorblockRemoveCategories.length <= 0 ? 'No' : sponsorblockMode === "mark" && sponsorblockMark === "custom" && sponsorblockMarkCategories.length <= 0 ? 'No' : 'Yes'}, Mode: {sponsorblockMode === "remove" ? 'Remove' : 'Mark'}, Status: {useSponsorblock && !useCustomCommands ? 'Enabled' : 'Disabled'})</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent key="commands" value="commands" className="flex flex-col gap-4 min-h-[350px]">
|
||||
<div className="custom-commands">
|
||||
<h3 className="font-semibold">Custom Commands</h3>
|
||||
<p className="text-xs text-muted-foreground mb-3"> Run custom yt-dlp commands for your downloads</p>
|
||||
<Alert className="mb-3">
|
||||
<TriangleAlert />
|
||||
<AlertTitle className="text-sm">Most Settings will be Disabled!</AlertTitle>
|
||||
<AlertDescription className="text-xs">
|
||||
This feature is intended for advanced users only. Turning it on will disable most other settings in the app. Make sure you know what you are doing before using this feature, otherwise things could break easily.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Switch
|
||||
id="use-custom-commands"
|
||||
checked={useCustomCommands}
|
||||
onCheckedChange={(checked) => {
|
||||
saveSettingsKey('use_custom_commands', checked)
|
||||
resetDownloadConfiguration();
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="use-custom-commands">Use Custom Commands</Label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Form {...addCustomCommandForm}>
|
||||
<form onSubmit={addCustomCommandForm.handleSubmit(handleAddCustomCommandSubmit)} className="flex flex-col gap-3" autoComplete="off">
|
||||
<FormField
|
||||
control={addCustomCommandForm.control}
|
||||
name="args"
|
||||
disabled={!useCustomCommands}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter yt-dlp command line arguments (no need to start with 'yt-dlp', already passed args: url, output paths, selected formats, selected subtitles, playlist item. also, bulk downloading is not supported)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-4 w-full">
|
||||
<FormField
|
||||
control={addCustomCommandForm.control}
|
||||
name="label"
|
||||
disabled={!useCustomCommands}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="focus-visible:ring-0"
|
||||
placeholder="Enter template label"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{/* <Label htmlFor="label" 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={!watchedLabel || !watchedArgs || Object.keys(addCustomCommandFormErrors).length > 0 || !useCustomCommands}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="flex-flex-col gap-2 mt-4">
|
||||
<Label className="text-xs mb-3">Custom Command Templates</Label>
|
||||
{customCommands.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">NO CUSTOM COMMAND TEMPLATE ADDED YET!</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
{customCommands.map((command) => (
|
||||
<div key={command.id} className="p-2 flex justify-between gap-2 border border-border rounded-md">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-sm mb-1">{command.label}</h5>
|
||||
<p className="text-xs font-mono text-muted-foreground">{command.args}</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
disabled={!useCustomCommands}
|
||||
onClick={() => {
|
||||
handleRemoveCustomCommandSubmit(command.id);
|
||||
}}
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { DownloadState } from '@/types/download';
|
||||
import { DownloadConfiguration } from '@/types/settings';
|
||||
import { RawVideoInfo } from '@/types/video';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface AppContextType {
|
||||
fetchVideoMetadata: (url: string, formatId?: string) => Promise<RawVideoInfo | null>;
|
||||
startDownload: (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
||||
fetchVideoMetadata: (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState) => Promise<RawVideoInfo | null>;
|
||||
startDownload: (url: string, selectedFormat: string, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
||||
pauseDownload: (state: DownloadState) => Promise<void>;
|
||||
resumeDownload: (state: DownloadState) => Promise<void>;
|
||||
cancelDownload: (state: DownloadState) => Promise<void>;
|
||||
|
||||
@@ -202,7 +202,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
embed_thumbnail = $28,
|
||||
sponsorblock_remove = $29,
|
||||
sponsorblock_mark = $30,
|
||||
use_aria2 = $31
|
||||
use_aria2 = $31,
|
||||
custom_command = $32
|
||||
WHERE download_id = $1`,
|
||||
[
|
||||
downloadState.download_id,
|
||||
@@ -235,7 +236,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
downloadState.embed_thumbnail,
|
||||
downloadState.sponsorblock_remove,
|
||||
downloadState.sponsorblock_mark,
|
||||
downloadState.use_aria2
|
||||
downloadState.use_aria2,
|
||||
downloadState.custom_command
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -270,8 +272,9 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
embed_thumbnail,
|
||||
sponsorblock_remove,
|
||||
sponsorblock_mark,
|
||||
use_aria2
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)`,
|
||||
use_aria2,
|
||||
custom_command
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32)`,
|
||||
[
|
||||
downloadState.download_id,
|
||||
downloadState.download_status,
|
||||
@@ -303,7 +306,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
||||
downloadState.embed_thumbnail,
|
||||
downloadState.sponsorblock_remove,
|
||||
downloadState.sponsorblock_mark,
|
||||
downloadState.use_aria2
|
||||
downloadState.use_aria2,
|
||||
downloadState.custom_command
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,22 +47,45 @@ export const useCurrentVideoMetadataStore = create<CurrentVideoMetadataStore>((s
|
||||
|
||||
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({
|
||||
activeDownloadModeTab: 'selective',
|
||||
activeDownloadConfigurationTab: 'options',
|
||||
isStartingDownload: false,
|
||||
selectedDownloadFormat: 'best',
|
||||
selectedCombinableVideoFormat: '',
|
||||
selectedCombinableAudioFormat: '',
|
||||
selectedSubtitles: [],
|
||||
selectedPlaylistVideoIndex: '1',
|
||||
downloadConfiguration: {
|
||||
output_format: null,
|
||||
embed_metadata: null,
|
||||
embed_thumbnail: null,
|
||||
custom_command: null
|
||||
},
|
||||
isErrored: false,
|
||||
isErrorExpected: false,
|
||||
erroredDownloadId: null,
|
||||
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
||||
setActiveDownloadConfigurationTab: (tab) => set(() => ({ activeDownloadConfigurationTab: tab })),
|
||||
setIsStartingDownload: (isStarting) => set(() => ({ isStartingDownload: isStarting })),
|
||||
setSelectedDownloadFormat: (format) => set(() => ({ selectedDownloadFormat: format })),
|
||||
setSelectedCombinableVideoFormat: (format) => set(() => ({ selectedCombinableVideoFormat: format })),
|
||||
setSelectedCombinableAudioFormat: (format) => set(() => ({ selectedCombinableAudioFormat: format })),
|
||||
setSelectedSubtitles: (subtitles) => set(() => ({ selectedSubtitles: subtitles })),
|
||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index })),
|
||||
setDownloadConfigurationKey: (key, value) => set((state) => ({
|
||||
downloadConfiguration: {
|
||||
...state.downloadConfiguration,
|
||||
[key]: value
|
||||
}
|
||||
})),
|
||||
setDownloadConfiguration: (config) => set(() => ({ downloadConfiguration: config })),
|
||||
resetDownloadConfiguration: () => set(() => ({
|
||||
downloadConfiguration: {
|
||||
output_format: null,
|
||||
embed_metadata: null,
|
||||
embed_thumbnail: null,
|
||||
custom_command: null
|
||||
}
|
||||
})),
|
||||
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
|
||||
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
||||
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
||||
@@ -154,6 +177,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
use_aria2: false,
|
||||
use_force_internet_protocol: false,
|
||||
force_internet_protocol: 'ipv4',
|
||||
use_custom_commands: false,
|
||||
custom_commands: [],
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
@@ -212,6 +237,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
||||
use_aria2: false,
|
||||
use_force_internet_protocol: false,
|
||||
force_internet_protocol: 'ipv4',
|
||||
use_custom_commands: false,
|
||||
custom_commands: [],
|
||||
// extension settings
|
||||
websocket_port: 53511
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface DownloadState {
|
||||
sponsorblock_remove: string | null;
|
||||
sponsorblock_mark: string | null;
|
||||
use_aria2: number;
|
||||
custom_command: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -79,6 +80,7 @@ export interface Download {
|
||||
sponsorblock_remove: string | null;
|
||||
sponsorblock_mark: string | null;
|
||||
use_aria2: number;
|
||||
custom_command: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ export interface SettingsTable {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CustomCommand {
|
||||
id: string;
|
||||
label: string;
|
||||
args: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
ytdlp_update_channel: string;
|
||||
ytdlp_auto_update: boolean;
|
||||
@@ -35,6 +41,15 @@ export interface Settings {
|
||||
use_aria2: boolean;
|
||||
use_force_internet_protocol: boolean;
|
||||
force_internet_protocol: string;
|
||||
use_custom_commands: boolean;
|
||||
custom_commands: CustomCommand[];
|
||||
// extension settings
|
||||
websocket_port: number;
|
||||
}
|
||||
|
||||
export interface DownloadConfiguration {
|
||||
output_format: string | null;
|
||||
embed_metadata: boolean | null;
|
||||
embed_thumbnail: boolean | null;
|
||||
custom_command: string | null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownloadState } from "@/types/download";
|
||||
import { RawVideoInfo } from "@/types/video";
|
||||
import { Settings } from "@/types/settings";
|
||||
import { DownloadConfiguration, Settings } from "@/types/settings";
|
||||
import { KvStore } from "@/types/kvStore";
|
||||
import { Update } from "@tauri-apps/plugin-updater";
|
||||
import { Log } from "@/types/logs";
|
||||
@@ -37,22 +37,28 @@ export interface CurrentVideoMetadataStore {
|
||||
|
||||
export interface DownloaderPageStatesStore {
|
||||
activeDownloadModeTab: string;
|
||||
activeDownloadConfigurationTab: string;
|
||||
isStartingDownload: boolean;
|
||||
selectedDownloadFormat: string;
|
||||
selectedCombinableVideoFormat: string;
|
||||
selectedCombinableAudioFormat: string;
|
||||
selectedSubtitles: string[];
|
||||
selectedPlaylistVideoIndex: string;
|
||||
downloadConfiguration: DownloadConfiguration;
|
||||
isErrored: boolean;
|
||||
isErrorExpected: boolean;
|
||||
erroredDownloadId: string | null;
|
||||
setActiveDownloadModeTab: (tab: string) => void;
|
||||
setActiveDownloadConfigurationTab: (tab: string) => void;
|
||||
setIsStartingDownload: (isStarting: boolean) => void;
|
||||
setSelectedDownloadFormat: (format: string) => void;
|
||||
setSelectedCombinableVideoFormat: (format: string) => void;
|
||||
setSelectedCombinableAudioFormat: (format: string) => void;
|
||||
setSelectedSubtitles: (subtitles: string[]) => void;
|
||||
setSelectedPlaylistVideoIndex: (index: string) => void;
|
||||
setDownloadConfigurationKey: (key: string, value: unknown) => void;
|
||||
setDownloadConfiguration: (config: DownloadConfiguration) => void;
|
||||
resetDownloadConfiguration: () => void;
|
||||
setIsErrored: (isErrored: boolean) => void;
|
||||
setIsErrorExpected: (isErrorExpected: boolean) => void;
|
||||
setErroredDownloadId: (downloadId: string | null) => void;
|
||||
|
||||
@@ -209,6 +209,10 @@ export const formatCodec = (codec: string) => {
|
||||
return codec.toUpperCase();
|
||||
}
|
||||
|
||||
export const generateID = () => {
|
||||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
export const generateDownloadId = (videoId: string, host: string) => {
|
||||
host = host.trim().split('.')[0];
|
||||
return `${host}_${videoId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
Reference in New Issue
Block a user