diff --git a/src/App.tsx b/src/App.tsx index 263463d..15e2b0f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ 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"; export default function App({ children }: { children: React.ReactNode }) { const { data: downloadStates, isSuccess: isSuccessFetchingDownloadStates } = useFetchAllDownloadStates(); @@ -93,6 +94,7 @@ export default function App({ children }: { children: React.ReactNode }) { const appWindow = getCurrentWebviewWindow() const navigate = useNavigate(); + const LOG = useLogger(); const { updateYtDlp } = useYtDlpUpdater(); const { registerToMac } = useMacOsRegisterer(); const { checkForAppUpdate } = useAppUpdater(); @@ -151,12 +153,14 @@ export default function App({ children }: { children: React.ReactNode }) { 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; } @@ -165,6 +169,7 @@ export default function App({ children }: { children: React.ReactNode }) { } 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); } } @@ -172,23 +177,28 @@ export default function App({ children }: { children: React.ReactNode }) { 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, 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); @@ -343,6 +353,7 @@ export default function App({ children }: { children: React.ReactNode }) { '--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_ARIA2) { @@ -357,6 +368,7 @@ export default function App({ children }: { children: React.ReactNode }) { 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); @@ -364,6 +376,7 @@ export default function App({ children }: { children: React.ReactNode }) { } else { if (await fs.exists(tempDownloadPath)) { downloadFilePath = await generateSafeFilePath(downloadFilePath); + LOG.info('NEODLP', `yt-dlp download completed with id: ${downloadId}, moving downloaded file from: "${tempDownloadPath}" to final destination: "${downloadFilePath}"`); await fs.copyFile(tempDownloadPath, downloadFilePath); await fs.remove(tempDownloadPath); } @@ -392,6 +405,7 @@ export default function App({ children }: { children: React.ReactNode }) { command.on('error', error => { console.error(`Error: ${error}`); + LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`); setIsErrored(true); setErroredDownloadId(downloadId); }); @@ -399,6 +413,7 @@ export default function App({ children }: { children: React.ReactNode }) { command.stdout.on('data', line => { if (line.startsWith('status:') || line.startsWith('[#')) { console.log(line); + LOG.info(`YT-DLP Download ${downloadId}`, line); const currentProgress = parseProgressLine(line); const state: DownloadState = { download_id: downloadId, @@ -457,6 +472,7 @@ export default function App({ children }: { children: React.ReactNode }) { }) } else { console.log(line); + if (line.trim() !== '') LOG.info(`YT-DLP Download ${downloadId}`, line); } }); @@ -553,21 +569,25 @@ export default function App({ children }: { children: React.ReactNode }) { }) if (!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) { + LOG.info('NEODLP', `Starting yt-dlp download with args: ${args.join(' ')}`); 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); @@ -611,6 +631,7 @@ export default function App({ children }: { children: React.ReactNode }) { 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; } @@ -618,6 +639,7 @@ export default function App({ children }: { children: React.ReactNode }) { 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, @@ -627,12 +649,14 @@ export default function App({ children }: { children: React.ReactNode }) { 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); @@ -658,6 +682,7 @@ export default function App({ children }: { children: React.ReactNode }) { 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; } } @@ -698,6 +723,7 @@ export default function App({ children }: { children: React.ReactNode }) { } 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 @@ -719,6 +745,7 @@ export default function App({ children }: { children: React.ReactNode }) { } catch (error) { console.error("Error processing download queue:", error); + LOG.error('NEODLP', `Error processing download queue: ${error}`); } finally { // Important: reset the processing flag setTimeout(() => { @@ -761,6 +788,7 @@ export default function App({ children }: { children: React.ReactNode }) { 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); @@ -912,6 +940,7 @@ export default function App({ children }: { children: React.ReactNode }) { 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..."); + LOG.info('NEODLP', 'Updating yt-dlp to latest version (triggered because auto-update is enabled)'); updateYtDlp(); } else { console.log("Skipping yt-dlp auto-update, either disabled or recently updated."); @@ -938,14 +967,18 @@ export default function App({ children }: { children: React.ReactNode }) { const currentPlatform = platform(); 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]); diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index a1ee307..7849531 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,12 +1,15 @@ import { useLocation } from "react-router-dom"; import { SidebarTrigger } from "@/components/ui/sidebar"; import { getRouteName } from "@/utils"; -// import { Button } from "@/components/ui/button"; -// import { Terminal } from "lucide-react"; -// import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { Button } from "@/components/ui/button"; +import { Terminal } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { useLogger } from "@/helpers/use-logger"; export default function Navbar() { const location = useLocation(); + const logs = useLogger().getLogs(); return ( ) diff --git a/src/helpers/use-logger.ts b/src/helpers/use-logger.ts new file mode 100644 index 0000000..be872c9 --- /dev/null +++ b/src/helpers/use-logger.ts @@ -0,0 +1,26 @@ +import { useLogsStore } from "@/services/store"; + +export function useLogger() { + const logs = useLogsStore((state) => state.logs); + const addLog = useLogsStore((state) => state.addLog); + const clearLogs = useLogsStore((state) => state.clearLogs); + + const logger = { + info: (context: string, message: string) => { + addLog({ timestamp: Date.now(), level: 'info', context, message }); + }, + warning: (context: string, message: string) => { + addLog({ timestamp: Date.now(), level: 'warning', context, message }); + }, + error: (context: string, message: string) => { + addLog({ timestamp: Date.now(), level: 'error', context, message }); + }, + debug: (context: string, message: string) => { + addLog({ timestamp: Date.now(), level: 'debug', context, message }); + }, + getLogs: () => logs, + clearLogs, + }; + + return logger; +} \ No newline at end of file diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 9e8a8b8..d7ab2d0 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -741,7 +741,7 @@ export default function SettingsPage() {

Force Internet Protocol

-

Force using a specific internet protocol (ipv4/ipv6) for all downloads, useful if you network supports only one (some sites may not work)

+

Force using a specific internet protocol (ipv4/ipv6) for all downloads, useful if your network supports only one (some sites may not work)

((set) => ({ @@ -238,4 +238,11 @@ export const useKvPairsStatesStore = create((set) => ({ } })), setKvPairs: (kvPairs) => set(() => ({ kvPairs })) +})); + +export const useLogsStore = create((set) => ({ + logs: [], + setLogs: (logs) => set(() => ({ logs })), + addLog: (log) => set((state) => ({ logs: [...state.logs, log] })), + clearLogs: () => set(() => ({ logs: [] })) })); \ No newline at end of file diff --git a/src/types/logs.ts b/src/types/logs.ts new file mode 100644 index 0000000..49aa57a --- /dev/null +++ b/src/types/logs.ts @@ -0,0 +1,6 @@ +export interface Log { + timestamp: number; + level: 'info' | 'warning' | 'error' | 'debug'; + context: string; + message: string; +} \ No newline at end of file diff --git a/src/types/store.ts b/src/types/store.ts index 05511e7..07ca440 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -3,6 +3,7 @@ import { RawVideoInfo } from "@/types/video"; import { Settings } from "@/types/settings"; import { KvStore } from "@/types/kvStore"; import { Update } from "@tauri-apps/plugin-updater"; +import { Log } from "@/types/logs"; export interface BasePathsStore { ffmpegPath: string | null; @@ -118,4 +119,11 @@ export interface KvPairsStatesStore { kvPairs: KvStore setKvPairsKey: (key: string, value: unknown) => void; setKvPairs: (kvPairs: KvStore) => void; +} + +export interface LogsStore { + logs: Log[]; + setLogs: (logs: Log[]) => void; + addLog: (log: Log) => void; + clearLogs: () => void; } \ No newline at end of file