mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2025-12-20 00:49: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;
|
END;
|
||||||
",
|
",
|
||||||
kind: MigrationKind::Up,
|
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 { Toaster as Sonner } from "@/components/ui/sonner";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useLogger } from "@/helpers/use-logger";
|
import { useLogger } from "@/helpers/use-logger";
|
||||||
|
import { DownloadConfiguration } from "./types/settings";
|
||||||
|
|
||||||
export default function App({ children }: { children: React.ReactNode }) {
|
export default function App({ children }: { children: React.ReactNode }) {
|
||||||
const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates();
|
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_ARIA2 = useSettingsPageStatesStore(state => state.settings.use_aria2);
|
||||||
const USE_FORCE_INTERNET_PROTOCOL = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol);
|
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 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 isErrored = useDownloaderPageStatesStore((state) => state.isErrored);
|
||||||
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected);
|
||||||
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId);
|
const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId);
|
||||||
|
const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration);
|
||||||
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
|
const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored);
|
||||||
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected);
|
||||||
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
|
const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId);
|
||||||
@@ -118,23 +122,25 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
const hasRunYtDlpAutoUpdateRef = useRef(false);
|
||||||
const isRegisteredToMacOsRef = 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 {
|
try {
|
||||||
const args = [url, '--dump-single-json', '--no-warnings'];
|
const args = [url, '--dump-single-json', '--no-warnings'];
|
||||||
if (formatId) args.push('-f', formatId);
|
if (formatId) args.push('-f', formatId);
|
||||||
|
if (selectedSubtitles) args.push('--embed-subs', '--sub-lang', selectedSubtitles);
|
||||||
if (playlistIndex) args.push('--playlist-items', playlistIndex);
|
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-all-formats');
|
||||||
if (STRICT_DOWNLOADABILITY_CHECK && formatId) args.push('--check-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') {
|
if (FORCE_INTERNET_PROTOCOL === 'ipv4') {
|
||||||
args.push('--force-ipv4');
|
args.push('--force-ipv4');
|
||||||
} else if (FORCE_INTERNET_PROTOCOL === 'ipv6') {
|
} else if (FORCE_INTERNET_PROTOCOL === 'ipv6') {
|
||||||
args.push('--force-ipv6');
|
args.push('--force-ipv6');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (USE_COOKIES) {
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_COOKIES) {
|
||||||
if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) {
|
if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) {
|
||||||
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
||||||
} else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) {
|
} 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}`);
|
LOG.info('NEODLP', `Initiating yt-dlp download for URL: ${url}`);
|
||||||
// set error states to default
|
// set error states to default
|
||||||
setIsErrored(false);
|
setIsErrored(false);
|
||||||
setIsErrorExpected(false);
|
setIsErrorExpected(false);
|
||||||
setErroredDownloadId(null);
|
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) {
|
if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) {
|
||||||
console.error('FFmpeg or download paths not found');
|
console.error('FFmpeg or download paths not found');
|
||||||
return;
|
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 isPlaylist = (playlistItems && typeof playlistItems === 'string') || (resumeState?.playlist_id && resumeState?.playlist_index) ? true : false;
|
||||||
const playlistIndex = isPlaylist ? (resumeState?.playlist_index?.toString() || playlistItems) : null;
|
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) {
|
if (!videoMetadata) {
|
||||||
console.error('Failed to fetch video metadata');
|
console.error('Failed to fetch video metadata');
|
||||||
toast.error("Download Failed", {
|
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;
|
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;
|
if (resumeState && resumeState.output_format) videoMetadata.ext = resumeState.output_format;
|
||||||
|
|
||||||
const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain);
|
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);
|
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;
|
let outputFormat = null;
|
||||||
if (fileType !== 'unknown' && ((VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto') || resumeState?.output_format)) {
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && ((fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) || configOutputFormat || 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)));
|
const format = resumeState?.output_format || configOutputFormat;
|
||||||
if ((VIDEO_FORMAT !== 'auto' && fileType === 'video+audio') || (resumeState?.output_format && fileType === 'video+audio')) {
|
|
||||||
if (ALWAYS_REENCODE_VIDEO) {
|
if (format) {
|
||||||
args.push('--recode-video', resumeState?.output_format || VIDEO_FORMAT);
|
outputFormat = format;
|
||||||
} else {
|
} else if (fileType === 'audio' && AUDIO_FORMAT !== 'auto') {
|
||||||
args.push('--merge-output-format', resumeState?.output_format || VIDEO_FORMAT);
|
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')) {
|
// Handle video only
|
||||||
if (ALWAYS_REENCODE_VIDEO) {
|
else if (fileType === 'video' && (VIDEO_FORMAT !== 'auto' || format)) {
|
||||||
args.push('--recode-video', resumeState?.output_format || VIDEO_FORMAT);
|
args.push(recodeOrRemux, formatToUse);
|
||||||
} else {
|
|
||||||
args.push('--remux-video', resumeState?.output_format || VIDEO_FORMAT);
|
|
||||||
}
|
}
|
||||||
|
// 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;
|
let embedMetadata = 0;
|
||||||
if (fileType !== 'unknown' && ((EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA) || resumeState?.embed_metadata)) {
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) {
|
||||||
if ((EMBED_VIDEO_METADATA || resumeState?.embed_metadata) && (fileType === 'video+audio' || fileType === 'video')) {
|
const shouldEmbedForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || (EMBED_VIDEO_METADATA && downloadConfiguration.embed_metadata === null));
|
||||||
embedMetadata = 1;
|
const shouldEmbedForAudio = fileType === 'audio' && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata || (EMBED_AUDIO_METADATA && downloadConfiguration.embed_metadata === null));
|
||||||
args.push('--embed-metadata');
|
const shouldEmbedForUnknown = fileType === 'unknown' && (downloadConfiguration.embed_metadata || resumeState?.embed_metadata);
|
||||||
}
|
|
||||||
if ((EMBED_AUDIO_METADATA || resumeState?.embed_metadata) && fileType === 'audio') {
|
if (shouldEmbedForUnknown || shouldEmbedForVideo || shouldEmbedForAudio) {
|
||||||
embedMetadata = 1;
|
embedMetadata = 1;
|
||||||
args.push('--embed-metadata');
|
args.push('--embed-metadata');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let embedThumbnail = 0;
|
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;
|
embedThumbnail = 1;
|
||||||
args.push('--embed-thumbnail');
|
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);
|
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}`);
|
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') {
|
if (FORCE_INTERNET_PROTOCOL === 'ipv4') {
|
||||||
args.push('--force-ipv4');
|
args.push('--force-ipv4');
|
||||||
} else if (FORCE_INTERNET_PROTOCOL === 'ipv6') {
|
} 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) {
|
if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) {
|
||||||
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
args.push('--cookies-from-browser', COOKIES_BROWSER);
|
||||||
} else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) {
|
} else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) {
|
||||||
@@ -331,7 +369,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
let sponsorblockRemove = null;
|
let sponsorblockRemove = null;
|
||||||
let sponsorblockMark = 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) {
|
if (SPONSORBLOCK_MODE === 'remove' || resumeState?.sponsorblock_remove) {
|
||||||
sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? (
|
sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? (
|
||||||
SPONSORBLOCK_REMOVE_CATEGORIES.length > 0 ? SPONSORBLOCK_REMOVE_CATEGORIES.join(',') : 'default'
|
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;
|
let useAria2 = 0;
|
||||||
if (USE_ARIA2 || resumeState?.use_aria2) {
|
if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (USE_ARIA2 || resumeState?.use_aria2)) {
|
||||||
useAria2 = 1;
|
useAria2 = 1;
|
||||||
args.push(
|
args.push(
|
||||||
'--downloader', 'aria2c',
|
'--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).`);
|
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');
|
args.push('--continue');
|
||||||
} else {
|
} else {
|
||||||
args.push('--no-continue');
|
args.push('--no-continue');
|
||||||
@@ -459,7 +497,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
embed_thumbnail: embedThumbnail,
|
embed_thumbnail: embedThumbnail,
|
||||||
sponsorblock_remove: sponsorblockRemove,
|
sponsorblock_remove: sponsorblockRemove,
|
||||||
sponsorblock_mark: sponsorblockMark,
|
sponsorblock_mark: sponsorblockMark,
|
||||||
use_aria2: useAria2
|
use_aria2: useAria2,
|
||||||
|
custom_command: customCommandArgs,
|
||||||
};
|
};
|
||||||
downloadStateSaver.mutate(state, {
|
downloadStateSaver.mutate(state, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -551,7 +590,8 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
embed_thumbnail: resumeState?.embed_thumbnail || 0,
|
embed_thumbnail: resumeState?.embed_thumbnail || 0,
|
||||||
sponsorblock_remove: resumeState?.sponsorblock_remove || null,
|
sponsorblock_remove: resumeState?.sponsorblock_remove || null,
|
||||||
sponsorblock_mark: resumeState?.sponsorblock_mark || 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, {
|
downloadStateSaver.mutate(state, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -643,6 +683,12 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
await startDownload(
|
await startDownload(
|
||||||
downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url,
|
downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url,
|
||||||
downloadState.format_id,
|
downloadState.format_id,
|
||||||
|
{
|
||||||
|
output_format: null,
|
||||||
|
embed_metadata: null,
|
||||||
|
embed_thumbnail: null,
|
||||||
|
custom_command: null
|
||||||
|
},
|
||||||
downloadState.subtitle_id,
|
downloadState.subtitle_id,
|
||||||
downloadState
|
downloadState
|
||||||
);
|
);
|
||||||
@@ -739,6 +785,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
|||||||
await startDownload(
|
await startDownload(
|
||||||
downloadToStart.url,
|
downloadToStart.url,
|
||||||
downloadToStart.format_id,
|
downloadToStart.format_id,
|
||||||
|
downloadConfiguration,
|
||||||
downloadToStart.subtitle_id,
|
downloadToStart.subtitle_id,
|
||||||
downloadToStart
|
downloadToStart
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function Navbar() {
|
|||||||
<DialogContent className="sm:max-w-[600px]">
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Log Viewer</DialogTitle>
|
<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>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-2 p-2 max-h-[300px] overflow-y-scroll overflow-x-hidden bg-muted">
|
<div className="flex flex-col gap-2 p-2 max-h-[300px] overflow-y-scroll overflow-x-hidden bg-muted">
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { toast } from "sonner";
|
|||||||
import { useAppContext } from "@/providers/appContextProvider";
|
import { useAppContext } from "@/providers/appContextProvider";
|
||||||
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
import { useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useSettingsPageStatesStore } from "@/services/store";
|
||||||
import { determineFileType, fileFormatFilter, formatBitrate, formatDurationString, formatFileSize, formatReleaseDate, formatYtStyleCount, isObjEmpty, sortByBitrate } from "@/utils";
|
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 { FormatSelectionGroup, FormatSelectionGroupItem } from "@/components/custom/formatSelectionGroup";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
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({
|
const searchFormSchema = z.object({
|
||||||
url: z.url({
|
url: z.url({
|
||||||
@@ -51,22 +56,32 @@ export default function DownloaderPage() {
|
|||||||
const setShowSearchError = useCurrentVideoMetadataStore((state) => state.setShowSearchError);
|
const setShowSearchError = useCurrentVideoMetadataStore((state) => state.setShowSearchError);
|
||||||
|
|
||||||
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
|
const activeDownloadModeTab = useDownloaderPageStatesStore((state) => state.activeDownloadModeTab);
|
||||||
|
const activeDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.activeDownloadConfigurationTab);
|
||||||
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
const isStartingDownload = useDownloaderPageStatesStore((state) => state.isStartingDownload);
|
||||||
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
|
const selectedDownloadFormat = useDownloaderPageStatesStore((state) => state.selectedDownloadFormat);
|
||||||
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
|
const selectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableVideoFormat);
|
||||||
const selectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormat);
|
const selectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.selectedCombinableAudioFormat);
|
||||||
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
|
const selectedSubtitles = useDownloaderPageStatesStore((state) => state.selectedSubtitles);
|
||||||
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
|
const selectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.selectedPlaylistVideoIndex);
|
||||||
|
const downloadConfiguration = useDownloaderPageStatesStore((state) => state.downloadConfiguration);
|
||||||
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
|
const setActiveDownloadModeTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadModeTab);
|
||||||
|
const setActiveDownloadConfigurationTab = useDownloaderPageStatesStore((state) => state.setActiveDownloadConfigurationTab);
|
||||||
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
|
const setIsStartingDownload = useDownloaderPageStatesStore((state) => state.setIsStartingDownload);
|
||||||
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
|
const setSelectedDownloadFormat = useDownloaderPageStatesStore((state) => state.setSelectedDownloadFormat);
|
||||||
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
|
const setSelectedCombinableVideoFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableVideoFormat);
|
||||||
const setSelectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormat);
|
const setSelectedCombinableAudioFormat = useDownloaderPageStatesStore((state) => state.setSelectedCombinableAudioFormat);
|
||||||
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
|
const setSelectedSubtitles = useDownloaderPageStatesStore((state) => state.setSelectedSubtitles);
|
||||||
const setSelectedPlaylistVideoIndex = useDownloaderPageStatesStore((state) => state.setSelectedPlaylistVideoIndex);
|
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 videoFormat = useSettingsPageStatesStore(state => state.settings.video_format);
|
||||||
const audioFormat = useSettingsPageStatesStore(state => state.settings.audio_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 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'))) : [];
|
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';
|
let selectedFormatExtensionMsg = 'Auto - unknown';
|
||||||
if (activeDownloadModeTab === 'combine') {
|
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()}`;
|
selectedFormatExtensionMsg = `Combined - ${videoFormat.toUpperCase()}`;
|
||||||
}
|
}
|
||||||
else if (selectedAudioFormat?.ext && selectedVideoFormat?.ext) {
|
else if (selectedAudioFormat?.ext && selectedVideoFormat?.ext) {
|
||||||
@@ -155,10 +173,12 @@ export default function DownloaderPage() {
|
|||||||
selectedFormatExtensionMsg = `Combined - unknown`;
|
selectedFormatExtensionMsg = `Combined - unknown`;
|
||||||
}
|
}
|
||||||
} else if (selectedFormat?.ext) {
|
} else if (selectedFormat?.ext) {
|
||||||
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && videoFormat !== 'auto') {
|
if ((selectedFormatFileType === 'video+audio' || selectedFormatFileType === 'video') && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || videoFormat !== 'auto')) {
|
||||||
selectedFormatExtensionMsg = `Forced - ${videoFormat.toUpperCase()}`;
|
selectedFormatExtensionMsg = `Forced - ${(downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') ? downloadConfiguration.output_format.toUpperCase() : videoFormat.toUpperCase()}`;
|
||||||
} else if (selectedFormatFileType === 'audio' && audioFormat !== 'auto') {
|
} else if (selectedFormatFileType === 'audio' && ((downloadConfiguration.output_format && downloadConfiguration.output_format !== 'auto') || audioFormat !== 'auto')) {
|
||||||
selectedFormatExtensionMsg = `Forced - ${audioFormat.toUpperCase()}`;
|
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 {
|
} else {
|
||||||
selectedFormatExtensionMsg = `Auto - ${selectedFormat.ext.toUpperCase()}`;
|
selectedFormatExtensionMsg = `Auto - ${selectedFormat.ext.toUpperCase()}`;
|
||||||
}
|
}
|
||||||
@@ -220,6 +240,7 @@ export default function DownloaderPage() {
|
|||||||
setSelectedCombinableAudioFormat('');
|
setSelectedCombinableAudioFormat('');
|
||||||
setSelectedSubtitles([]);
|
setSelectedSubtitles([]);
|
||||||
setSelectedPlaylistVideoIndex('1');
|
setSelectedPlaylistVideoIndex('1');
|
||||||
|
resetDownloadConfiguration();
|
||||||
|
|
||||||
fetchVideoMetadata(values.url).then((metadata) => {
|
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)) {
|
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(() => {
|
useEffect(() => {
|
||||||
if (watchedUrl !== videoUrl) {
|
if (watchedUrl !== videoUrl) {
|
||||||
setVideoUrl(watchedUrl);
|
setVideoUrl(watchedUrl);
|
||||||
@@ -435,7 +460,12 @@ export default function DownloaderPage() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
className=""
|
className=""
|
||||||
value={activeDownloadModeTab}
|
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">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-sm flex items-center gap-2">
|
<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') {
|
// if (currentlySelectedFormat?.ext !== 'mp4' && currentlySelectedFormat?.ext !== 'mkv' && currentlySelectedFormat?.ext !== 'webm') {
|
||||||
// setSelectedSubtitles([]);
|
// setSelectedSubtitles([]);
|
||||||
// }
|
// }
|
||||||
|
setDownloadConfigurationKey('output_format', null);
|
||||||
|
setDownloadConfigurationKey('embed_metadata', null);
|
||||||
|
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-xs">Suggested</p>
|
<p className="text-xs">Suggested</p>
|
||||||
@@ -581,6 +614,9 @@ export default function DownloaderPage() {
|
|||||||
value={selectedCombinableAudioFormat}
|
value={selectedCombinableAudioFormat}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setSelectedCombinableAudioFormat(value);
|
setSelectedCombinableAudioFormat(value);
|
||||||
|
setDownloadConfigurationKey('output_format', null);
|
||||||
|
setDownloadConfigurationKey('embed_metadata', null);
|
||||||
|
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
|
{videoOnlyFormats && videoOnlyFormats.length > 0 && audioOnlyFormats && audioOnlyFormats.length > 0 && (
|
||||||
@@ -602,6 +638,9 @@ export default function DownloaderPage() {
|
|||||||
value={selectedCombinableVideoFormat}
|
value={selectedCombinableVideoFormat}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setSelectedCombinableVideoFormat(value);
|
setSelectedCombinableVideoFormat(value);
|
||||||
|
setDownloadConfigurationKey('output_format', null);
|
||||||
|
setDownloadConfigurationKey('embed_metadata', null);
|
||||||
|
setDownloadConfigurationKey('embed_thumbnail', null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
|
{audioOnlyFormats && audioOnlyFormats.length > 0 && videoOnlyFormats && videoOnlyFormats.length > 0 && (
|
||||||
@@ -668,6 +707,7 @@ export default function DownloaderPage() {
|
|||||||
setSelectedSubtitles([]);
|
setSelectedSubtitles([]);
|
||||||
setSelectedCombinableVideoFormat('');
|
setSelectedCombinableVideoFormat('');
|
||||||
setSelectedCombinableAudioFormat('');
|
setSelectedCombinableAudioFormat('');
|
||||||
|
resetDownloadConfiguration();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{videoMetadata.entries.map((entry) => entry ? (
|
{videoMetadata.entries.map((entry) => entry ? (
|
||||||
@@ -909,6 +949,198 @@ export default function DownloaderPage() {
|
|||||||
<span className="text-xs text-muted-foreground">{selectedFormatFinalMsg}</span>
|
<span className="text-xs text-muted-foreground">{selectedFormatFinalMsg}</span>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsStartingDownload(true);
|
setIsStartingDownload(true);
|
||||||
@@ -917,6 +1149,7 @@ export default function DownloaderPage() {
|
|||||||
await startDownload(
|
await startDownload(
|
||||||
videoMetadata.original_url,
|
videoMetadata.original_url,
|
||||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat,
|
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.entries[Number(selectedPlaylistVideoIndex) - 1].requested_downloads[0].format_id : selectedDownloadFormat,
|
||||||
|
downloadConfiguration,
|
||||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
|
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null,
|
||||||
undefined,
|
undefined,
|
||||||
selectedPlaylistVideoIndex
|
selectedPlaylistVideoIndex
|
||||||
@@ -925,6 +1158,7 @@ export default function DownloaderPage() {
|
|||||||
await startDownload(
|
await startDownload(
|
||||||
videoMetadata.webpage_url,
|
videoMetadata.webpage_url,
|
||||||
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
|
activeDownloadModeTab === 'combine' ? `${selectedCombinableVideoFormat}+${selectedCombinableAudioFormat}` : selectedDownloadFormat === 'best' ? videoMetadata.requested_downloads[0].format_id : selectedDownloadFormat,
|
||||||
|
downloadConfiguration,
|
||||||
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
|
selectedSubtitles.length > 0 ? selectedSubtitles.join(',') : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -941,7 +1175,7 @@ export default function DownloaderPage() {
|
|||||||
setIsStartingDownload(false);
|
setIsStartingDownload(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat))}
|
disabled={isStartingDownload || !selectedDownloadFormat || (activeDownloadModeTab === 'combine' && (!selectedCombinableVideoFormat || !selectedCombinableAudioFormat)) || (useCustomCommands && !downloadConfiguration.custom_command)}
|
||||||
>
|
>
|
||||||
{isStartingDownload ? (
|
{isStartingDownload ? (
|
||||||
<>
|
<>
|
||||||
@@ -953,6 +1187,7 @@ export default function DownloaderPage() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Heading from "@/components/heading";
|
import Heading from "@/components/heading";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
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 { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { toast } from "sonner";
|
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 { cn } from "@/lib/utils";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTheme } from "@/providers/themeProvider";
|
import { useTheme } from "@/providers/themeProvider";
|
||||||
@@ -26,8 +26,10 @@ 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";
|
import { formatSpeed, generateID } from "@/utils";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/custom/legacyToggleGroup";
|
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({
|
const websocketPortSchema = z.object({
|
||||||
port: z.coerce.number<number>({
|
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() {
|
export default function SettingsPage() {
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
@@ -109,6 +116,8 @@ export default function SettingsPage() {
|
|||||||
const useAria2 = useSettingsPageStatesStore(state => state.settings.use_aria2);
|
const useAria2 = useSettingsPageStatesStore(state => state.settings.use_aria2);
|
||||||
const useForceInternetProtocol = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol);
|
const useForceInternetProtocol = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol);
|
||||||
const forceInternetProtocol = useSettingsPageStatesStore(state => state.settings.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 websocketPort = useSettingsPageStatesStore(state => state.settings.websocket_port);
|
||||||
const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort);
|
const isChangingWebSocketPort = useSettingsPageStatesStore(state => state.isChangingWebSocketPort);
|
||||||
@@ -116,6 +125,9 @@ export default function SettingsPage() {
|
|||||||
const isRestartingWebSocketServer = useSettingsPageStatesStore(state => state.isRestartingWebSocketServer);
|
const isRestartingWebSocketServer = useSettingsPageStatesStore(state => state.isRestartingWebSocketServer);
|
||||||
const setIsRestartingWebSocketServer = useSettingsPageStatesStore(state => state.setIsRestartingWebSocketServer);
|
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 downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||||
const ongoingDownloads = downloadStates.filter(state =>
|
const ongoingDownloads = downloadStates.filter(state =>
|
||||||
['starting', 'downloading', 'queued'].includes(state.download_status)
|
['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 {
|
interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
@@ -422,9 +484,14 @@ export default function SettingsPage() {
|
|||||||
value="sponsorblock"
|
value="sponsorblock"
|
||||||
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start px-3 py-1.5 gap-2"
|
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>
|
><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>
|
</TabsList>
|
||||||
<div className="min-h-full flex flex-col max-w-[55%] w-full border-l border-border pl-4">
|
<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">
|
<div className="max-parallel-downloads">
|
||||||
<h3 className="font-semibold">Max Parallel Downloads</h3>
|
<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>
|
<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"
|
id="aria2"
|
||||||
checked={useAria2}
|
checked={useAria2}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_aria2', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('use_aria2', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<div className="app-theme">
|
||||||
<h3 className="font-semibold">Theme</h3>
|
<h3 className="font-semibold">Theme</h3>
|
||||||
<p className="text-xs text-muted-foreground mb-3">Choose app interface theme</p>
|
<p className="text-xs text-muted-foreground mb-3">Choose app interface theme</p>
|
||||||
@@ -502,7 +570,7 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<div className="download-dir">
|
||||||
<h3 className="font-semibold">Download Folder</h3>
|
<h3 className="font-semibold">Download Folder</h3>
|
||||||
<p className="text-xs text-muted-foreground mb-3">Set default download folder (directory)</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<div className="video-format">
|
||||||
<h3 className="font-semibold">Video Format</h3>
|
<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>
|
<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"
|
className="flex items-center gap-4"
|
||||||
value={videoFormat}
|
value={videoFormat}
|
||||||
onValueChange={(value) => saveSettingsKey('video_format', value)}
|
onValueChange={(value) => saveSettingsKey('video_format', value)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="auto" id="v-auto" />
|
<RadioGroupItem value="auto" id="v-auto" />
|
||||||
@@ -598,6 +667,7 @@ export default function SettingsPage() {
|
|||||||
className="flex items-center gap-4"
|
className="flex items-center gap-4"
|
||||||
value={audioFormat}
|
value={audioFormat}
|
||||||
onValueChange={(value) => saveSettingsKey('audio_format', value)}
|
onValueChange={(value) => saveSettingsKey('audio_format', value)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="auto" id="a-auto" />
|
<RadioGroupItem value="auto" id="a-auto" />
|
||||||
@@ -624,10 +694,11 @@ export default function SettingsPage() {
|
|||||||
id="always-reencode-video"
|
id="always-reencode-video"
|
||||||
checked={alwaysReencodeVideo}
|
checked={alwaysReencodeVideo}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('always_reencode_video', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('always_reencode_video', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<div className="embed-video-metadata">
|
||||||
<h3 className="font-semibold">Embed Metadata</h3>
|
<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>
|
<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"
|
id="embed-video-metadata"
|
||||||
checked={embedVideoMetadata}
|
checked={embedVideoMetadata}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('embed_video_metadata', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('embed_video_metadata', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="embed-video-metadata">Video</Label>
|
<Label htmlFor="embed-video-metadata">Video</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -644,6 +716,7 @@ export default function SettingsPage() {
|
|||||||
id="embed-audio-metadata"
|
id="embed-audio-metadata"
|
||||||
checked={embedAudioMetadata}
|
checked={embedAudioMetadata}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('embed_audio_metadata', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('embed_audio_metadata', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="embed-audio-metadata">Audio</Label>
|
<Label htmlFor="embed-audio-metadata">Audio</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -655,10 +728,11 @@ export default function SettingsPage() {
|
|||||||
id="embed-audio-thumbnail"
|
id="embed-audio-thumbnail"
|
||||||
checked={embedAudioThumbnail}
|
checked={embedAudioThumbnail}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('embed_audio_thumbnail', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('embed_audio_thumbnail', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<div className="proxy">
|
||||||
<h3 className="font-semibold">Proxy</h3>
|
<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>
|
<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"
|
id="use-proxy"
|
||||||
checked={useProxy}
|
checked={useProxy}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_proxy', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('use_proxy', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="use-proxy">Use Proxy</Label>
|
<Label htmlFor="use-proxy">Use Proxy</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -682,10 +757,11 @@ export default function SettingsPage() {
|
|||||||
<Input
|
<Input
|
||||||
className="focus-visible:ring-0"
|
className="focus-visible:ring-0"
|
||||||
placeholder="Enter proxy URL"
|
placeholder="Enter proxy URL"
|
||||||
|
readOnly={useCustomCommands}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</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 />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -707,6 +783,7 @@ export default function SettingsPage() {
|
|||||||
id="use-rate-limit"
|
id="use-rate-limit"
|
||||||
checked={useRateLimit}
|
checked={useRateLimit}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_rate_limit', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('use_rate_limit', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="use-rate-limit">Use Rate Limit</Label>
|
<Label htmlFor="use-rate-limit">Use Rate Limit</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -722,10 +799,11 @@ export default function SettingsPage() {
|
|||||||
<Input
|
<Input
|
||||||
className="focus-visible:ring-0"
|
className="focus-visible:ring-0"
|
||||||
placeholder="Enter rate limit in bytes/s"
|
placeholder="Enter rate limit in bytes/s"
|
||||||
|
readOnly={useCustomCommands}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</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 />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -741,12 +819,13 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="force-internet-protocol">
|
<div className="force-internet-protocol">
|
||||||
<h3 className="font-semibold">Force Internet Protocol</h3>
|
<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">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<Switch
|
<Switch
|
||||||
id="use-force-internet-protocol"
|
id="use-force-internet-protocol"
|
||||||
checked={useForceInternetProtocol}
|
checked={useForceInternetProtocol}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_force_internet_protocol', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('use_force_internet_protocol', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="use-force-internet-protocol">Force</Label>
|
<Label htmlFor="use-force-internet-protocol">Force</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -755,7 +834,7 @@ export default function SettingsPage() {
|
|||||||
className="flex items-center gap-4 mb-2"
|
className="flex items-center gap-4 mb-2"
|
||||||
value={forceInternetProtocol}
|
value={forceInternetProtocol}
|
||||||
onValueChange={(value) => saveSettingsKey('force_internet_protocol', value)}
|
onValueChange={(value) => saveSettingsKey('force_internet_protocol', value)}
|
||||||
disabled={!useForceInternetProtocol}
|
disabled={!useForceInternetProtocol || useCustomCommands}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="ipv4" id="force-ipv4" />
|
<RadioGroupItem value="ipv4" id="force-ipv4" />
|
||||||
@@ -766,10 +845,10 @@ export default function SettingsPage() {
|
|||||||
<Label htmlFor="force-ipv6">Use IPv6 Only</Label>
|
<Label htmlFor="force-ipv6">Use IPv6 Only</Label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</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>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<div className="cookies">
|
||||||
<h3 className="font-semibold">Cookies</h3>
|
<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>
|
<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"
|
id="use-cookies"
|
||||||
checked={useCookies}
|
checked={useCookies}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_cookies', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('use_cookies', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="use-cookies">Use Cookies</Label>
|
<Label htmlFor="use-cookies">Use Cookies</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -786,7 +866,7 @@ export default function SettingsPage() {
|
|||||||
className="flex items-center gap-4"
|
className="flex items-center gap-4"
|
||||||
value={importCookiesFrom}
|
value={importCookiesFrom}
|
||||||
onValueChange={(value) => saveSettingsKey('import_cookies_from', value)}
|
onValueChange={(value) => saveSettingsKey('import_cookies_from', value)}
|
||||||
disabled={!useCookies}
|
disabled={!useCookies || useCustomCommands}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="browser" id="cookies-browser" />
|
<RadioGroupItem value="browser" id="cookies-browser" />
|
||||||
@@ -802,7 +882,7 @@ export default function SettingsPage() {
|
|||||||
<Select
|
<Select
|
||||||
value={cookiesBrowser}
|
value={cookiesBrowser}
|
||||||
onValueChange={(value) => saveSettingsKey('cookies_browser', value)}
|
onValueChange={(value) => saveSettingsKey('cookies_browser', value)}
|
||||||
disabled={importCookiesFrom !== "browser" || !useCookies}
|
disabled={importCookiesFrom !== "browser" || !useCookies || useCustomCommands}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[230px] ring-0 focus:ring-0">
|
<SelectTrigger className="w-[230px] ring-0 focus:ring-0">
|
||||||
<SelectValue placeholder="Select browser to import cookies" />
|
<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/>
|
<Input className="focus-visible:ring-0" type="text" placeholder="Select cookies text file" value={cookiesFile ?? ''} disabled={importCookiesFrom !== "file" || !useCookies} readOnly/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={importCookiesFrom !== "file" || !useCookies}
|
disabled={importCookiesFrom !== "file" || !useCookies || useCustomCommands}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const file = await open({
|
const file = await open({
|
||||||
@@ -854,10 +934,10 @@ export default function SettingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</TabsContent>
|
</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">
|
<div className="sponsorblock">
|
||||||
<h3 className="font-semibold">Sponsor Block</h3>
|
<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>
|
<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"
|
id="use-sponsorblock"
|
||||||
checked={useSponsorblock}
|
checked={useSponsorblock}
|
||||||
onCheckedChange={(checked) => saveSettingsKey('use_sponsorblock', checked)}
|
onCheckedChange={(checked) => saveSettingsKey('use_sponsorblock', checked)}
|
||||||
|
disabled={useCustomCommands}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="use-sponsorblock">Use Sponsorblock</Label>
|
<Label htmlFor="use-sponsorblock">Use Sponsorblock</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -874,7 +955,7 @@ export default function SettingsPage() {
|
|||||||
className="flex items-center gap-4"
|
className="flex items-center gap-4"
|
||||||
value={sponsorblockMode}
|
value={sponsorblockMode}
|
||||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mode', value)}
|
onValueChange={(value) => saveSettingsKey('sponsorblock_mode', value)}
|
||||||
disabled={!useSponsorblock}
|
disabled={!useSponsorblock || useCustomCommands}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="remove" id="sponsorblock-remove" />
|
<RadioGroupItem value="remove" id="sponsorblock-remove" />
|
||||||
@@ -892,7 +973,7 @@ export default function SettingsPage() {
|
|||||||
className="flex items-center gap-4"
|
className="flex items-center gap-4"
|
||||||
value={sponsorblockRemove}
|
value={sponsorblockRemove}
|
||||||
onValueChange={(value) => saveSettingsKey('sponsorblock_remove', value)}
|
onValueChange={(value) => saveSettingsKey('sponsorblock_remove', value)}
|
||||||
disabled={!useSponsorblock || sponsorblockMode !== "remove"}
|
disabled={!useSponsorblock || sponsorblockMode !== "remove" || useCustomCommands}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="default" id="sponsorblock-remove-default" />
|
<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"
|
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) : []}
|
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)}
|
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">
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
{sponsorblockCategories.map((category) => (
|
{sponsorblockCategories.map((category) => (
|
||||||
@@ -939,7 +1020,7 @@ export default function SettingsPage() {
|
|||||||
className="flex items-center gap-4"
|
className="flex items-center gap-4"
|
||||||
value={sponsorblockMark}
|
value={sponsorblockMark}
|
||||||
onValueChange={(value) => saveSettingsKey('sponsorblock_mark', value)}
|
onValueChange={(value) => saveSettingsKey('sponsorblock_mark', value)}
|
||||||
disabled={!useSponsorblock || sponsorblockMode !== "mark"}
|
disabled={!useSponsorblock || sponsorblockMode !== "mark" || useCustomCommands}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem value="default" id="sponsorblock-mark-default" />
|
<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"
|
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) : []}
|
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)}
|
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">
|
<div className="flex gap-2 flex-wrap items-center">
|
||||||
{sponsorblockCategories.map((category) => (
|
{sponsorblockCategories.map((category) => (
|
||||||
@@ -977,7 +1058,109 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { DownloadState } from '@/types/download';
|
import { DownloadState } from '@/types/download';
|
||||||
|
import { DownloadConfiguration } from '@/types/settings';
|
||||||
import { RawVideoInfo } from '@/types/video';
|
import { RawVideoInfo } from '@/types/video';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
interface AppContextType {
|
interface AppContextType {
|
||||||
fetchVideoMetadata: (url: string, formatId?: string) => Promise<RawVideoInfo | null>;
|
fetchVideoMetadata: (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState) => Promise<RawVideoInfo | null>;
|
||||||
startDownload: (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
startDownload: (url: string, selectedFormat: string, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => Promise<void>;
|
||||||
pauseDownload: (state: DownloadState) => Promise<void>;
|
pauseDownload: (state: DownloadState) => Promise<void>;
|
||||||
resumeDownload: (state: DownloadState) => Promise<void>;
|
resumeDownload: (state: DownloadState) => Promise<void>;
|
||||||
cancelDownload: (state: DownloadState) => Promise<void>;
|
cancelDownload: (state: DownloadState) => Promise<void>;
|
||||||
|
|||||||
@@ -202,7 +202,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
|||||||
embed_thumbnail = $28,
|
embed_thumbnail = $28,
|
||||||
sponsorblock_remove = $29,
|
sponsorblock_remove = $29,
|
||||||
sponsorblock_mark = $30,
|
sponsorblock_mark = $30,
|
||||||
use_aria2 = $31
|
use_aria2 = $31,
|
||||||
|
custom_command = $32
|
||||||
WHERE download_id = $1`,
|
WHERE download_id = $1`,
|
||||||
[
|
[
|
||||||
downloadState.download_id,
|
downloadState.download_id,
|
||||||
@@ -235,7 +236,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
|||||||
downloadState.embed_thumbnail,
|
downloadState.embed_thumbnail,
|
||||||
downloadState.sponsorblock_remove,
|
downloadState.sponsorblock_remove,
|
||||||
downloadState.sponsorblock_mark,
|
downloadState.sponsorblock_mark,
|
||||||
downloadState.use_aria2
|
downloadState.use_aria2,
|
||||||
|
downloadState.custom_command
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -270,8 +272,9 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
|||||||
embed_thumbnail,
|
embed_thumbnail,
|
||||||
sponsorblock_remove,
|
sponsorblock_remove,
|
||||||
sponsorblock_mark,
|
sponsorblock_mark,
|
||||||
use_aria2
|
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)`,
|
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_id,
|
||||||
downloadState.download_status,
|
downloadState.download_status,
|
||||||
@@ -303,7 +306,8 @@ export const saveDownloadState = async (downloadState: DownloadState) => {
|
|||||||
downloadState.embed_thumbnail,
|
downloadState.embed_thumbnail,
|
||||||
downloadState.sponsorblock_remove,
|
downloadState.sponsorblock_remove,
|
||||||
downloadState.sponsorblock_mark,
|
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) => ({
|
export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((set) => ({
|
||||||
activeDownloadModeTab: 'selective',
|
activeDownloadModeTab: 'selective',
|
||||||
|
activeDownloadConfigurationTab: 'options',
|
||||||
isStartingDownload: false,
|
isStartingDownload: false,
|
||||||
selectedDownloadFormat: 'best',
|
selectedDownloadFormat: 'best',
|
||||||
selectedCombinableVideoFormat: '',
|
selectedCombinableVideoFormat: '',
|
||||||
selectedCombinableAudioFormat: '',
|
selectedCombinableAudioFormat: '',
|
||||||
selectedSubtitles: [],
|
selectedSubtitles: [],
|
||||||
selectedPlaylistVideoIndex: '1',
|
selectedPlaylistVideoIndex: '1',
|
||||||
|
downloadConfiguration: {
|
||||||
|
output_format: null,
|
||||||
|
embed_metadata: null,
|
||||||
|
embed_thumbnail: null,
|
||||||
|
custom_command: null
|
||||||
|
},
|
||||||
isErrored: false,
|
isErrored: false,
|
||||||
isErrorExpected: false,
|
isErrorExpected: false,
|
||||||
erroredDownloadId: null,
|
erroredDownloadId: null,
|
||||||
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
setActiveDownloadModeTab: (tab) => set(() => ({ activeDownloadModeTab: tab })),
|
||||||
|
setActiveDownloadConfigurationTab: (tab) => set(() => ({ activeDownloadConfigurationTab: 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 })),
|
||||||
|
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 })),
|
setIsErrored: (isErrored) => set(() => ({ isErrored: isErrored })),
|
||||||
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
setIsErrorExpected: (isErrorExpected) => set(() => ({ isErrorExpected: isErrorExpected })),
|
||||||
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
setErroredDownloadId: (downloadId) => set(() => ({ erroredDownloadId: downloadId })),
|
||||||
@@ -154,6 +177,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
use_aria2: false,
|
use_aria2: false,
|
||||||
use_force_internet_protocol: false,
|
use_force_internet_protocol: false,
|
||||||
force_internet_protocol: 'ipv4',
|
force_internet_protocol: 'ipv4',
|
||||||
|
use_custom_commands: false,
|
||||||
|
custom_commands: [],
|
||||||
// extension settings
|
// extension settings
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
@@ -212,6 +237,8 @@ export const useSettingsPageStatesStore = create<SettingsPageStatesStore>((set)
|
|||||||
use_aria2: false,
|
use_aria2: false,
|
||||||
use_force_internet_protocol: false,
|
use_force_internet_protocol: false,
|
||||||
force_internet_protocol: 'ipv4',
|
force_internet_protocol: 'ipv4',
|
||||||
|
use_custom_commands: false,
|
||||||
|
custom_commands: [],
|
||||||
// extension settings
|
// extension settings
|
||||||
websocket_port: 53511
|
websocket_port: 53511
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface DownloadState {
|
|||||||
sponsorblock_remove: string | null;
|
sponsorblock_remove: string | null;
|
||||||
sponsorblock_mark: string | null;
|
sponsorblock_mark: string | null;
|
||||||
use_aria2: number;
|
use_aria2: number;
|
||||||
|
custom_command: string | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
@@ -79,6 +80,7 @@ export interface Download {
|
|||||||
sponsorblock_remove: string | null;
|
sponsorblock_remove: string | null;
|
||||||
sponsorblock_mark: string | null;
|
sponsorblock_mark: string | null;
|
||||||
use_aria2: number;
|
use_aria2: number;
|
||||||
|
custom_command: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ export interface SettingsTable {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomCommand {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
args: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
ytdlp_update_channel: string;
|
ytdlp_update_channel: string;
|
||||||
ytdlp_auto_update: boolean;
|
ytdlp_auto_update: boolean;
|
||||||
@@ -35,6 +41,15 @@ export interface Settings {
|
|||||||
use_aria2: boolean;
|
use_aria2: boolean;
|
||||||
use_force_internet_protocol: boolean;
|
use_force_internet_protocol: boolean;
|
||||||
force_internet_protocol: string;
|
force_internet_protocol: string;
|
||||||
|
use_custom_commands: boolean;
|
||||||
|
custom_commands: CustomCommand[];
|
||||||
// extension settings
|
// extension settings
|
||||||
websocket_port: number;
|
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 { DownloadState } from "@/types/download";
|
||||||
import { RawVideoInfo } from "@/types/video";
|
import { RawVideoInfo } from "@/types/video";
|
||||||
import { Settings } from "@/types/settings";
|
import { DownloadConfiguration, Settings } from "@/types/settings";
|
||||||
import { KvStore } from "@/types/kvStore";
|
import { KvStore } from "@/types/kvStore";
|
||||||
import { Update } from "@tauri-apps/plugin-updater";
|
import { Update } from "@tauri-apps/plugin-updater";
|
||||||
import { Log } from "@/types/logs";
|
import { Log } from "@/types/logs";
|
||||||
@@ -37,22 +37,28 @@ export interface CurrentVideoMetadataStore {
|
|||||||
|
|
||||||
export interface DownloaderPageStatesStore {
|
export interface DownloaderPageStatesStore {
|
||||||
activeDownloadModeTab: string;
|
activeDownloadModeTab: string;
|
||||||
|
activeDownloadConfigurationTab: string;
|
||||||
isStartingDownload: boolean;
|
isStartingDownload: boolean;
|
||||||
selectedDownloadFormat: string;
|
selectedDownloadFormat: string;
|
||||||
selectedCombinableVideoFormat: string;
|
selectedCombinableVideoFormat: string;
|
||||||
selectedCombinableAudioFormat: string;
|
selectedCombinableAudioFormat: string;
|
||||||
selectedSubtitles: string[];
|
selectedSubtitles: string[];
|
||||||
selectedPlaylistVideoIndex: string;
|
selectedPlaylistVideoIndex: string;
|
||||||
|
downloadConfiguration: DownloadConfiguration;
|
||||||
isErrored: boolean;
|
isErrored: boolean;
|
||||||
isErrorExpected: boolean;
|
isErrorExpected: boolean;
|
||||||
erroredDownloadId: string | null;
|
erroredDownloadId: string | null;
|
||||||
setActiveDownloadModeTab: (tab: string) => void;
|
setActiveDownloadModeTab: (tab: string) => void;
|
||||||
|
setActiveDownloadConfigurationTab: (tab: string) => void;
|
||||||
setIsStartingDownload: (isStarting: boolean) => void;
|
setIsStartingDownload: (isStarting: boolean) => void;
|
||||||
setSelectedDownloadFormat: (format: string) => void;
|
setSelectedDownloadFormat: (format: string) => void;
|
||||||
setSelectedCombinableVideoFormat: (format: string) => void;
|
setSelectedCombinableVideoFormat: (format: string) => void;
|
||||||
setSelectedCombinableAudioFormat: (format: string) => void;
|
setSelectedCombinableAudioFormat: (format: string) => void;
|
||||||
setSelectedSubtitles: (subtitles: string[]) => void;
|
setSelectedSubtitles: (subtitles: string[]) => void;
|
||||||
setSelectedPlaylistVideoIndex: (index: string) => void;
|
setSelectedPlaylistVideoIndex: (index: string) => void;
|
||||||
|
setDownloadConfigurationKey: (key: string, value: unknown) => void;
|
||||||
|
setDownloadConfiguration: (config: DownloadConfiguration) => void;
|
||||||
|
resetDownloadConfiguration: () => void;
|
||||||
setIsErrored: (isErrored: boolean) => void;
|
setIsErrored: (isErrored: boolean) => void;
|
||||||
setIsErrorExpected: (isErrorExpected: boolean) => void;
|
setIsErrorExpected: (isErrorExpected: boolean) => void;
|
||||||
setErroredDownloadId: (downloadId: string | null) => void;
|
setErroredDownloadId: (downloadId: string | null) => void;
|
||||||
|
|||||||
@@ -209,6 +209,10 @@ export const formatCodec = (codec: string) => {
|
|||||||
return codec.toUpperCase();
|
return codec.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateID = () => {
|
||||||
|
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export const generateDownloadId = (videoId: string, host: string) => {
|
export const generateDownloadId = (videoId: string, host: string) => {
|
||||||
host = host.trim().split('.')[0];
|
host = host.trim().split('.')[0];
|
||||||
return `${host}_${videoId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
return `${host}_${videoId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user