import { ThemeProvider } from "@/providers/themeProvider"; import { TooltipProvider } from "@/components/ui/tooltip"; import { AppContext } from "@/providers/appContextProvider"; import { DownloadState } from "@/types/download"; import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useRef, useState } from "react"; import { arch, exeExtension } from "@tauri-apps/plugin-os"; import { downloadDir, join, resourceDir, tempDir } from "@tauri-apps/api/path"; import { useBasePathsStore, useCurrentVideoMetadataStore, useDownloaderPageStatesStore, useDownloadStatesStore, useKvPairsStatesStore, useSettingsPageStatesStore } from "@/services/store"; import { determineFileType, generateVideoId, isObjEmpty, parseProgressLine } from "@/utils"; import { Command } from "@tauri-apps/plugin-shell"; import { RawVideoInfo } from "@/types/video"; import { useDeleteDownloadState, useSaveDownloadState, useSavePlaylistInfo, useSaveVideoInfo, useUpdateDownloadFilePath, useUpdateDownloadStatus } from "@/services/mutations"; import { useQueryClient } from "@tanstack/react-query"; import { useFetchAllDownloadStates, useFetchAllkVPairs, useFetchAllSettings } from "@/services/queries"; import { config } from "@/config"; import * as fs from "@tauri-apps/plugin-fs"; import { useYtDlpUpdater } from "@/helpers/use-ytdlp-updater"; import { getVersion } from "@tauri-apps/api/app"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { listen } from "@tauri-apps/api/event"; import { WebSocketMessage } from "@/types/websocket"; import { useNavigate } from "react-router-dom"; import { platform } from "@tauri-apps/plugin-os"; import { useMacOsRegisterer } from "@/helpers/use-macos-registerer"; import useAppUpdater from "@/helpers/use-app-updater"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { toast } from "sonner"; import { useLogger } from "@/helpers/use-logger"; import { DownloadConfiguration } from "@/types/settings"; import { ulid } from "ulid"; import { sendNotification } from '@tauri-apps/plugin-notification'; export default function App({ children }: { children: React.ReactNode }) { const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates(); const { data: settings, isSuccess: isSuccessFetchingSettings } = useFetchAllSettings(); const { data: kvPairs, isSuccess: isSuccessFetchingKvPairs } = useFetchAllkVPairs(); const [isSettingsStatePropagated, setIsSettingsStatePropagated] = useState(false); const [isKvPairsStatePropagated, setIsKvPairsStatePropagated] = useState(false); const globalDownloadStates = useDownloadStatesStore((state) => state.downloadStates); const setDownloadStates = useDownloadStatesStore((state) => state.setDownloadStates); const setPath = useBasePathsStore((state) => state.setPath); const ffmpegPath = useBasePathsStore((state) => state.ffmpegPath); const tempDownloadDirPath = useBasePathsStore((state) => state.tempDownloadDirPath); const downloadDirPath = useBasePathsStore((state) => state.downloadDirPath); const setSearchPid = useCurrentVideoMetadataStore((state) => state.setSearchPid); const setIsUsingDefaultSettings = useSettingsPageStatesStore((state) => state.setIsUsingDefaultSettings); const setSettingsKey = useSettingsPageStatesStore((state) => state.setSettingsKey); const appVersion = useSettingsPageStatesStore(state => state.appVersion); const ytDlpVersion = useSettingsPageStatesStore(state => state.ytDlpVersion); const setYtDlpVersion = useSettingsPageStatesStore((state) => state.setYtDlpVersion); const setIsFetchingYtDlpVersion = useSettingsPageStatesStore((state) => state.setIsFetchingYtDlpVersion); const setAppVersion = useSettingsPageStatesStore((state) => state.setAppVersion); const setIsFetchingAppVersion = useSettingsPageStatesStore((state) => state.setIsFetchingAppVersion); const YTDLP_AUTO_UPDATE = useSettingsPageStatesStore(state => state.settings.ytdlp_auto_update); const YTDLP_UPDATE_CHANNEL = useSettingsPageStatesStore(state => state.settings.ytdlp_update_channel); const APP_THEME = useSettingsPageStatesStore(state => state.settings.theme); const MAX_PARALLEL_DOWNLOADS = useSettingsPageStatesStore(state => state.settings.max_parallel_downloads); const MAX_RETRIES = useSettingsPageStatesStore(state => state.settings.max_retries); const DOWNLOAD_DIR = useSettingsPageStatesStore(state => state.settings.download_dir); const PREFER_VIDEO_OVER_PLAYLIST = useSettingsPageStatesStore(state => state.settings.prefer_video_over_playlist); const STRICT_DOWNLOADABILITY_CHECK = useSettingsPageStatesStore(state => state.settings.strict_downloadablity_check); const USE_PROXY = useSettingsPageStatesStore(state => state.settings.use_proxy); const PROXY_URL = useSettingsPageStatesStore(state => state.settings.proxy_url); const USE_RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.use_rate_limit); const RATE_LIMIT = useSettingsPageStatesStore(state => state.settings.rate_limit); const VIDEO_FORMAT = useSettingsPageStatesStore(state => state.settings.video_format); const AUDIO_FORMAT = useSettingsPageStatesStore(state => state.settings.audio_format); const ALWAYS_REENCODE_VIDEO = useSettingsPageStatesStore(state => state.settings.always_reencode_video); const EMBED_VIDEO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_video_metadata); const EMBED_AUDIO_METADATA = useSettingsPageStatesStore(state => state.settings.embed_audio_metadata); const EMBED_AUDIO_THUMBNAIL = useSettingsPageStatesStore(state => state.settings.embed_audio_thumbnail); const USE_COOKIES = useSettingsPageStatesStore(state => state.settings.use_cookies); const IMPORT_COOKIES_FROM = useSettingsPageStatesStore(state => state.settings.import_cookies_from); const COOKIES_BROWSER = useSettingsPageStatesStore(state => state.settings.cookies_browser); const COOKIES_FILE = useSettingsPageStatesStore(state => state.settings.cookies_file); const USE_SPONSORBLOCK = useSettingsPageStatesStore(state => state.settings.use_sponsorblock); const SPONSORBLOCK_MODE = useSettingsPageStatesStore(state => state.settings.sponsorblock_mode); const SPONSORBLOCK_REMOVE = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove); const SPONSORBLOCK_MARK = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark); const SPONSORBLOCK_REMOVE_CATEGORIES = useSettingsPageStatesStore(state => state.settings.sponsorblock_remove_categories); const SPONSORBLOCK_MARK_CATEGORIES = useSettingsPageStatesStore(state => state.settings.sponsorblock_mark_categories); const USE_ARIA2 = useSettingsPageStatesStore(state => state.settings.use_aria2); const USE_FORCE_INTERNET_PROTOCOL = useSettingsPageStatesStore(state => state.settings.use_force_internet_protocol); const FORCE_INTERNET_PROTOCOL = useSettingsPageStatesStore(state => state.settings.force_internet_protocol); const USE_CUSTOM_COMMANDS = useSettingsPageStatesStore(state => state.settings.use_custom_commands); const CUSTOM_COMMANDS = useSettingsPageStatesStore(state => state.settings.custom_commands); const FILENAME_TEMPLATE = useSettingsPageStatesStore(state => state.settings.filename_template); const DEBUG_MODE = useSettingsPageStatesStore(state => state.settings.debug_mode); const LOG_VERBOSE = useSettingsPageStatesStore(state => state.settings.log_verbose); const LOG_WARNING = useSettingsPageStatesStore(state => state.settings.log_warning); const LOG_PROGRESS = useSettingsPageStatesStore(state => state.settings.log_progress); const ENABLE_NOTIFICATIONS = useSettingsPageStatesStore(state => state.settings.enable_notifications); const DOWNLOAD_COMPLETION_NOTIFICATION = useSettingsPageStatesStore(state => state.settings.download_completion_notification); const isErrored = useDownloaderPageStatesStore((state) => state.isErrored); const isErrorExpected = useDownloaderPageStatesStore((state) => state.isErrorExpected); const erroredDownloadId = useDownloaderPageStatesStore((state) => state.erroredDownloadId); const setIsErrored = useDownloaderPageStatesStore((state) => state.setIsErrored); const setIsErrorExpected = useDownloaderPageStatesStore((state) => state.setIsErrorExpected); const setErroredDownloadId = useDownloaderPageStatesStore((state) => state.setErroredDownloadId); const appWindow = getCurrentWebviewWindow() const navigate = useNavigate(); const LOG = useLogger(); const currentPlatform = platform(); const { updateYtDlp } = useYtDlpUpdater(); const { registerToMac } = useMacOsRegisterer(); const { checkForAppUpdate } = useAppUpdater(); const setKvPairsKey = useKvPairsStatesStore((state) => state.setKvPairsKey); const ytDlpUpdateLastCheck = useKvPairsStatesStore(state => state.kvPairs.ytdlp_update_last_check); const macOsRegisteredVersion = useKvPairsStatesStore(state => state.kvPairs.macos_registered_version); const queryClient = useQueryClient(); const downloadStateSaver = useSaveDownloadState(); const downloadStatusUpdater = useUpdateDownloadStatus(); const downloadFilePathUpdater = useUpdateDownloadFilePath(); const videoInfoSaver = useSaveVideoInfo(); const downloadStateDeleter = useDeleteDownloadState(); const playlistInfoSaver = useSavePlaylistInfo(); const ongoingDownloads = globalDownloadStates.filter(state => state.download_status === 'downloading' || state.download_status === 'starting'); const queuedDownloads = globalDownloadStates.filter(state => state.download_status === 'queued').sort((a, b) => a.queue_index! - b.queue_index!); const isProcessingQueueRef = useRef(false); const lastProcessedDownloadIdRef = useRef(null); const hasRunYtDlpAutoUpdateRef = useRef(false); const hasRunAppUpdateCheckRef = useRef(false); const isRegisteredToMacOsRef = useRef(false); const fetchVideoMetadata = async (url: string, formatId?: string, playlistIndex?: string, selectedSubtitles?: string | null, resumeState?: DownloadState, downloadConfig?: DownloadConfiguration): Promise => { try { const args = [url, '--dump-single-json', '--no-warnings']; if (formatId) args.push('--format', formatId); if (selectedSubtitles) args.push('--embed-subs', '--sub-lang', selectedSubtitles); if (playlistIndex) args.push('--playlist-items', playlistIndex); if (PREFER_VIDEO_OVER_PLAYLIST && !playlistIndex) args.push('--no-playlist'); if (STRICT_DOWNLOADABILITY_CHECK && !formatId) args.push('--check-all-formats'); if (STRICT_DOWNLOADABILITY_CHECK && formatId) args.push('--check-formats'); if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfig?.custom_command) || resumeState?.custom_command) { let customCommandArgs = null; if (resumeState?.custom_command) { customCommandArgs = resumeState.custom_command; } else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig?.custom_command)) { let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig?.custom_command); customCommandArgs = customCommand ? customCommand.args : ''; } if (customCommandArgs && customCommandArgs.trim() !== '') args.push(...customCommandArgs.split(' ')); } if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) args.push('--proxy', PROXY_URL); if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) { if (FORCE_INTERNET_PROTOCOL === 'ipv4') { args.push('--force-ipv4'); } else if (FORCE_INTERNET_PROTOCOL === 'ipv6') { args.push('--force-ipv6'); } } if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_COOKIES) { if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) { args.push('--cookies-from-browser', COOKIES_BROWSER); } else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) { args.push('--cookies', COOKIES_FILE); } } if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (USE_SPONSORBLOCK || (resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark))) { if (SPONSORBLOCK_MODE === 'remove' || resumeState?.sponsorblock_remove) { let sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? ( SPONSORBLOCK_REMOVE_CATEGORIES.length > 0 ? SPONSORBLOCK_REMOVE_CATEGORIES.join(',') : 'default' ) : (SPONSORBLOCK_REMOVE)); args.push('--sponsorblock-remove', sponsorblockRemove); } else if (SPONSORBLOCK_MODE === 'mark' || resumeState?.sponsorblock_mark) { let sponsorblockMark = resumeState?.sponsorblock_mark || (SPONSORBLOCK_MARK === 'custom' ? ( SPONSORBLOCK_MARK_CATEGORIES.length > 0 ? SPONSORBLOCK_MARK_CATEGORIES.join(',') : 'default' ) : (SPONSORBLOCK_MARK)); args.push('--sponsorblock-mark', sponsorblockMark); } }; const command = Command.sidecar('binaries/yt-dlp', args); let jsonOutput = ''; return new Promise((resolve) => { command.stdout.on('data', line => { jsonOutput += line; }); command.on('close', async (data) => { if (data.code !== 0) { console.error(`yt-dlp failed to fetch metadata with code ${data.code}`); LOG.error('NEODLP', `yt-dlp exited with code ${data.code} while fetching metadata for URL: ${url} (ignore if you manually cancelled)`); resolve(null); } else { try { const matchedJson = jsonOutput.match(/{.*}/); if (!matchedJson) { console.error(`Failed to match JSON: ${jsonOutput}`); LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url})`); resolve(null); return; } const parsedJson: RawVideoInfo = JSON.parse(matchedJson[0]); resolve(parsedJson); } catch (e) { console.error(`Failed to parse JSON: ${e}`); LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url}) with error: ${e}`); resolve(null); } } }); command.on('error', error => { console.error(`Error fetching metadata: ${error}`); LOG.error('NEODLP', `Error occurred while fetching metadata for URL: ${url} : ${error}`); resolve(null); }); LOG.info('NEODLP', `Fetching metadata for URL: ${url}, with args: ${args.join(' ')}`); command.spawn().then(child => { setSearchPid(child.pid); }).catch(e => { console.error(`Failed to spawn command: ${e}`); LOG.error('NEODLP', `Failed to spawn yt-dlp process for fetching metadata for URL: ${url} : ${e}`); resolve(null); }); }); } catch (e) { console.error(`Failed to fetch metadata: ${e}`); LOG.error('NEODLP', `Failed to fetch metadata for URL: ${url} : ${e}`); return null; } }; const startDownload = async (url: string, selectedFormat: string, downloadConfig: DownloadConfiguration, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => { LOG.info('NEODLP', `Initiating yt-dlp download for URL: ${url}`); // set error states to default setIsErrored(false); setIsErrorExpected(false); setErroredDownloadId(null); console.log('Starting download:', { url, selectedFormat, downloadConfig, selectedSubtitles, resumeState, playlistItems }); if (!ffmpegPath || !tempDownloadDirPath || !downloadDirPath) { console.error('FFmpeg or download paths not found'); return; } const isPlaylist = (playlistItems && typeof playlistItems === 'string') || (resumeState?.playlist_id && resumeState?.playlist_index) ? true : false; const playlistIndex = isPlaylist ? (resumeState?.playlist_index?.toString() || playlistItems) : null; let videoMetadata = await fetchVideoMetadata(url, selectedFormat, isPlaylist && playlistIndex && typeof playlistIndex === 'string' ? playlistIndex : undefined, selectedSubtitles, resumeState); if (!videoMetadata) { console.error('Failed to fetch video metadata'); toast.error("Download Failed", { description: "yt-dlp failed to fetch video metadata. Please try again later.", }); return; } console.log('Video Metadata:', videoMetadata); videoMetadata = isPlaylist ? videoMetadata.entries[0] : videoMetadata; const fileType = determineFileType(videoMetadata.vcodec, videoMetadata.acodec); if (fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) { if (VIDEO_FORMAT !== 'auto' && (fileType === 'video+audio' || fileType === 'video')) videoMetadata.ext = VIDEO_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; const videoId = resumeState?.video_id || generateVideoId(videoMetadata.id, videoMetadata.webpage_url_domain); const playlistId = isPlaylist ? (resumeState?.playlist_id || generateVideoId(videoMetadata.playlist_id, videoMetadata.webpage_url_domain)) : null; const downloadId = resumeState?.download_id || ulid() /*generateDownloadId(videoMetadata.id, videoMetadata.webpage_url_domain)*/; // const tempDownloadPathForYtdlp = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.%(ext)s`); // const tempDownloadPath = await join(tempDownloadDirPath, `${downloadId}_${selectedFormat}.${videoMetadata.ext}`); // let downloadFilePath = resumeState?.filepath || await join(downloadDirPath, sanitizeFilename(`${videoMetadata.title}_${videoMetadata.resolution || 'unknown'}[${videoMetadata.id}].${videoMetadata.ext}`)); let downloadFilePath: string | null = null; let processPid: number | null = null; const args = [ url, '--newline', '--progress-template', 'status:%(progress.status)s,progress:%(progress._percent_str)s,speed:%(progress.speed)f,downloaded:%(progress.downloaded_bytes)d,total:%(progress.total_bytes)d,eta:%(progress.eta)d', '--paths', `temp:${tempDownloadDirPath}`, '--paths', `home:${downloadDirPath}`, '--output', `${FILENAME_TEMPLATE}[${downloadId}].%(ext)s`, '--windows-filenames', '--restrict-filenames', '--exec', 'after_move:echo Finalpath: {}', '--format', selectedFormat, '--no-mtime', '--retries', MAX_RETRIES.toString(), ]; if (currentPlatform === 'macos') { args.push('--ffmpeg-location', '/Applications/NeoDLP.app/Contents/MacOS', '--js-runtimes deno:/Applications/NeoDLP.app/Contents/MacOS/deno'); } if (!DEBUG_MODE || (DEBUG_MODE && !LOG_WARNING)) { args.push('--no-warnings'); } if (DEBUG_MODE && LOG_VERBOSE) { args.push('--verbose'); } if (selectedSubtitles) { args.push('--embed-subs', '--sub-lang', selectedSubtitles); } if (isPlaylist && playlistIndex && typeof playlistIndex === 'string') { args.push('--playlist-items', playlistIndex); } let customCommandArgs = null; if ((USE_CUSTOM_COMMANDS && CUSTOM_COMMANDS && downloadConfig.custom_command) || resumeState?.custom_command) { if (resumeState?.custom_command) { customCommandArgs = resumeState.custom_command; } else if (CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig.custom_command)) { let customCommand = CUSTOM_COMMANDS.find(cmd => cmd.id === downloadConfig.custom_command); customCommandArgs = customCommand ? customCommand.args : ''; } if (customCommandArgs && customCommandArgs.trim() !== '') args.push(...customCommandArgs.split(' ')); } let outputFormat = null; if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && ((fileType !== 'unknown' && (VIDEO_FORMAT !== 'auto' || AUDIO_FORMAT !== 'auto')) || configOutputFormat || resumeState?.output_format)) { const format = resumeState?.output_format || configOutputFormat; if (format) { outputFormat = format; } else if (fileType === 'audio' && AUDIO_FORMAT !== 'auto') { outputFormat = AUDIO_FORMAT; } else if ((fileType === 'video' || fileType === 'video+audio') && VIDEO_FORMAT !== 'auto') { outputFormat = VIDEO_FORMAT; } const recodeOrRemux = ALWAYS_REENCODE_VIDEO ? '--recode-video' : '--remux-video'; const formatToUse = format || VIDEO_FORMAT; // Handle video+audio if (fileType === 'video+audio' && (VIDEO_FORMAT !== 'auto' || format)) { args.push(ALWAYS_REENCODE_VIDEO ? '--recode-video' : '--merge-output-format', formatToUse); } // Handle video only else if (fileType === 'video' && (VIDEO_FORMAT !== 'auto' || format)) { args.push(recodeOrRemux, formatToUse); } // Handle audio only else if (fileType === 'audio' && (AUDIO_FORMAT !== 'auto' || format)) { args.push('--extract-audio', '--audio-format', format || AUDIO_FORMAT); } // Handle unknown filetype else if (fileType === 'unknown' && format) { if (['mkv', 'mp4', 'webm'].includes(format)) { args.push(recodeOrRemux, formatToUse); } else if (['mp3', 'm4a', 'opus'].includes(format)) { args.push('--extract-audio', '--audio-format', format); } } } let embedMetadata = 0; if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_metadata || resumeState?.embed_metadata || EMBED_VIDEO_METADATA || EMBED_AUDIO_METADATA)) { const shouldEmbedForVideo = (fileType === 'video+audio' || fileType === 'video') && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_VIDEO_METADATA && downloadConfig.embed_metadata === null)); const shouldEmbedForAudio = fileType === 'audio' && (downloadConfig.embed_metadata || resumeState?.embed_metadata || (EMBED_AUDIO_METADATA && downloadConfig.embed_metadata === null)); const shouldEmbedForUnknown = fileType === 'unknown' && (downloadConfig.embed_metadata || resumeState?.embed_metadata); if (shouldEmbedForUnknown || shouldEmbedForVideo || shouldEmbedForAudio) { embedMetadata = 1; args.push('--embed-metadata'); } } let embedThumbnail = 0; if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (downloadConfig.embed_thumbnail || resumeState?.embed_thumbnail || (fileType === 'audio' && EMBED_AUDIO_THUMBNAIL && downloadConfig.embed_thumbnail === null))) { embedThumbnail = 1; args.push('--embed-thumbnail'); } if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_PROXY && PROXY_URL) { args.push('--proxy', PROXY_URL); } if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_RATE_LIMIT && RATE_LIMIT) { args.push('--limit-rate', `${RATE_LIMIT}`); } if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_FORCE_INTERNET_PROTOCOL && FORCE_INTERNET_PROTOCOL) { if (FORCE_INTERNET_PROTOCOL === 'ipv4') { args.push('--force-ipv4'); } else if (FORCE_INTERNET_PROTOCOL === 'ipv6') { args.push('--force-ipv6'); } } if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && USE_COOKIES) { if (IMPORT_COOKIES_FROM === 'browser' && COOKIES_BROWSER) { args.push('--cookies-from-browser', COOKIES_BROWSER); } else if (IMPORT_COOKIES_FROM === 'file' && COOKIES_FILE) { args.push('--cookies', COOKIES_FILE); } } let sponsorblockRemove = null; let sponsorblockMark = null; if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && ((downloadConfig.sponsorblock && downloadConfig.sponsorblock !== 'auto') || resumeState?.sponsorblock_remove || resumeState?.sponsorblock_mark || USE_SPONSORBLOCK)) { if (downloadConfig?.sponsorblock === 'remove' || resumeState?.sponsorblock_remove || (SPONSORBLOCK_MODE === 'remove' && !downloadConfig.sponsorblock)) { sponsorblockRemove = resumeState?.sponsorblock_remove || (SPONSORBLOCK_REMOVE === 'custom' ? ( SPONSORBLOCK_REMOVE_CATEGORIES.length > 0 ? SPONSORBLOCK_REMOVE_CATEGORIES.join(',') : 'default' ) : (SPONSORBLOCK_REMOVE)); args.push('--sponsorblock-remove', sponsorblockRemove); } else if (downloadConfig?.sponsorblock === 'mark' || resumeState?.sponsorblock_mark || (SPONSORBLOCK_MODE === 'mark' && !downloadConfig.sponsorblock)) { sponsorblockMark = resumeState?.sponsorblock_mark || (SPONSORBLOCK_MARK === 'custom' ? ( SPONSORBLOCK_MARK_CATEGORIES.length > 0 ? SPONSORBLOCK_MARK_CATEGORIES.join(',') : 'default' ) : (SPONSORBLOCK_MARK)); args.push('--sponsorblock-mark', sponsorblockMark); } } let useAria2 = 0; if ((!USE_CUSTOM_COMMANDS && !resumeState?.custom_command) && (USE_ARIA2 || resumeState?.use_aria2)) { useAria2 = 1; args.push( '--downloader', 'aria2c', '--downloader', 'dash,m3u8:native', '--downloader-args', 'aria2c:-c -j 16 -x 16 -s 16 -k 1M --check-certificate=false' ); 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_CUSTOM_COMMANDS && USE_ARIA2)) { args.push('--continue'); } else { args.push('--no-continue'); } console.log('Starting download with args:', args); const command = Command.sidecar('binaries/yt-dlp', args); command.on('close', async (data) => { if (data.code !== 0) { console.error(`Download failed with code ${data.code}`); LOG.error(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code} (ignore if you manually paused or cancelled the download)`); if (!isErrorExpected) { setIsErrored(true); setErroredDownloadId(downloadId); } } else { LOG.info(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code}`); } }); command.on('error', error => { console.error(`Error: ${error}`); LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`); setIsErrored(true); setErroredDownloadId(downloadId); }); command.stdout.on('data', line => { if (line.startsWith('status:') || line.startsWith('[#')) { // console.log(line); if (DEBUG_MODE && LOG_PROGRESS) LOG.progress(`YT-DLP Download ${downloadId}`, line); const currentProgress = parseProgressLine(line); const state: DownloadState = { download_id: downloadId, download_status: 'downloading', video_id: videoId, format_id: selectedFormat, subtitle_id: selectedSubtitles || null, queue_index: null, playlist_id: playlistId, playlist_index: playlistIndex ? Number(playlistIndex) : null, title: videoMetadata.title, url: url, host: videoMetadata.webpage_url_domain, thumbnail: videoMetadata.thumbnail || null, channel: videoMetadata.channel || null, duration_string: videoMetadata.duration_string || null, release_date: videoMetadata.release_date || null, view_count: videoMetadata.view_count || null, like_count: videoMetadata.like_count || null, playlist_title: videoMetadata.playlist_title, playlist_url: videoMetadata.playlist_webpage_url, playlist_n_entries: videoMetadata.playlist_count || videoMetadata.n_entries, playlist_channel: videoMetadata.playlist_channel || null, resolution: videoMetadata.resolution || null, ext: videoMetadata.ext || null, abr: videoMetadata.abr || null, vbr: videoMetadata.vbr || null, acodec: videoMetadata.acodec || null, vcodec: videoMetadata.vcodec || null, dynamic_range: videoMetadata.dynamic_range || null, process_id: processPid, status: currentProgress.status || null, progress: currentProgress.progress || null, total: currentProgress.total || null, downloaded: currentProgress.downloaded || null, speed: currentProgress.speed || null, eta: currentProgress.eta || null, filepath: downloadFilePath, filetype: determineFileType(videoMetadata.vcodec, videoMetadata.acodec) || null, filesize: videoMetadata.filesize_approx || null, output_format: outputFormat, embed_metadata: embedMetadata, embed_thumbnail: embedThumbnail, sponsorblock_remove: sponsorblockRemove, sponsorblock_mark: sponsorblockMark, use_aria2: useAria2, custom_command: customCommandArgs, queue_config: null }; downloadStateSaver.mutate(state, { onSuccess: (data) => { console.log("Download State saved successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); }, onError: (error) => { console.error("Failed to save download state:", error); } }) } else { // console.log(line); if (line.trim() !== '') LOG.info(`YT-DLP Download ${downloadId}`, line); if (line.startsWith('Finalpath: ')) { downloadFilePath = line.replace('Finalpath: ', '').trim().replace(/^"|"$/g, ''); const downloadedFileExt = downloadFilePath.split('.').pop(); // Update completion status after a short delay to ensure database states are propagated correctly console.log(`Download completed with ID: ${downloadId}, updating filepath and status after 1.5s delay...`); setTimeout(async () => { LOG.info('NEODLP', `yt-dlp download completed with id: ${downloadId}`); downloadFilePathUpdater.mutate({ download_id: downloadId, filepath: downloadFilePath as string, ext: downloadedFileExt as string }, { onSuccess: (data) => { console.log("Download filepath updated successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); }, onError: (error) => { console.error("Failed to update download filepath:", error); } }); downloadStatusUpdater.mutate({ download_id: downloadId, download_status: 'completed' }, { onSuccess: (data) => { console.log("Download status updated successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); }, onError: (error) => { console.error("Failed to update download status:", error); } }); toast.success("Download Completed", { description: `The download for "${videoMetadata.title}" has completed successfully.`, }); if (ENABLE_NOTIFICATIONS && DOWNLOAD_COMPLETION_NOTIFICATION) { sendNotification({ title: "Download Completed", body: `The download for "${videoMetadata.title}" has completed successfully.`, }); } }, 1500); } } }); try { videoInfoSaver.mutate({ video_id: videoId, title: videoMetadata.title, url: url, host: videoMetadata.webpage_url_domain, thumbnail: videoMetadata.thumbnail || null, channel: videoMetadata.channel || videoMetadata.uploader || null, duration_string: videoMetadata.duration_string || null, release_date: videoMetadata.release_date || null, view_count: videoMetadata.view_count || null, like_count: videoMetadata.like_count || null }, { onSuccess: (data) => { console.log("Video Info saved successfully:", data); if (isPlaylist) { playlistInfoSaver.mutate({ playlist_id: playlistId ? playlistId : '', playlist_title: videoMetadata.playlist_title, playlist_url: videoMetadata.playlist_webpage_url, playlist_n_entries: videoMetadata.playlist_count || videoMetadata.n_entries, playlist_channel: videoMetadata.playlist_channel || null }, { onSuccess: (data) => { console.log("Playlist Info saved successfully:", data); }, onError: (error) => { console.error("Failed to save playlist info:", error); } }) } const state: DownloadState = { download_id: downloadId, download_status: (!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) ? 'starting' : 'queued', video_id: videoId, format_id: selectedFormat, subtitle_id: selectedSubtitles || null, queue_index: (!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) ? null : (queuedDownloads?.length || 0), playlist_id: playlistId, playlist_index: playlistIndex ? Number(playlistIndex) : null, title: videoMetadata.title, url: url, host: videoMetadata.webpage_url_domain, thumbnail: videoMetadata.thumbnail || null, channel: videoMetadata.channel || null, duration_string: videoMetadata.duration_string || null, release_date: videoMetadata.release_date || null, view_count: videoMetadata.view_count || null, like_count: videoMetadata.like_count || null, playlist_title: videoMetadata.playlist_title, playlist_url: videoMetadata.playlist_webpage_url, playlist_n_entries: videoMetadata.playlist_count || videoMetadata.n_entries, playlist_channel: videoMetadata.playlist_channel || null, resolution: resumeState?.resolution || null, ext: resumeState?.ext || null, abr: resumeState?.abr || null, vbr: resumeState?.vbr || null, acodec: resumeState?.acodec || null, vcodec: resumeState?.vcodec || null, dynamic_range: resumeState?.dynamic_range || null, process_id: resumeState?.process_id || null, status: resumeState?.status || null, progress: resumeState?.progress || null, total: resumeState?.total || null, downloaded: resumeState?.downloaded || null, speed: resumeState?.speed || null, eta: resumeState?.eta || null, filepath: downloadFilePath, filetype: resumeState?.filetype || null, filesize: resumeState?.filesize || null, output_format: resumeState?.output_format || null, embed_metadata: resumeState?.embed_metadata || 0, embed_thumbnail: resumeState?.embed_thumbnail || 0, sponsorblock_remove: resumeState?.sponsorblock_remove || null, sponsorblock_mark: resumeState?.sponsorblock_mark || null, use_aria2: resumeState?.use_aria2 || 0, custom_command: resumeState?.custom_command || null, queue_config: resumeState?.queue_config || ((!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) ? null : JSON.stringify(downloadConfig)) } downloadStateSaver.mutate(state, { onSuccess: (data) => { console.log("Download State saved successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); }, onError: (error) => { console.error("Failed to save download state:", error); } }) }, onError: (error) => { console.error("Failed to save video info:", error); } }) if (!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) { LOG.info('NEODLP', `Starting yt-dlp download with args: ${args.join(' ')}`); if(!DEBUG_MODE || (DEBUG_MODE && !LOG_PROGRESS)) LOG.warning('NEODLP', `Progress logs are hidden. Enable 'Debug Mode > Log Progress' in Settings to unhide.`); const child = await command.spawn(); processPid = child.pid; return Promise.resolve(); } else { console.log("Download is queued, not starting immediately."); LOG.info('NEODLP', `Download queued with id: ${downloadId}`); return Promise.resolve(); } } catch (e) { console.error(`Failed to start download: ${e}`); LOG.error('NEODLP', `Failed to start download for URL: ${url} with error: ${e}`); throw e; } }; const pauseDownload = async (downloadState: DownloadState) => { try { LOG.info('NEODLP', `Pausing yt-dlp download with id: ${downloadState.download_id} (as per user request)`); if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) { setIsErrorExpected(true); // Set error expected to true to handle UI state console.log("Killing process with PID:", downloadState.process_id); await invoke('kill_all_process', { pid: downloadState.process_id }); } downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, { onSuccess: (data) => { console.log("Download status updated successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); /* re-check if the download is properly paused (if not try again after a small delay) as the pause opertion happens within high throughput of operations and have a high chgance of failure. */ if (isSuccessFetchingDownloadStates && downloadStates.find(state => state.download_id === downloadState.download_id)?.download_status !== 'paused') { console.log("Download status not updated to paused yet, retrying..."); setTimeout(() => { downloadStatusUpdater.mutate({ download_id: downloadState.download_id, download_status: 'paused' }, { onSuccess: (data) => { console.log("Download status updated successfully on retry:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); }, onError: (error) => { console.error("Failed to update download status:", error); } }); }, 200); } // Reset the processing flag to ensure queue can be processed isProcessingQueueRef.current = false; // Process the queue after a short delay to ensure state is updated setTimeout(() => { processQueuedDownloads(); }, 1000); }, onError: (error) => { console.error("Failed to update download status:", error); } }) return Promise.resolve(); } catch (e) { console.error(`Failed to pause download: ${e}`); LOG.error('NEODLP', `Failed to pause download with id: ${downloadState.download_id} with error: ${e}`); isProcessingQueueRef.current = false; throw e; } }; const resumeDownload = async (downloadState: DownloadState) => { try { LOG.info('NEODLP', `Resuming yt-dlp download with id: ${downloadState.download_id} (as per user request)`); await startDownload( downloadState.playlist_id && downloadState.playlist_index ? downloadState.playlist_url : downloadState.url, downloadState.format_id, downloadState.queue_config ? JSON.parse(downloadState.queue_config) : { output_format: null, embed_metadata: null, embed_thumbnail: null, sponsorblock: null, custom_command: null }, downloadState.subtitle_id, downloadState ); return Promise.resolve(); } catch (e) { console.error(`Failed to resume download: ${e}`); LOG.error('NEODLP', `Failed to resume download with id: ${downloadState.download_id} with error: ${e}`); throw e; } }; const cancelDownload = async (downloadState: DownloadState) => { try { LOG.info('NEODLP', `Cancelling yt-dlp download with id: ${downloadState.download_id} (as per user request)`); if ((downloadState.download_status === 'downloading' && downloadState.process_id) || (downloadState.download_status === 'starting' && downloadState.process_id)) { setIsErrorExpected(true); // Set error expected to true to handle UI state console.log("Killing process with PID:", downloadState.process_id); await invoke('kill_all_process', { pid: downloadState.process_id }); } downloadStateDeleter.mutate(downloadState.download_id, { onSuccess: (data) => { console.log("Download State deleted successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); // Reset processing flag and trigger queue processing isProcessingQueueRef.current = false; // Process the queue after a short delay setTimeout(() => { processQueuedDownloads(); }, 1000); }, onError: (error) => { console.error("Failed to delete download state:", error); isProcessingQueueRef.current = false; } }) return Promise.resolve(); } catch (e) { console.error(`Failed to cancel download: ${e}`); LOG.error('NEODLP', `Failed to cancel download with id: ${downloadState.download_id} with error: ${e}`); throw e; } } const processQueuedDownloads = useCallback(async () => { // Prevent concurrent processing if (isProcessingQueueRef.current) { console.log("Queue processing already in progress, skipping..."); return; } // Check if we can process more downloads if (!queuedDownloads?.length || ongoingDownloads?.length >= MAX_PARALLEL_DOWNLOADS) { return; } try { isProcessingQueueRef.current = true; console.log("Processing download queue..."); // Get the first download in queue const downloadToStart = queuedDownloads[0]; // Skip if we just processed this download to prevent loops if (lastProcessedDownloadIdRef.current === downloadToStart.download_id) { console.log("Skipping recently processed download:", downloadToStart.download_id); return; } // Double-check current state from global state const currentState = globalDownloadStates.find( state => state.download_id === downloadToStart.download_id ); if (!currentState || currentState.download_status !== 'queued') { console.log("Download no longer in queued state:", downloadToStart.download_id); return; } console.log("Starting queued download:", downloadToStart.download_id); LOG.info('NEODLP', `Starting queued download with id: ${downloadToStart.download_id}`); lastProcessedDownloadIdRef.current = downloadToStart.download_id; // Update status to 'starting' first await downloadStatusUpdater.mutateAsync({ download_id: downloadToStart.download_id, download_status: 'starting' }); // Fetch latest state after status update await queryClient.invalidateQueries({ queryKey: ['download-states'] }); // Start the download await startDownload( downloadToStart.url, downloadToStart.format_id, downloadToStart.queue_config ? JSON.parse(downloadToStart.queue_config) : { output_format: null, embed_metadata: null, embed_thumbnail: null, sponsorblock: null, custom_command: null }, downloadToStart.subtitle_id, downloadToStart ); } catch (error) { console.error("Error processing download queue:", error); LOG.error('NEODLP', `Error processing download queue: ${error}`); } finally { // Important: reset the processing flag setTimeout(() => { isProcessingQueueRef.current = false; console.log("Queue processor released lock"); }, 1000); // Small delay to prevent rapid re-processing } }, [queuedDownloads, ongoingDownloads, globalDownloadStates, queryClient]); // Prevent right click context menu in production if (!import.meta.env.DEV) { document.oncontextmenu = (event) => { event.preventDefault() } } // Prevent app from closing useEffect(() => { const handleCloseRequested = (event: any) => { event.preventDefault(); appWindow.hide(); }; appWindow.onCloseRequested(handleCloseRequested); }, []); // Listen for websocket messages useEffect(() => { const unlisten = listen('websocket-message', (event) => { if(event.payload.command === 'download') { const handleDownload = async () => { appWindow.show(); appWindow.setFocus(); navigate('/'); if (event.payload.url) { LOG.info('NEODLP', `Received download request from neodlp browser extension for URL: ${event.payload.url}`); const { setRequestedUrl, setAutoSubmitSearch } = useCurrentVideoMetadataStore.getState(); setRequestedUrl(event.payload.url); setAutoSubmitSearch(true); } } handleDownload().catch((error) => { console.error("Error handling download:", error); }); } }); return () => { unlisten.then(f => f()); }; }, []); // useEffect(() => { // const fetchConfigPath = async () => { // const configPath = await invoke('get_config_file_path'); // console.log("Config path fetched successfully:", configPath); // }; // fetchConfigPath().catch((error) => { // console.error("Error fetching config path:", error); // }); // }, []); // Fetch download states from database and sync with state useEffect(() => { if (isSuccessFetchingSettings && settings) { console.log("Settings fetched successfully:", settings); if (!isObjEmpty(settings)) { setIsUsingDefaultSettings(false); Object.keys(settings).forEach((key) => { setSettingsKey(key, settings[key]); }); } else { setIsUsingDefaultSettings(true); } setIsSettingsStatePropagated(true); } }, [settings, isSuccessFetchingSettings]); // Fetch KV pairs from database and sync with state useEffect(() => { if (isSuccessFetchingKvPairs && kvPairs) { console.log("KvPairs fetched successfully:", kvPairs); if (!isObjEmpty(kvPairs)) { Object.keys(kvPairs).forEach((key) => { setKvPairsKey(key, kvPairs[key]); }); } setIsKvPairsStatePropagated(true); } }, [kvPairs, isSuccessFetchingKvPairs]); // Initiate/Resolve base app paths useEffect(() => { const initPaths = async () => { try { const currentArch = arch(); const currentExeExtension = exeExtension(); const downloadDirPath = await downloadDir(); const tempDirPath = await tempDir(); const resourceDirPath = await resourceDir(); const ffmpegPath = await join(resourceDirPath, 'binaries', `ffmpeg-${currentArch}${currentExeExtension ? '.' + currentExeExtension : ''}`); const tempDownloadDirPath = await join(tempDirPath, config.appPkgName, 'downloads'); const appDownloadDirPath = await join(downloadDirPath, config.appName); if(!await fs.exists(tempDownloadDirPath)) fs.mkdir(tempDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${tempDownloadDirPath}`) }); setPath('ffmpegPath', ffmpegPath); setPath('tempDownloadDirPath', tempDownloadDirPath); if (DOWNLOAD_DIR) { setPath('downloadDirPath', DOWNLOAD_DIR); } else { if(!await fs.exists(appDownloadDirPath)) fs.mkdir(appDownloadDirPath, { recursive: true }).then(() => { console.log(`Created DIR: ${appDownloadDirPath}`) }); setPath('downloadDirPath', appDownloadDirPath); } console.log('Paths initialized:', { ffmpegPath, tempDownloadDirPath, downloadDirPath: DOWNLOAD_DIR || appDownloadDirPath }); } catch (e) { console.error('Failed to fetch paths:', e); } }; initPaths(); }, [DOWNLOAD_DIR, setPath]); // Fetch app version useEffect(() => { const fetchAppVersion = async () => { setIsFetchingAppVersion(true); try { const version = await getVersion(); console.log("App version fetched successfully:", version); setAppVersion(version); } catch (e) { console.error('Failed to fetch app version:', e); } finally { setIsFetchingAppVersion(false); } } fetchAppVersion(); }, []); // Fetch yt-dlp version useEffect(() => { const fetchYtDlpVersion = async () => { setIsFetchingYtDlpVersion(true); try { const command = Command.sidecar('binaries/yt-dlp', ['--version']); const output = await command.execute(); if (output.code === 0) { const version = output.stdout.trim(); console.log("yt-dlp version fetched successfully:", version); setYtDlpVersion(version); } else { console.error("Failed to fetch yt-dlp version:", output.stderr); } } catch (e) { console.error('Failed to fetch yt-dlp version:', e); } finally { setIsFetchingYtDlpVersion(false); } }; fetchYtDlpVersion(); }, [ytDlpVersion, setYtDlpVersion]); // Check for app update useEffect(() => { // Only run once when both settings and KV pairs are loaded if (!isSettingsStatePropagated || !isKvPairsStatePropagated) { console.log("Skipping app update check, waiting for configs to load..."); return; } // Skip if we've already run the update check once if (hasRunAppUpdateCheckRef.current) { console.log("App update check already performed in this session, skipping"); return; } hasRunAppUpdateCheckRef.current = true; checkForAppUpdate().catch((error) => { console.error("Error checking for app update:", error); }); }, [isSettingsStatePropagated, isKvPairsStatePropagated]); // Check for yt-dlp auto-update useEffect(() => { // Only run once when both settings and KV pairs are loaded if (!isSettingsStatePropagated || !isKvPairsStatePropagated) { console.log("Skipping yt-dlp auto-update check, waiting for configs to load..."); return; } // Skip if we've already run the auto-update once if (hasRunYtDlpAutoUpdateRef.current) { console.log("Auto-update check already performed in this session, skipping"); return; } hasRunYtDlpAutoUpdateRef.current = true; console.log("Checking yt-dlp auto-update with loaded config values:", { autoUpdate: YTDLP_AUTO_UPDATE, updateChannel: YTDLP_UPDATE_CHANNEL, lastCheck: ytDlpUpdateLastCheck }); const currentTimestamp = Date.now() const YTDLP_UPDATE_INTERVAL = 86400000 // 24H; if (YTDLP_AUTO_UPDATE && (ytDlpUpdateLastCheck === null || currentTimestamp - ytDlpUpdateLastCheck > YTDLP_UPDATE_INTERVAL)) { console.log("Running auto-update for yt-dlp..."); updateYtDlp(); } else { console.log("Skipping yt-dlp auto-update, either disabled or recently updated."); } }, [isSettingsStatePropagated, isKvPairsStatePropagated]); // Check for MacOS auto-registration useEffect(() => { // Only run once when both settings and KV pairs are loaded if (!isSettingsStatePropagated || !isKvPairsStatePropagated) { console.log("Skipping MacOS auto registration, waiting for configs to load..."); return; } // Skip if we've already run the macos auto-registration once if (isRegisteredToMacOsRef.current) { console.log("MacOS auto registration check already performed in this session, skipping"); return; } isRegisteredToMacOsRef.current = true; console.log("Checking MacOS auto registration with loaded config values:", { appVersion: appVersion, registeredVersion: macOsRegisteredVersion }); if (currentPlatform === 'macos' && (!macOsRegisteredVersion || macOsRegisteredVersion !== appVersion)) { console.log("Running MacOS auto registration..."); LOG.info('NEODLP', 'Running macOS registration'); registerToMac().then((result: { success: boolean, message: string }) => { if (result.success) { console.log("MacOS registration successful:", result.message); LOG.info('NEODLP', 'macOS registration successful'); } else { console.error("MacOS registration failed:", result.message); LOG.error('NEODLP', `macOS registration failed: ${result.message}`); } }).catch((error) => { console.error("Error during macOS registration:", error); LOG.error('NEODLP', `Error during macOS registration: ${error}`); }); } }, [isSettingsStatePropagated, isKvPairsStatePropagated]); useEffect(() => { if (isSuccessFetchingDownloadStates && downloadStates) { console.log("Download States fetched successfully:", downloadStates); setDownloadStates(downloadStates); } }, [downloadStates, isSuccessFetchingDownloadStates, setDownloadStates]); // Process queued downloads useEffect(() => { // Safely process the queue when dependencies change const timeoutId = setTimeout(() => { processQueuedDownloads(); }, 500); // Cleanup timeout if component unmounts or dependencies change return () => clearTimeout(timeoutId); }, [processQueuedDownloads, ongoingDownloads, queuedDownloads]); // show a toast and pause the download when yt-dlp exits unexpectedly useEffect(() => { if (isErrored && !isErrorExpected) { toast.error("Download Failed", { description: "yt-dlp exited unexpectedly. Please try again later", }); if (erroredDownloadId) { downloadStatusUpdater.mutate({ download_id: erroredDownloadId, download_status: 'paused' }, { onSuccess: (data) => { console.log("Download status updated successfully:", data); queryClient.invalidateQueries({ queryKey: ['download-states'] }); }, onError: (error) => { console.error("Failed to update download status:", error); } }) setErroredDownloadId(null); } setIsErrored(false); setIsErrorExpected(false); } }, [isErrored, isErrorExpected, erroredDownloadId, setIsErrored, setIsErrorExpected, setErroredDownloadId]); // auto reset error states after 3 seconds of expecting an error useEffect(() => { if (isErrorExpected) { const timeoutId = setTimeout(() => { setIsErrored(false); setIsErrorExpected(false); setErroredDownloadId(null); }, 3000); return () => clearTimeout(timeoutId); } }, [isErrorExpected, setIsErrorExpected]); return ( {children} ); }