feat: added neodlp logger and log viewer

This commit is contained in:
2025-09-02 00:15:29 +05:30
parent a280be323d
commit 9498464fa2
7 changed files with 120 additions and 15 deletions

View File

@@ -26,6 +26,7 @@ import { useMacOsRegisterer } from "@/helpers/use-macos-registerer";
import useAppUpdater from "@/helpers/use-app-updater"; 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";
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();
@@ -93,6 +94,7 @@ export default function App({ children }: { children: React.ReactNode }) {
const appWindow = getCurrentWebviewWindow() const appWindow = getCurrentWebviewWindow()
const navigate = useNavigate(); const navigate = useNavigate();
const LOG = useLogger();
const { updateYtDlp } = useYtDlpUpdater(); const { updateYtDlp } = useYtDlpUpdater();
const { registerToMac } = useMacOsRegisterer(); const { registerToMac } = useMacOsRegisterer();
const { checkForAppUpdate } = useAppUpdater(); const { checkForAppUpdate } = useAppUpdater();
@@ -151,12 +153,14 @@ export default function App({ children }: { children: React.ReactNode }) {
command.on('close', async (data) => { command.on('close', async (data) => {
if (data.code !== 0) { if (data.code !== 0) {
console.error(`yt-dlp failed to fetch metadata with code ${data.code}`); 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); resolve(null);
} else { } else {
try { try {
const matchedJson = jsonOutput.match(/{.*}/); const matchedJson = jsonOutput.match(/{.*}/);
if (!matchedJson) { if (!matchedJson) {
console.error(`Failed to match JSON: ${jsonOutput}`); console.error(`Failed to match JSON: ${jsonOutput}`);
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url})`);
resolve(null); resolve(null);
return; return;
} }
@@ -165,6 +169,7 @@ export default function App({ children }: { children: React.ReactNode }) {
} }
catch (e) { catch (e) {
console.error(`Failed to parse JSON: ${e}`); console.error(`Failed to parse JSON: ${e}`);
LOG.error('NEODLP', `Failed to parse metadata JSON for URL: ${url}) with error: ${e}`);
resolve(null); resolve(null);
} }
} }
@@ -172,23 +177,28 @@ export default function App({ children }: { children: React.ReactNode }) {
command.on('error', error => { command.on('error', error => {
console.error(`Error fetching metadata: ${error}`); console.error(`Error fetching metadata: ${error}`);
LOG.error('NEODLP', `Error occurred while fetching metadata for URL: ${url} : ${error}`);
resolve(null); resolve(null);
}); });
LOG.info('NEODLP', `Fetching metadata for URL: ${url}, with args: ${args.join(' ')}`);
command.spawn().then(child => { command.spawn().then(child => {
setSearchPid(child.pid); setSearchPid(child.pid);
}).catch(e => { }).catch(e => {
console.error(`Failed to spawn command: ${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); resolve(null);
}); });
}); });
} catch (e) { } catch (e) {
console.error(`Failed to fetch metadata: ${e}`); console.error(`Failed to fetch metadata: ${e}`);
LOG.error('NEODLP', `Failed to fetch metadata for URL: ${url} : ${e}`);
return null; return null;
} }
}; };
const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => { const startDownload = async (url: string, selectedFormat: string, selectedSubtitles?: string | null, resumeState?: DownloadState, playlistItems?: string) => {
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);
@@ -343,6 +353,7 @@ export default function App({ children }: { children: React.ReactNode }) {
'--downloader', 'dash,m3u8:native', '--downloader', 'dash,m3u8:native',
'--downloader-args', 'aria2c:-c -j 16 -x 16 -s 16 -k 1M --check-certificate=false' '--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) { if (resumeState || USE_ARIA2) {
@@ -357,6 +368,7 @@ export default function App({ children }: { children: React.ReactNode }) {
command.on('close', async (data) => { command.on('close', async (data) => {
if (data.code !== 0) { if (data.code !== 0) {
console.error(`Download failed with code ${data.code}`); console.error(`Download failed with code ${data.code}`);
LOG.error(`YT-DLP Download ${downloadId}`, `yt-dlp exited with code ${data.code} (ignore if you manually paused or cancelled the download)`);
if (!isErrorExpected) { if (!isErrorExpected) {
setIsErrored(true); setIsErrored(true);
setErroredDownloadId(downloadId); setErroredDownloadId(downloadId);
@@ -364,6 +376,7 @@ export default function App({ children }: { children: React.ReactNode }) {
} else { } else {
if (await fs.exists(tempDownloadPath)) { if (await fs.exists(tempDownloadPath)) {
downloadFilePath = await generateSafeFilePath(downloadFilePath); 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.copyFile(tempDownloadPath, downloadFilePath);
await fs.remove(tempDownloadPath); await fs.remove(tempDownloadPath);
} }
@@ -392,6 +405,7 @@ export default function App({ children }: { children: React.ReactNode }) {
command.on('error', error => { command.on('error', error => {
console.error(`Error: ${error}`); console.error(`Error: ${error}`);
LOG.error(`YT-DLP Download ${downloadId}`, `Error occurred: ${error}`);
setIsErrored(true); setIsErrored(true);
setErroredDownloadId(downloadId); setErroredDownloadId(downloadId);
}); });
@@ -399,6 +413,7 @@ export default function App({ children }: { children: React.ReactNode }) {
command.stdout.on('data', line => { command.stdout.on('data', line => {
if (line.startsWith('status:') || line.startsWith('[#')) { if (line.startsWith('status:') || line.startsWith('[#')) {
console.log(line); console.log(line);
LOG.info(`YT-DLP Download ${downloadId}`, line);
const currentProgress = parseProgressLine(line); const currentProgress = parseProgressLine(line);
const state: DownloadState = { const state: DownloadState = {
download_id: downloadId, download_id: downloadId,
@@ -457,6 +472,7 @@ export default function App({ children }: { children: React.ReactNode }) {
}) })
} else { } else {
console.log(line); 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) { if (!ongoingDownloads || ongoingDownloads && ongoingDownloads?.length < MAX_PARALLEL_DOWNLOADS) {
LOG.info('NEODLP', `Starting yt-dlp download with args: ${args.join(' ')}`);
const child = await command.spawn(); const child = await command.spawn();
processPid = child.pid; processPid = child.pid;
return Promise.resolve(); return Promise.resolve();
} else { } else {
console.log("Download is queued, not starting immediately."); console.log("Download is queued, not starting immediately.");
LOG.info('NEODLP', `Download queued with id: ${downloadId}`);
return Promise.resolve(); return Promise.resolve();
} }
} catch (e) { } catch (e) {
console.error(`Failed to start download: ${e}`); console.error(`Failed to start download: ${e}`);
LOG.error('NEODLP', `Failed to start download for URL: ${url} with error: ${e}`);
throw e; throw e;
} }
}; };
const pauseDownload = async (downloadState: DownloadState) => { const pauseDownload = async (downloadState: DownloadState) => {
try { 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)) { 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 setIsErrorExpected(true); // Set error expected to true to handle UI state
console.log("Killing process with PID:", downloadState.process_id); console.log("Killing process with PID:", downloadState.process_id);
@@ -611,6 +631,7 @@ export default function App({ children }: { children: React.ReactNode }) {
return Promise.resolve(); return Promise.resolve();
} catch (e) { } catch (e) {
console.error(`Failed to pause download: ${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; isProcessingQueueRef.current = false;
throw e; throw e;
} }
@@ -618,6 +639,7 @@ export default function App({ children }: { children: React.ReactNode }) {
const resumeDownload = async (downloadState: DownloadState) => { const resumeDownload = async (downloadState: DownloadState) => {
try { try {
LOG.info('NEODLP', `Resuming yt-dlp download with id: ${downloadState.download_id} (as per user request)`);
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,
@@ -627,12 +649,14 @@ export default function App({ children }: { children: React.ReactNode }) {
return Promise.resolve(); return Promise.resolve();
} catch (e) { } catch (e) {
console.error(`Failed to resume download: ${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; throw e;
} }
}; };
const cancelDownload = async (downloadState: DownloadState) => { const cancelDownload = async (downloadState: DownloadState) => {
try { 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)) { 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 setIsErrorExpected(true); // Set error expected to true to handle UI state
console.log("Killing process with PID:", downloadState.process_id); console.log("Killing process with PID:", downloadState.process_id);
@@ -658,6 +682,7 @@ export default function App({ children }: { children: React.ReactNode }) {
return Promise.resolve(); return Promise.resolve();
} catch (e) { } catch (e) {
console.error(`Failed to cancel download: ${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; throw e;
} }
} }
@@ -698,6 +723,7 @@ export default function App({ children }: { children: React.ReactNode }) {
} }
console.log("Starting queued download:", downloadToStart.download_id); console.log("Starting queued download:", downloadToStart.download_id);
LOG.info('NEODLP', `Starting queued download with id: ${downloadToStart.download_id}`);
lastProcessedDownloadIdRef.current = downloadToStart.download_id; lastProcessedDownloadIdRef.current = downloadToStart.download_id;
// Update status to 'starting' first // Update status to 'starting' first
@@ -719,6 +745,7 @@ export default function App({ children }: { children: React.ReactNode }) {
} catch (error) { } catch (error) {
console.error("Error processing download queue:", error); console.error("Error processing download queue:", error);
LOG.error('NEODLP', `Error processing download queue: ${error}`);
} finally { } finally {
// Important: reset the processing flag // Important: reset the processing flag
setTimeout(() => { setTimeout(() => {
@@ -761,6 +788,7 @@ export default function App({ children }: { children: React.ReactNode }) {
appWindow.setFocus(); appWindow.setFocus();
navigate('/'); navigate('/');
if (event.payload.url) { if (event.payload.url) {
LOG.info('NEODLP', `Received download request from neodlp browser extension for URL: ${event.payload.url}`);
const { setRequestedUrl, setAutoSubmitSearch } = useCurrentVideoMetadataStore.getState(); const { setRequestedUrl, setAutoSubmitSearch } = useCurrentVideoMetadataStore.getState();
setRequestedUrl(event.payload.url); setRequestedUrl(event.payload.url);
setAutoSubmitSearch(true); setAutoSubmitSearch(true);
@@ -912,6 +940,7 @@ export default function App({ children }: { children: React.ReactNode }) {
const YTDLP_UPDATE_INTERVAL = 86400000 // 24H; const YTDLP_UPDATE_INTERVAL = 86400000 // 24H;
if (YTDLP_AUTO_UPDATE && (ytDlpUpdateLastCheck === null || currentTimestamp - ytDlpUpdateLastCheck > YTDLP_UPDATE_INTERVAL)) { if (YTDLP_AUTO_UPDATE && (ytDlpUpdateLastCheck === null || currentTimestamp - ytDlpUpdateLastCheck > YTDLP_UPDATE_INTERVAL)) {
console.log("Running auto-update for yt-dlp..."); console.log("Running auto-update for yt-dlp...");
LOG.info('NEODLP', 'Updating yt-dlp to latest version (triggered because auto-update is enabled)');
updateYtDlp(); updateYtDlp();
} else { } else {
console.log("Skipping yt-dlp auto-update, either disabled or recently updated."); 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(); const currentPlatform = platform();
if (currentPlatform === 'macos' && (!macOsRegisteredVersion || macOsRegisteredVersion !== appVersion)) { if (currentPlatform === 'macos' && (!macOsRegisteredVersion || macOsRegisteredVersion !== appVersion)) {
console.log("Running MacOS auto registration..."); console.log("Running MacOS auto registration...");
LOG.info('NEODLP', 'Running macOS registration');
registerToMac().then((result: { success: boolean, message: string }) => { registerToMac().then((result: { success: boolean, message: string }) => {
if (result.success) { if (result.success) {
console.log("MacOS registration successful:", result.message); console.log("MacOS registration successful:", result.message);
LOG.info('NEODLP', 'macOS registration successful');
} else { } else {
console.error("MacOS registration failed:", result.message); console.error("MacOS registration failed:", result.message);
LOG.error('NEODLP', `macOS registration failed: ${result.message}`);
} }
}).catch((error) => { }).catch((error) => {
console.error("Error during macOS registration:", error); console.error("Error during macOS registration:", error);
LOG.error('NEODLP', `Error during macOS registration: ${error}`);
}); });
} }
}, [isSettingsStatePropagated, isKvPairsStatePropagated]); }, [isSettingsStatePropagated, isKvPairsStatePropagated]);

View File

@@ -1,12 +1,15 @@
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarTrigger } from "@/components/ui/sidebar";
import { getRouteName } from "@/utils"; import { getRouteName } from "@/utils";
// import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
// import { Terminal } from "lucide-react"; import { Terminal } from "lucide-react";
// import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; 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() { export default function Navbar() {
const location = useLocation(); const location = useLocation();
const logs = useLogger().getLogs();
return ( return (
<nav className="flex justify-between items-center py-3 px-4 sticky top-0 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b z-50"> <nav className="flex justify-between items-center py-3 px-4 sticky top-0 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b z-50">
@@ -15,16 +18,38 @@ export default function Navbar() {
<h1 className="text-lg text-primary font-semibold ml-4">{getRouteName(location.pathname)}</h1> <h1 className="text-lg text-primary font-semibold ml-4">{getRouteName(location.pathname)}</h1>
</div> </div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
{/* <Tooltip> <Dialog>
<TooltipTrigger asChild> <Tooltip>
<Button variant="outline" size="icon"> <TooltipTrigger asChild>
<Terminal /> <DialogTrigger asChild>
</Button> <Button variant="outline" size="icon">
</TooltipTrigger> <Terminal />
<TooltipContent> </Button>
<p>Logs</p> </DialogTrigger>
</TooltipContent> </TooltipTrigger>
</Tooltip> */} <TooltipContent>
<p>Logs</p>
</TooltipContent>
</Tooltip>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Log Viewer</DialogTitle>
<DialogDescription>Monitor real-time neodlp logs for the current session</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2 p-2 max-h-[300px] overflow-y-scroll overflow-x-hidden bg-muted">
{logs.length === 0 ? (
<p className="text-sm text-muted-foreground">NO LOGS TO SHOW!</p>
) : (
logs.slice().reverse().map((log, index) => (
<div key={index} className={`flex flex-col ${log.level === 'error' ? 'text-red-500' : log.level === 'warning' ? 'text-amber-500' : log.level === 'debug' ? 'text-sky-500' : 'text-foreground'}`}>
<p className="text-xs"><strong>{new Date(log.timestamp).toLocaleTimeString()}</strong> [{log.level.toUpperCase()}] <em>{log.context}</em> :</p>
<p className="text-xs font-mono break-all">{log.message}</p>
</div>
))
)}
</div>
</DialogContent>
</Dialog>
</div> </div>
</nav> </nav>
) )

26
src/helpers/use-logger.ts Normal file
View File

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

View File

@@ -741,7 +741,7 @@ 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 you network supports only one (some sites may not work)</p> <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>
<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"

View File

@@ -1,4 +1,4 @@
import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, LibraryPageStatesStore, SettingsPageStatesStore } from '@/types/store'; import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, LibraryPageStatesStore, LogsStore, SettingsPageStatesStore } from '@/types/store';
import { create } from 'zustand'; import { create } from 'zustand';
export const useBasePathsStore = create<BasePathsStore>((set) => ({ export const useBasePathsStore = create<BasePathsStore>((set) => ({
@@ -238,4 +238,11 @@ export const useKvPairsStatesStore = create<KvPairsStatesStore>((set) => ({
} }
})), })),
setKvPairs: (kvPairs) => set(() => ({ kvPairs })) setKvPairs: (kvPairs) => set(() => ({ kvPairs }))
}));
export const useLogsStore = create<LogsStore>((set) => ({
logs: [],
setLogs: (logs) => set(() => ({ logs })),
addLog: (log) => set((state) => ({ logs: [...state.logs, log] })),
clearLogs: () => set(() => ({ logs: [] }))
})); }));

6
src/types/logs.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface Log {
timestamp: number;
level: 'info' | 'warning' | 'error' | 'debug';
context: string;
message: string;
}

View File

@@ -3,6 +3,7 @@ import { RawVideoInfo } from "@/types/video";
import { Settings } from "@/types/settings"; import { 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";
export interface BasePathsStore { export interface BasePathsStore {
ffmpegPath: string | null; ffmpegPath: string | null;
@@ -118,4 +119,11 @@ export interface KvPairsStatesStore {
kvPairs: KvStore kvPairs: KvStore
setKvPairsKey: (key: string, value: unknown) => void; setKvPairsKey: (key: string, value: unknown) => void;
setKvPairs: (kvPairs: KvStore) => void; setKvPairs: (kvPairs: KvStore) => void;
}
export interface LogsStore {
logs: Log[];
setLogs: (logs: Log[]) => void;
addLog: (log: Log) => void;
clearLogs: () => void;
} }