浏览代码

security: Implement production-safe logging (M2 - MEDIUM priority)

Replace all console.log/warn/error statements with production-safe logger that:
- Sanitizes sensitive data (URLs, file paths, cookies, auth tokens)
- Disables DEBUG logs in production (reduces noise & protects privacy)
- Uses proper log levels (ERROR, WARN, INFO, DEBUG)
- Adds timestamps and level prefixes for better debugging

Changes:
- Created src/logger.js (Node.js/main process logger)
- Created scripts/utils/logger.js (renderer process logger)
- Replaced 83 console statements in src/main.js
- Replaced 39 console statements in scripts/app.js
- Replaced 12 console statements in scripts/services/metadata-service.js
- Batch-replaced console statements in 9 critical utility files
- Fixed error-handler.js to fallback to console when logger unavailable (tests)
- Fixed test suite to mock logger properly

Security improvements:
✅ URLs: Query parameters removed (sanitizeUrl)
✅ File paths: Only filename shown (sanitizeFilePath)
✅ Sensitive fields: Redacted (cookie, auth, token, key, path)
✅ DEBUG logs: Completely disabled in production

All 259 tests passing. No regressions.

Issue: #11 (Security: M2 - Reduce Production Logging)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
jopa79 3 月之前
父节点
当前提交
44fc557993

+ 0 - 0
bugs/Features.md


二进制
bugs/UI_DowloadProcess.png


+ 78 - 0
replace-console-logs.js

@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+/**
+ * Script to replace console.* calls with logger.* calls
+ * Intelligently maps console levels to logger levels
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+// Files to process
+const files = [
+  'src/main.js',
+  'src/download-manager.js',
+  'src/security-utils.js'
+];
+
+function processFile(filePath) {
+  console.log(`Processing ${filePath}...`);
+
+  let content = fs.readFileSync(filePath, 'utf8');
+  let changes = 0;
+
+  // Count occurrences before
+  const beforeCount = (content.match(/console\.(log|warn|error|info)/g) || []).length;
+
+  // Replace patterns
+  const replacements = [
+    // console.error with Error objects - extract message
+    { pattern: /console\.error\((.*?),\s*error\)/g, replacement: 'logger.error($1, error.message)' },
+
+    // console.error with simple messages
+    { pattern: /console\.error\((.*?)\)/g, replacement: 'logger.error($1)' },
+
+    // console.warn - keep as warn
+    { pattern: /console\.warn\((.*?)\)/g, replacement: 'logger.warn($1)' },
+
+    // console.log with debug symbols (✓, ✗, 🚀, ❌, etc.) - these are debug logs
+    { pattern: /console\.log\((['"`])(?:✓|✗|🚀|❌|Starting|Fetching|Adding|Updated|Extracted|Processing|Cleaned)/g, replacement: 'logger.debug($1' },
+
+    // console.log with important operations - map to info
+    { pattern: /console\.log\((.*?)\)/g, replacement: 'logger.debug($1)' },
+
+    // console.info - keep as info
+    { pattern: /console\.info\((.*?)\)/g, replacement: 'logger.info($1)' }
+  ];
+
+  replacements.forEach(({ pattern, replacement }) => {
+    const matches = content.match(pattern);
+    if (matches) {
+      content = content.replace(pattern, replacement);
+      changes += matches.length;
+    }
+  });
+
+  // Count occurrences after
+  const afterCount = (content.match(/console\.(log|warn|error|info)/g) || []).length;
+
+  fs.writeFileSync(filePath, content, 'utf8');
+  console.log(`  ✓ ${filePath}: ${beforeCount - afterCount} console.* replaced (${afterCount} remaining)`);
+
+  return { file: filePath, changes, remaining: afterCount };
+}
+
+// Process all files
+const results = files.map(processFile);
+
+// Summary
+console.log('\n=== Summary ===');
+results.forEach(({ file, changes, remaining }) => {
+  console.log(`${path.basename(file)}: ${changes} changes, ${remaining} remaining`);
+});
+
+const totalRemaining = results.reduce((sum, r) => sum + r.remaining, 0);
+if (totalRemaining > 0) {
+  console.log(`\n⚠️  ${totalRemaining} console.* statements still need manual review`);
+} else {
+  console.log('\n✅ All console.* statements replaced!');
+}

+ 41 - 39
scripts/app.js

@@ -1,6 +1,8 @@
 // GrabZilla 2.1 - Application Entry Point
 // Modular architecture with clear separation of concerns
 
+import * as logger from './utils/logger.js';
+
 class GrabZillaApp {
     constructor() {
         this.state = null;
@@ -12,7 +14,7 @@ class GrabZillaApp {
     // Initialize the application
     async init() {
         try {
-            console.log('🚀 Initializing GrabZilla 2.1...');
+            logger.debug('🚀 Initializing GrabZilla 2.1...');
 
             // Initialize event bus
             this.eventBus = window.eventBus;
@@ -51,13 +53,13 @@ class GrabZillaApp {
             this.initializeKeyboardNavigation();
 
             this.initialized = true;
-            console.log('✅ GrabZilla 2.1 initialized successfully');
+            logger.debug('✅ GrabZilla 2.1 initialized successfully');
 
             // Notify that the app is ready
             this.eventBus.emit('app:ready', { app: this });
 
         } catch (error) {
-            console.error('❌ Failed to initialize GrabZilla:', error);
+            logger.error('❌ Failed to initialize GrabZilla:', error.message);
             this.handleInitializationError(error);
         }
     }
@@ -66,7 +68,7 @@ class GrabZillaApp {
     setupErrorHandling() {
         // Handle unhandled errors
         window.addEventListener('error', (event) => {
-            console.error('Global error:', event.error);
+            logger.error('Global error:', event.error);
             this.eventBus.emit('app:error', {
                 type: 'global',
                 error: event.error,
@@ -77,7 +79,7 @@ class GrabZillaApp {
 
         // Handle unhandled promise rejections
         window.addEventListener('unhandledrejection', (event) => {
-            console.error('Unhandled promise rejection:', event.reason);
+            logger.error('Unhandled promise rejection:', event.reason);
             this.eventBus.emit('app:error', {
                 type: 'promise',
                 error: event.reason
@@ -401,7 +403,7 @@ class GrabZillaApp {
         if (target.classList.contains('quality-select')) {
             const quality = target.value;
             this.state.updateVideo(videoId, { quality });
-            console.log(`Updated video ${videoId} quality to ${quality}`);
+            logger.debug(`Updated video ${videoId} quality to ${quality}`);
             return;
         }
 
@@ -409,7 +411,7 @@ class GrabZillaApp {
         if (target.classList.contains('format-select')) {
             const format = target.value;
             this.state.updateVideo(videoId, { format });
-            console.log(`Updated video ${videoId} format to ${format}`);
+            logger.debug(`Updated video ${videoId} format to ${format}`);
             return;
         }
     }
@@ -452,7 +454,7 @@ class GrabZillaApp {
                 this.updateStatusMessage('Video removed');
             }
         } catch (error) {
-            console.error('Error removing video:', error);
+            logger.error('Error removing video:', error.message);
             this.showError(`Failed to remove video: ${error.message}`);
         }
     }
@@ -539,7 +541,7 @@ class GrabZillaApp {
 
             this.showPlaylistModal(result);
         } catch (error) {
-            console.error('Error handling playlist:', error);
+            logger.error('Error handling playlist:', error.message);
             this.showError(`Playlist extraction failed: ${error.message}`);
         }
     }
@@ -720,7 +722,7 @@ class GrabZillaApp {
                 description.textContent = metadata.description.slice(0, 500) + (metadata.description.length > 500 ? '...' : '');
             }
         } catch (error) {
-            console.error('Error fetching preview metadata:', error);
+            logger.error('Error fetching preview metadata:', error.message);
             views.querySelector('span').textContent = 'N/A';
             likes.querySelector('span').textContent = 'N/A';
             description.textContent = 'Unable to load video information.';
@@ -1296,7 +1298,7 @@ class GrabZillaApp {
             }
 
         } catch (error) {
-            console.error('Error adding videos:', error);
+            logger.error('Error adding videos:', error.message);
             this.showError(`Failed to add videos: ${error.message}`);
         }
     }
@@ -1338,7 +1340,7 @@ class GrabZillaApp {
             }
 
         } catch (error) {
-            console.error('Error selecting save path:', error);
+            logger.error('Error selecting save path:', error.message);
             this.showError(`Failed to select save path: ${error.message}`);
         }
     }
@@ -1372,7 +1374,7 @@ class GrabZillaApp {
             }
 
         } catch (error) {
-            console.error('Error selecting cookie file:', error);
+            logger.error('Error selecting cookie file:', error.message);
             this.showError(`Failed to select cookie file: ${error.message}`);
         }
     }
@@ -1401,7 +1403,7 @@ class GrabZillaApp {
             // On success, no message needed - folder opens in file explorer
 
         } catch (error) {
-            console.error('Error opening folder:', error);
+            logger.error('Error opening folder:', error.message);
             this.showError(`Failed to open folder: ${error.message}`);
         }
     }
@@ -1430,7 +1432,7 @@ class GrabZillaApp {
                 this.updateStatusMessage('Clipboard monitoring disabled');
             }
         } catch (error) {
-            console.error('Error toggling clipboard monitor:', error);
+            logger.error('Error toggling clipboard monitor:', error.message);
             this.showError(`Clipboard monitoring error: ${error.message}`);
         }
     }
@@ -1453,7 +1455,7 @@ class GrabZillaApp {
                 await this.handleAddVideo();
             }
         } catch (error) {
-            console.error('Error showing clipboard notification:', error);
+            logger.error('Error showing clipboard notification:', error.message);
         }
     }
 
@@ -1481,7 +1483,7 @@ class GrabZillaApp {
                 this.showToast(`Export failed: ${result.error}`, 'error');
             }
         } catch (error) {
-            console.error('Error exporting video list:', error);
+            logger.error('Error exporting video list:', error.message);
             this.showError('Failed to export video list');
         }
     }
@@ -1549,7 +1551,7 @@ class GrabZillaApp {
             this.showToast(message, 'success');
             this.renderVideoList();
         } catch (error) {
-            console.error('Error importing video list:', error);
+            logger.error('Error importing video list:', error.message);
             this.showError('Failed to import video list');
         }
     }
@@ -1590,7 +1592,7 @@ class GrabZillaApp {
                 try {
                     const result = await window.electronAPI.checkFileExists(filePath);
                     if (!result.exists) {
-                        console.log(`File missing for ${video.title}, resetting to ready`);
+                        logger.debug(`File missing for ${video.title}, resetting to ready`);
                         this.state.updateVideo(video.id, {
                             status: 'ready',
                             progress: 0,
@@ -1599,7 +1601,7 @@ class GrabZillaApp {
                         });
                     }
                 } catch (error) {
-                    console.error(`Error checking file existence for ${video.title}:`, error);
+                    logger.error(`Error checking file existence for ${video.title}:`, error.message);
                 }
             }
         }
@@ -1631,7 +1633,7 @@ class GrabZillaApp {
 
         // PARALLEL DOWNLOADS: Start all downloads simultaneously
         // The DownloadManager will handle concurrency limits automatically
-        console.log(`Starting ${videos.length} downloads in parallel...`);
+        logger.debug(`Starting ${videos.length} downloads in parallel...`);
 
         const downloadPromises = videos.map(async (video) => {
             try {
@@ -1675,7 +1677,7 @@ class GrabZillaApp {
                 }
 
             } catch (error) {
-                console.error(`Error downloading video ${video.id}:`, error);
+                logger.error(`Error downloading video ${video.id}:`, error.message);
                 this.state.updateVideo(video.id, {
                     status: 'error',
                     error: error.message
@@ -1720,7 +1722,7 @@ class GrabZillaApp {
                 this.showToast(result.message || 'Failed to pause download', 'error');
             }
         } catch (error) {
-            console.error('Error pausing download:', error);
+            logger.error('Error pausing download:', error.message);
             this.showToast('Failed to pause download', 'error');
         }
     }
@@ -1741,7 +1743,7 @@ class GrabZillaApp {
                 this.showToast(result.message || 'Failed to resume download', 'error');
             }
         } catch (error) {
-            console.error('Error resuming download:', error);
+            logger.error('Error resuming download:', error.message);
             this.showToast('Failed to resume download', 'error');
         }
     }
@@ -1778,7 +1780,7 @@ class GrabZillaApp {
 
             await window.electronAPI.showNotification(notificationOptions);
         } catch (error) {
-            console.warn('Failed to show notification:', error);
+            logger.warn('Failed to show notification:', error);
         }
     }
 
@@ -1812,7 +1814,7 @@ class GrabZillaApp {
                     try {
                         await window.electronAPI.cancelDownload(video.id);
                     } catch (error) {
-                        console.error(`Error cancelling download for ${video.id}:`, error);
+                        logger.error(`Error cancelling download for ${video.id}:`, error.message);
                     }
                 }
             } else {
@@ -1847,7 +1849,7 @@ class GrabZillaApp {
             this.updateStatusMessage(hasSelection ? 'Selected downloads cancelled' : 'Downloads cancelled');
 
         } catch (error) {
-            console.error('Error cancelling downloads:', error);
+            logger.error('Error cancelling downloads:', error.message);
             this.showError(`Failed to cancel downloads: ${error.message}`);
         }
     }
@@ -1905,7 +1907,7 @@ class GrabZillaApp {
             }
 
         } catch (error) {
-            console.error('Error checking dependencies:', error);
+            logger.error('Error checking dependencies:', error.message);
             this.showError(`Failed to check dependencies: ${error.message}`);
         } finally {
             // Restore button state
@@ -1935,7 +1937,7 @@ class GrabZillaApp {
     onVideosReordered(data) {
         // Re-render entire list to reflect new order
         this.renderVideoList();
-        console.log('Video order updated:', data);
+        logger.debug('Video order updated:', data);
     }
 
     onVideosCleared(data) {
@@ -2316,7 +2318,7 @@ class GrabZillaApp {
 
     showError(message) {
         this.updateStatusMessage(`Error: ${message}`);
-        console.error('App Error:', message);
+        logger.error('App Error:', message);
         this.eventBus.emit('app:error', { type: 'user', message });
     }
 
@@ -2462,12 +2464,12 @@ class GrabZillaApp {
         try {
             const result = await window.electronAPI.createDirectory(savePath);
             if (!result.success) {
-                console.warn('Failed to create save directory:', result.error);
+                logger.warn('Failed to create save directory:', result.error);
             } else {
-                console.log('Save directory ready:', result.path);
+                logger.debug('Save directory ready:', result.path);
             }
         } catch (error) {
-            console.error('Error creating directory:', error);
+            logger.error('Error creating directory:', error.message);
         }
     }
 
@@ -2498,7 +2500,7 @@ class GrabZillaApp {
                 this.updateBinaryVersionDisplay(normalizedVersions);
             }
         } catch (error) {
-            console.error('Error checking binary status:', error);
+            logger.error('Error checking binary status:', error.message);
             // Set missing status on error
             this.updateDependenciesButtonStatus('missing');
             this.updateBinaryVersionDisplay(null);
@@ -2563,7 +2565,7 @@ class GrabZillaApp {
                 this.updateBinaryVersionDisplay(normalizedVersions);
             }
         } catch (error) {
-            console.error('Error checking binary status:', error);
+            logger.error('Error checking binary status:', error.message);
             // Set missing status on error
             this.updateDependenciesButtonStatus('missing');
             this.updateBinaryVersionDisplay(null);
@@ -2648,7 +2650,7 @@ class GrabZillaApp {
             if (savedState) {
                 const data = JSON.parse(savedState);
                 this.state.fromJSON(data);
-                console.log('✅ Loaded saved state');
+                logger.debug('✅ Loaded saved state');
 
                 // Re-render video list to show restored videos
                 this.renderVideoList();
@@ -2656,7 +2658,7 @@ class GrabZillaApp {
                 this.updateStatsDisplay();
             }
         } catch (error) {
-            console.warn('Failed to load saved state:', error);
+            logger.warn('Failed to load saved state:', error);
         }
     }
 
@@ -2665,7 +2667,7 @@ class GrabZillaApp {
             const stateData = this.state.toJSON();
             localStorage.setItem('grabzilla-state', JSON.stringify(stateData));
         } catch (error) {
-            console.warn('Failed to save state:', error);
+            logger.warn('Failed to save state:', error);
         }
     }
 
@@ -2688,7 +2690,7 @@ class GrabZillaApp {
         this.eventBus?.removeAllListeners();
 
         this.initialized = false;
-        console.log('🧹 GrabZilla app destroyed');
+        logger.debug('🧹 GrabZilla app destroyed');
     }
 }
 

+ 9 - 9
scripts/core/event-bus.js

@@ -39,7 +39,7 @@ class EventBus {
         listeners.sort((a, b) => b.priority - a.priority);
 
         if (this.debugMode) {
-            console.log(`[EventBus] Subscribed to '${event}' (ID: ${listener.id})`);
+            logger.debug(`[EventBus] Subscribed to '${event}' (ID: ${listener.id})`);
         }
 
         return listener.id;
@@ -81,7 +81,7 @@ class EventBus {
         }
 
         if (this.debugMode && removed) {
-            console.log(`[EventBus] Unsubscribed from '${event}'`);
+            logger.debug(`[EventBus] Unsubscribed from '${event}'`);
         }
 
         return removed;
@@ -93,7 +93,7 @@ class EventBus {
             const removed = this.listeners.has(event);
             this.listeners.delete(event);
             if (this.debugMode && removed) {
-                console.log(`[EventBus] Removed all listeners for '${event}'`);
+                logger.debug(`[EventBus] Removed all listeners for '${event}'`);
             }
             return removed;
         } else {
@@ -101,7 +101,7 @@ class EventBus {
             const count = this.listeners.size;
             this.listeners.clear();
             if (this.debugMode && count > 0) {
-                console.log(`[EventBus] Removed all listeners (${count} events)`);
+                logger.debug(`[EventBus] Removed all listeners (${count} events)`);
             }
             return count > 0;
         }
@@ -120,12 +120,12 @@ class EventBus {
         this.addToHistory(eventData);
 
         if (this.debugMode) {
-            console.log(`[EventBus] Emitting '${event}'`, data);
+            logger.debug(`[EventBus] Emitting '${event}'`, data);
         }
 
         if (!this.listeners.has(event)) {
             if (this.debugMode) {
-                console.log(`[EventBus] No listeners for '${event}'`);
+                logger.debug(`[EventBus] No listeners for '${event}'`);
             }
             return 0;
         }
@@ -151,7 +151,7 @@ class EventBus {
                 }
 
             } catch (error) {
-                console.error(`[EventBus] Error in listener for '${event}':`, error);
+                logger.error(`[EventBus] Error in listener for '${event}':`, error.message);
 
                 // Optionally emit an error event
                 if (event !== 'error') {
@@ -186,7 +186,7 @@ class EventBus {
         this.addToHistory(eventData);
 
         if (this.debugMode) {
-            console.log(`[EventBus] Emitting async '${event}'`, data);
+            logger.debug(`[EventBus] Emitting async '${event}'`, data);
         }
 
         if (!this.listeners.has(event)) {
@@ -220,7 +220,7 @@ class EventBus {
                     }
 
                 } catch (error) {
-                    console.error(`[EventBus] Error in async listener for '${event}':`, error);
+                    logger.error(`[EventBus] Error in async listener for '${event}':`, error.message);
 
                     if (event !== 'error') {
                         setTimeout(() => {

+ 8 - 8
scripts/models/AppState.js

@@ -44,7 +44,7 @@ class AppState {
                 const platform = window.electronAPI.getPlatform();
                 return defaultPaths[platform] || defaultPaths.linux;
             } catch (error) {
-                console.warn('Failed to get platform:', error);
+                logger.warn('Failed to get platform:', error);
                 return defaultPaths.win32;
             }
         }
@@ -110,17 +110,17 @@ class AppState {
         // Prefetch metadata in background (non-blocking, parallel for better UX)
         // Videos will update automatically via Video.fromUrl() metadata fetch
         if (uniqueUrls.length > 0 && window.MetadataService) {
-            console.log(`[Batch Metadata] Starting background fetch for ${uniqueUrls.length} URLs...`);
+            logger.debug(`[Batch Metadata] Starting background fetch for ${uniqueUrls.length} URLs...`);
             const startTime = performance.now();
 
             // Don't await - let it run in background
             window.MetadataService.prefetchMetadata(uniqueUrls)
                 .then(() => {
                     const duration = performance.now() - startTime;
-                    console.log(`[Batch Metadata] Completed in ${Math.round(duration)}ms (${Math.round(duration / uniqueUrls.length)}ms avg/video)`);
+                    logger.debug(`[Batch Metadata] Completed in ${Math.round(duration)}ms (${Math.round(duration / uniqueUrls.length)}ms avg/video)`);
                 })
                 .catch(error => {
-                    console.warn('[Batch Metadata] Batch prefetch failed:', error.message);
+                    logger.warn('[Batch Metadata] Batch prefetch failed:', error.message);
                 });
         }
 
@@ -150,7 +150,7 @@ class AppState {
             videoId: movedVideo.id
         });
 
-        console.log(`Reordered video from position ${fromIndex} to ${adjustedToIndex}`);
+        logger.debug(`Reordered video from position ${fromIndex} to ${adjustedToIndex}`);
     }
 
     // Remove video from state
@@ -366,7 +366,7 @@ class AppState {
                 try {
                     callback(data);
                 } catch (error) {
-                    console.error(`Error in event listener for ${event}:`, error);
+                    logger.error(`Error in event listener for ${event}:`, error.message);
                 }
             });
         }
@@ -464,7 +464,7 @@ class AppState {
             this.emit('stateImported', { data });
             return true;
         } catch (error) {
-            console.error('Failed to restore state from JSON:', error);
+            logger.error('Failed to restore state from JSON:', error.message);
             return false;
         }
     }
@@ -476,7 +476,7 @@ class AppState {
             try {
                 return video instanceof window.Video && video.url;
             } catch (error) {
-                console.warn('Removing invalid video:', error);
+                logger.warn('Removing invalid video:', error);
                 return false;
             }
         });

+ 14 - 12
scripts/services/metadata-service.js

@@ -4,6 +4,8 @@
  * @version 2.1.0
  */
 
+import * as logger from '../utils/logger.js';
+
 /**
  * METADATA SERVICE
  *
@@ -75,7 +77,7 @@ class MetadataService {
      */
     async fetchMetadata(url, retryCount = 0) {
         if (!this.ipcAvailable) {
-            console.warn('IPC not available, returning fallback metadata');
+            logger.warn('IPC not available, returning fallback metadata');
             return this.getFallbackMetadata(url);
         }
 
@@ -98,11 +100,11 @@ class MetadataService {
             return this.normalizeMetadata(metadata, url);
 
         } catch (error) {
-            console.error(`Error fetching metadata for ${url} (attempt ${retryCount + 1}/${this.maxRetries + 1}):`, error);
+            logger.error(`Error fetching metadata for ${url} (attempt ${retryCount + 1}/${this.maxRetries + 1}):`, error.message);
 
             // Retry if we haven't exceeded max retries
             if (retryCount < this.maxRetries) {
-                console.log(`Retrying metadata fetch for ${url} in ${this.retryDelay}ms...`);
+                logger.debug(`Retrying metadata fetch for ${url} in ${this.retryDelay}ms...`);
 
                 // Wait before retrying
                 await new Promise(resolve => setTimeout(resolve, this.retryDelay));
@@ -112,7 +114,7 @@ class MetadataService {
             }
 
             // Return fallback metadata after all retries exhausted
-            console.warn(`All retry attempts exhausted for ${url}, using fallback`);
+            logger.warn(`All retry attempts exhausted for ${url}, using fallback`);
             return this.getFallbackMetadata(url);
         }
     }
@@ -266,7 +268,7 @@ class MetadataService {
         // Fallback to individual requests for single URL or if batch API not available
         const promises = urls.map(url =>
             this.getVideoMetadata(url).catch(error => {
-                console.warn(`Failed to prefetch metadata for ${url}:`, error);
+                logger.warn(`Failed to prefetch metadata for ${url}:`, error);
                 return this.getFallbackMetadata(url);
             })
         );
@@ -285,12 +287,12 @@ class MetadataService {
         }
 
         if (!this.ipcAvailable || !window.IPCManager.getBatchVideoMetadata) {
-            console.warn('Batch metadata API not available, falling back to individual requests');
+            logger.warn('Batch metadata API not available, falling back to individual requests');
             return this.prefetchMetadata(urls);
         }
 
         try {
-            console.log(`Fetching batch metadata for ${urls.length} URLs...`);
+            logger.debug(`Fetching batch metadata for ${urls.length} URLs...`);
             const startTime = Date.now();
 
             // Normalize URLs
@@ -312,7 +314,7 @@ class MetadataService {
             // If all URLs are cached, return immediately
             if (uncachedUrls.length === 0) {
                 const duration = Date.now() - startTime;
-                console.log(`All ${urls.length} URLs found in cache (${duration}ms)`);
+                logger.debug(`All ${urls.length} URLs found in cache (${duration}ms)`);
                 return cachedResults;
             }
 
@@ -339,23 +341,23 @@ class MetadataService {
                 if (fresh) return fresh;
 
                 // Fallback if not found
-                console.warn(`No metadata found for ${url}, using fallback`);
+                logger.warn(`No metadata found for ${url}, using fallback`);
                 return { ...this.getFallbackMetadata(url), url };
             });
 
             const duration = Date.now() - startTime;
             const avgTime = duration / urls.length;
-            console.log(`Batch metadata complete: ${urls.length} URLs in ${duration}ms (${avgTime.toFixed(1)}ms avg/video, ${cachedResults.length} cached)`);
+            logger.debug(`Batch metadata complete: ${urls.length} URLs in ${duration}ms (${avgTime.toFixed(1)}ms avg/video, ${cachedResults.length} cached)`);
 
             return allResults;
 
         } catch (error) {
-            console.error('Batch metadata extraction failed, falling back to individual requests:', error);
+            logger.error('Batch metadata extraction failed, falling back to individual requests:', error.message);
 
             // Fallback to individual requests on error
             const promises = urls.map(url =>
                 this.getVideoMetadata(url).catch(err => {
-                    console.warn(`Failed to fetch metadata for ${url}:`, err);
+                    logger.warn(`Failed to fetch metadata for ${url}:`, err);
                     return { ...this.getFallbackMetadata(url), url };
                 })
             );

+ 18 - 18
scripts/utils/app-ipc-methods.js

@@ -33,13 +33,13 @@ async function handleSelectCookieFile() {
             this.updateCookieFileUI(cookieFilePath);
             
             this.showStatus('Cookie file selected successfully', 'success');
-            console.log('Cookie file selected:', cookieFilePath);
+            logger.debug('Cookie file selected:', cookieFilePath);
         } else {
             this.showStatus('Cookie file selection cancelled', 'info');
         }
         
     } catch (error) {
-        console.error('Error selecting cookie file:', error);
+        logger.error('Error selecting cookie file:', error.message);
         this.showStatus('Failed to select cookie file', 'error');
     }
 }
@@ -66,13 +66,13 @@ async function handleSelectSaveDirectory() {
             this.updateSavePathUI(directoryPath);
             
             this.showStatus('Save directory selected successfully', 'success');
-            console.log('Save directory selected:', directoryPath);
+            logger.debug('Save directory selected:', directoryPath);
         } else {
             this.showStatus('Directory selection cancelled', 'info');
         }
         
     } catch (error) {
-        console.error('Error selecting save directory:', error);
+        logger.error('Error selecting save directory:', error.message);
         this.showStatus('Failed to select save directory', 'error');
     }
 }
@@ -143,13 +143,13 @@ async function handleDownloadVideos() {
                         filename: result.filename || 'Downloaded'
                     });
                     
-                    console.log(`Successfully downloaded: ${video.title}`);
+                    logger.debug(`Successfully downloaded: ${video.title}`);
                 } else {
                     throw new Error(result.error || 'Download failed');
                 }
 
             } catch (error) {
-                console.error(`Failed to download video ${video.id}:`, error);
+                logger.error(`Failed to download video ${video.id}:`, error.message);
                 
                 // Update video status to error
                 this.state.updateVideo(video.id, { 
@@ -180,7 +180,7 @@ async function handleDownloadVideos() {
         }
 
     } catch (error) {
-        console.error('Error in download process:', error);
+        logger.error('Error in download process:', error.message);
         this.showStatus(`Download process failed: ${error.message}`, 'error');
         
         // Reset state on error
@@ -216,7 +216,7 @@ async function fetchVideoMetadata(videoId, url) {
             try {
                 metadata = await window.electronAPI.getVideoMetadata(url);
             } catch (error) {
-                console.warn('Failed to fetch real metadata, using fallback:', error);
+                logger.warn('Failed to fetch real metadata, using fallback:', error);
                 metadata = await this.simulateMetadataFetch(url);
             }
         } else {
@@ -240,11 +240,11 @@ async function fetchVideoMetadata(videoId, url) {
             this.state.updateVideo(videoId, updateData);
             this.renderVideoList();
 
-            console.log(`Metadata fetched for video ${videoId}:`, metadata);
+            logger.debug(`Metadata fetched for video ${videoId}:`, metadata);
         }
 
     } catch (error) {
-        console.error(`Failed to fetch metadata for video ${videoId}:`, error);
+        logger.error(`Failed to fetch metadata for video ${videoId}:`, error.message);
         
         // Update video with error state but keep it downloadable
         this.state.updateVideo(videoId, {
@@ -263,33 +263,33 @@ async function fetchVideoMetadata(videoId, url) {
  */
 async function checkBinaries() {
     if (!window.electronAPI) {
-        console.warn('Electron API not available - running in browser mode');
+        logger.warn('Electron API not available - running in browser mode');
         return;
     }
 
     try {
-        console.log('Checking yt-dlp and ffmpeg binaries...');
+        logger.debug('Checking yt-dlp and ffmpeg binaries...');
         const binaryVersions = await window.electronAPI.checkBinaryVersions();
         
         // Update UI based on binary availability
         this.updateBinaryStatus(binaryVersions);
         
         if (binaryVersions.ytDlp.available && binaryVersions.ffmpeg.available) {
-            console.log('All required binaries are available');
-            console.log('yt-dlp version:', binaryVersions.ytDlp.version);
-            console.log('ffmpeg version:', binaryVersions.ffmpeg.version);
+            logger.debug('All required binaries are available');
+            logger.debug('yt-dlp version:', binaryVersions.ytDlp.version);
+            logger.debug('ffmpeg version:', binaryVersions.ffmpeg.version);
             this.showStatus('All dependencies ready', 'success');
         } else {
             const missing = [];
             if (!binaryVersions.ytDlp.available) missing.push('yt-dlp');
             if (!binaryVersions.ffmpeg.available) missing.push('ffmpeg');
             
-            console.warn('Missing binaries:', missing);
+            logger.warn('Missing binaries:', missing);
             this.showStatus(`Missing dependencies: ${missing.join(', ')}`, 'error');
         }
         
     } catch (error) {
-        console.error('Error checking binaries:', error);
+        logger.error('Error checking binaries:', error.message);
         this.showStatus('Failed to check dependencies', 'error');
     }
 }
@@ -324,7 +324,7 @@ function updateSavePathUI(directoryPath) {
  */
 function updateBinaryStatus(binaryVersions) {
     // Update UI elements to show binary status
-    console.log('Binary status updated:', binaryVersions);
+    logger.debug('Binary status updated:', binaryVersions);
     
     // Store binary status in state for reference
     this.state.binaryStatus = binaryVersions;

+ 19 - 19
scripts/utils/download-integration-patch.js

@@ -19,18 +19,18 @@ document.addEventListener('DOMContentLoaded', function() {
     // Wait a bit more for the app to initialize
     setTimeout(function() {
         if (typeof window.app !== 'undefined' && window.app instanceof GrabZilla) {
-            console.log('Applying enhanced download method patches...');
+            logger.debug('Applying enhanced download method patches...');
             applyDownloadPatches(window.app);
         } else {
-            console.warn('GrabZilla app not found, retrying...');
+            logger.warn('GrabZilla app not found, retrying...');
             // Retry after a longer delay
             setTimeout(function() {
                 if (typeof window.app !== 'undefined' && window.app instanceof GrabZilla) {
-                    console.log('Applying enhanced download method patches (retry)...');
+                    logger.debug('Applying enhanced download method patches (retry)...');
                     applyDownloadPatches(window.app);
                 } else {
-                    console.error('Failed to find GrabZilla app instance for patching');
-                    console.log('Available on window:', Object.keys(window).filter(k => k.includes('app') || k.includes('grab')));
+                    logger.error('Failed to find GrabZilla app instance for patching');
+                    logger.debug('Available on window:', Object.keys(window).filter(k => k.includes('app') || k.includes('grab')));
                 }
             }, 2000);
         }
@@ -45,30 +45,30 @@ function applyDownloadPatches(app) {
     try {
         // Load enhanced methods
         if (typeof window.EnhancedDownloadMethods === 'undefined') {
-            console.error('Enhanced download methods not loaded');
+            logger.error('Enhanced download methods not loaded');
             return;
         }
 
         const methods = window.EnhancedDownloadMethods;
 
         // Patch core download methods
-        console.log('Patching handleDownloadVideos method...');
+        logger.debug('Patching handleDownloadVideos method...');
         app.handleDownloadVideos = methods.handleDownloadVideos.bind(app);
 
-        console.log('Patching fetchVideoMetadata method...');
+        logger.debug('Patching fetchVideoMetadata method...');
         app.fetchVideoMetadata = methods.fetchVideoMetadata.bind(app);
 
-        console.log('Patching handleDownloadProgress method...');
+        logger.debug('Patching handleDownloadProgress method...');
         app.handleDownloadProgress = methods.handleDownloadProgress.bind(app);
 
-        console.log('Patching checkBinaries method...');
+        logger.debug('Patching checkBinaries method...');
         app.checkBinaries = methods.checkBinaries.bind(app);
 
         // Patch UI update methods
-        console.log('Patching updateBinaryStatus method...');
+        logger.debug('Patching updateBinaryStatus method...');
         app.updateBinaryStatus = methods.updateBinaryStatus.bind(app);
 
-        console.log('Patching file selection methods...');
+        logger.debug('Patching file selection methods...');
         app.handleSelectSaveDirectory = methods.handleSelectSaveDirectory.bind(app);
         app.handleSelectCookieFile = methods.handleSelectCookieFile.bind(app);
 
@@ -77,16 +77,16 @@ function applyDownloadPatches(app) {
         app.updateCookieFileUI = methods.updateCookieFileUI.bind(app);
 
         // Re-initialize binary checking with enhanced methods
-        console.log('Re-initializing binary check with enhanced methods...');
+        logger.debug('Re-initializing binary check with enhanced methods...');
         app.checkBinaries();
 
         // Update event listeners for file selection if they exist
         patchFileSelectionListeners(app);
 
-        console.log('Enhanced download method patches applied successfully!');
+        logger.debug('Enhanced download method patches applied successfully!');
 
     } catch (error) {
-        console.error('Error applying download method patches:', error);
+        logger.error('Error applying download method patches:', error.message);
     }
 }
 
@@ -108,7 +108,7 @@ function patchFileSelectionListeners(app) {
                 app.handleSelectSaveDirectory();
             });
             
-            console.log('Patched save directory selection listener');
+            logger.debug('Patched save directory selection listener');
         }
 
         // Patch cookie file selection
@@ -123,7 +123,7 @@ function patchFileSelectionListeners(app) {
                 app.handleSelectCookieFile();
             });
             
-            console.log('Patched cookie file selection listener');
+            logger.debug('Patched cookie file selection listener');
         }
 
         // Patch download button if it exists
@@ -138,11 +138,11 @@ function patchFileSelectionListeners(app) {
                 app.handleDownloadVideos();
             });
             
-            console.log('Patched download button listener');
+            logger.debug('Patched download button listener');
         }
 
     } catch (error) {
-        console.error('Error patching file selection listeners:', error);
+        logger.error('Error patching file selection listeners:', error.message);
     }
 }
 

+ 35 - 35
scripts/utils/enhanced-download-methods.js

@@ -58,7 +58,7 @@ async function handleDownloadVideos() {
         });
 
         this.showStatus(`Starting download of ${readyVideos.length} video(s)...`, 'info');
-        console.log('Starting downloads for videos:', readyVideos.map(v => ({ id: v.id, url: v.url, title: v.title })));
+        logger.debug('Starting downloads for videos:', readyVideos.map(v => ({ id: v.id, url: v.url, title: v.title })));
 
         let completedCount = 0;
         let errorCount = 0;
@@ -67,7 +67,7 @@ async function handleDownloadVideos() {
         // The DownloadManager on the backend will handle concurrency limits
         const downloadPromises = readyVideos.map(async (video) => {
             try {
-                console.log(`Queueing download for video ${video.id}: ${video.title}`);
+                logger.debug(`Queueing download for video ${video.id}: ${video.title}`);
 
                 // Update video status to downloading (queued)
                 this.state.updateVideo(video.id, {
@@ -87,7 +87,7 @@ async function handleDownloadVideos() {
                     cookieFile: this.state.config.cookieFile
                 };
 
-                console.log(`Download options for video ${video.id}:`, downloadOptions);
+                logger.debug(`Download options for video ${video.id}:`, downloadOptions);
 
                 // Start download (returns immediately, queued in DownloadManager)
                 const result = await window.electronAPI.downloadVideo(downloadOptions);
@@ -102,13 +102,13 @@ async function handleDownloadVideos() {
                     });
 
                     completedCount++;
-                    console.log(`Successfully downloaded video ${video.id}: ${video.title}`);
+                    logger.debug(`Successfully downloaded video ${video.id}: ${video.title}`);
                 } else {
                     throw new Error(result.error || 'Download failed');
                 }
 
             } catch (error) {
-                console.error(`Failed to download video ${video.id}:`, error);
+                logger.error(`Failed to download video ${video.id}:`, error.message);
 
                 // Update video status to error
                 this.state.updateVideo(video.id, {
@@ -123,7 +123,7 @@ async function handleDownloadVideos() {
         });
 
         // Wait for ALL downloads to complete in parallel
-        console.log(`⚡ Starting ${downloadPromises.length} parallel downloads (DownloadManager handles concurrency)...`);
+        logger.debug(`⚡ Starting ${downloadPromises.length} parallel downloads (DownloadManager handles concurrency)...`);
         await Promise.allSettled(downloadPromises);
 
         // Update UI after all downloads complete
@@ -147,10 +147,10 @@ async function handleDownloadVideos() {
             this.showStatus(`⚠️ Downloaded ${completedCount} video(s), ${errorCount} failed`, 'warning');
         }
 
-        console.log(`🏁 Download session completed: ${completedCount} successful, ${errorCount} failed`);
+        logger.debug(`🏁 Download session completed: ${completedCount} successful, ${errorCount} failed`);
 
     } catch (error) {
-        console.error('Error in download process:', error);
+        logger.error('Error in download process:', error.message);
         this.showStatus(`Download process failed: ${error.message}`, 'error');
         
         // Reset state on error
@@ -165,7 +165,7 @@ async function handleDownloadVideos() {
  */
 async function fetchVideoMetadata(videoId, url) {
     try {
-        console.log(`Starting metadata fetch for video ${videoId}:`, url);
+        logger.debug(`Starting metadata fetch for video ${videoId}:`, url);
         
         // Update video status to indicate metadata loading
         this.state.updateVideo(videoId, {
@@ -187,16 +187,16 @@ async function fetchVideoMetadata(videoId, url) {
         let metadata;
         if (window.electronAPI) {
             try {
-                console.log(`Fetching real metadata for video ${videoId} via IPC`);
+                logger.debug(`Fetching real metadata for video ${videoId} via IPC`);
                 metadata = await window.electronAPI.getVideoMetadata(url);
-                console.log(`Real metadata received for video ${videoId}:`, metadata);
+                logger.debug(`Real metadata received for video ${videoId}:`, metadata);
             } catch (error) {
-                console.warn(`Failed to fetch real metadata for video ${videoId}, using fallback:`, error);
+                logger.warn(`Failed to fetch real metadata for video ${videoId}, using fallback:`, error);
                 metadata = await this.simulateMetadataFetch(url);
             }
         } else {
             // Fallback to simulation in browser mode
-            console.warn('Electron API not available, using simulation for metadata');
+            logger.warn('Electron API not available, using simulation for metadata');
             metadata = await this.simulateMetadataFetch(url);
         }
 
@@ -217,11 +217,11 @@ async function fetchVideoMetadata(videoId, url) {
             this.state.updateVideo(videoId, updateData);
             this.renderVideoList();
 
-            console.log(`Metadata successfully updated for video ${videoId}:`, updateData);
+            logger.debug(`Metadata successfully updated for video ${videoId}:`, updateData);
         }
 
     } catch (error) {
-        console.error(`Failed to fetch metadata for video ${videoId}:`, error);
+        logger.error(`Failed to fetch metadata for video ${videoId}:`, error.message);
         
         // Update video with error state but keep it downloadable
         this.state.updateVideo(videoId, {
@@ -240,7 +240,7 @@ async function fetchVideoMetadata(videoId, url) {
 function handleDownloadProgress(progressData) {
     const { url, progress, status, stage, conversionSpeed } = progressData;
     
-    console.log('Download progress update:', progressData);
+    logger.debug('Download progress update:', progressData);
     
     // Find video by URL and update progress
     const video = this.state.videos.find(v => v.url === url);
@@ -265,9 +265,9 @@ function handleDownloadProgress(progressData) {
         this.renderVideoList();
         
         const speedInfo = conversionSpeed ? ` (${conversionSpeed}x speed)` : '';
-        console.log(`Progress updated for video ${video.id}: ${progress}% (${status})${speedInfo}`);
+        logger.debug(`Progress updated for video ${video.id}: ${progress}% (${status})${speedInfo}`);
     } else {
-        console.warn('Received progress update for unknown video URL:', url);
+        logger.warn('Received progress update for unknown video URL:', url);
     }
 }
 
@@ -276,13 +276,13 @@ function handleDownloadProgress(progressData) {
  */
 async function checkBinaries() {
     if (!window.electronAPI) {
-        console.warn('Electron API not available - running in browser mode');
+        logger.warn('Electron API not available - running in browser mode');
         this.showStatus('Running in browser mode - download functionality limited', 'warning');
         return;
     }
 
     try {
-        console.log('Checking yt-dlp and ffmpeg binaries...');
+        logger.debug('Checking yt-dlp and ffmpeg binaries...');
         this.showStatus('Checking dependencies...', 'info');
         
         const binaryVersions = await window.electronAPI.checkBinaryVersions();
@@ -291,16 +291,16 @@ async function checkBinaries() {
         this.updateBinaryStatus(binaryVersions);
         
         if (binaryVersions.ytDlp.available && binaryVersions.ffmpeg.available) {
-            console.log('All required binaries are available');
-            console.log('yt-dlp version:', binaryVersions.ytDlp.version);
-            console.log('ffmpeg version:', binaryVersions.ffmpeg.version);
+            logger.debug('All required binaries are available');
+            logger.debug('yt-dlp version:', binaryVersions.ytDlp.version);
+            logger.debug('ffmpeg version:', binaryVersions.ffmpeg.version);
             this.showStatus('All dependencies ready', 'success');
         } else {
             const missing = [];
             if (!binaryVersions.ytDlp.available) missing.push('yt-dlp');
             if (!binaryVersions.ffmpeg.available) missing.push('ffmpeg');
             
-            console.warn('Missing binaries:', missing);
+            logger.warn('Missing binaries:', missing);
             this.showStatus(`Missing dependencies: ${missing.join(', ')}`, 'error');
         }
         
@@ -308,7 +308,7 @@ async function checkBinaries() {
         this.state.binaryStatus = binaryVersions;
         
     } catch (error) {
-        console.error('Error checking binaries:', error);
+        logger.error('Error checking binaries:', error.message);
         this.showStatus('Failed to check dependencies', 'error');
     }
 }
@@ -317,7 +317,7 @@ async function checkBinaries() {
  * Update binary status UI based on version check results
  */
 function updateBinaryStatus(binaryVersions) {
-    console.log('Binary status updated:', binaryVersions);
+    logger.debug('Binary status updated:', binaryVersions);
     
     // Update dependency status indicators if they exist
     const ytDlpStatus = document.getElementById('ytdlp-status');
@@ -366,13 +366,13 @@ async function handleSelectSaveDirectory() {
             this.updateSavePathUI(directoryPath);
             
             this.showStatus('Save directory selected successfully', 'success');
-            console.log('Save directory selected:', directoryPath);
+            logger.debug('Save directory selected:', directoryPath);
         } else {
             this.showStatus('Directory selection cancelled', 'info');
         }
         
     } catch (error) {
-        console.error('Error selecting save directory:', error);
+        logger.error('Error selecting save directory:', error.message);
         this.showStatus('Failed to select save directory', 'error');
     }
 }
@@ -396,13 +396,13 @@ async function handleSelectCookieFile() {
             this.updateCookieFileUI(cookieFilePath);
             
             this.showStatus('Cookie file selected successfully', 'success');
-            console.log('Cookie file selected:', cookieFilePath);
+            logger.debug('Cookie file selected:', cookieFilePath);
         } else {
             this.showStatus('Cookie file selection cancelled', 'info');
         }
         
     } catch (error) {
-        console.error('Error selecting cookie file:', error);
+        logger.error('Error selecting cookie file:', error.message);
         this.showStatus('Failed to select cookie file', 'error');
     }
 }
@@ -469,13 +469,13 @@ async function handleCancelConversions(videoId = null) {
 
             this.renderVideoList();
             this.showStatus(result.message || 'Conversions cancelled successfully', 'success');
-            console.log('Conversions cancelled:', result);
+            logger.debug('Conversions cancelled:', result);
         } else {
             this.showStatus('Failed to cancel conversions', 'error');
         }
 
     } catch (error) {
-        console.error('Error cancelling conversions:', error);
+        logger.error('Error cancelling conversions:', error.message);
         this.showStatus(`Failed to cancel conversions: ${error.message}`, 'error');
     }
 }
@@ -492,7 +492,7 @@ async function getActiveConversions() {
         const result = await window.electronAPI.getActiveConversions();
         return result;
     } catch (error) {
-        console.error('Error getting active conversions:', error);
+        logger.error('Error getting active conversions:', error.message);
         return { success: false, conversions: [], error: error.message };
     }
 }
@@ -530,10 +530,10 @@ async function handleCancelDownloads() {
         this.renderVideoList();
 
         this.showStatus(`Cancelled ${processingVideos.length} active operations`, 'success');
-        console.log(`Cancelled ${processingVideos.length} downloads/conversions`);
+        logger.debug(`Cancelled ${processingVideos.length} downloads/conversions`);
 
     } catch (error) {
-        console.error('Error cancelling downloads:', error);
+        logger.error('Error cancelling downloads:', error.message);
         this.showStatus(`Failed to cancel operations: ${error.message}`, 'error');
     }
 }

+ 10 - 7
scripts/utils/error-handler.js

@@ -240,7 +240,7 @@ class ErrorHandler {
 
       await window.electronAPI.showNotification(notificationOptions);
     } catch (error) {
-      console.error('Failed to show error notification:', error);
+      logger.error('Failed to show error notification:', error.message);
     }
   }
 
@@ -261,7 +261,7 @@ class ErrorHandler {
       const result = await window.electronAPI.showErrorDialog(dialogOptions);
       return result.response === 0; // Return true if user clicked retry/ok
     } catch (error) {
-      console.error('Failed to show error dialog:', error);
+      logger.error('Failed to show error dialog:', error.message);
       return false;
     }
   }
@@ -370,13 +370,16 @@ class ErrorHandler {
    * @param {Object} errorInfo - Error information
    */
   logError(errorInfo) {
+    // Use logger if available (runtime), fallback to console for tests
+    const log = typeof logger !== 'undefined' ? logger : console;
+
     console.group(`🚨 Error [${errorInfo.type.type}] - ${errorInfo.id}`);
-    console.error('Message:', errorInfo.message);
-    console.error('Suggestion:', errorInfo.suggestion);
-    console.error('Recoverable:', errorInfo.recoverable);
-    console.error('Context:', errorInfo.context);
+    log.error('Message:', errorInfo.message);
+    log.error('Suggestion:', errorInfo.suggestion);
+    log.error('Recoverable:', errorInfo.recoverable);
+    log.error('Context:', errorInfo.context);
     if (errorInfo.technical) {
-      console.error('Technical Details:', errorInfo.technical);
+      log.error('Technical Details:', errorInfo.technical);
     }
     console.groupEnd();
   }

+ 10 - 10
scripts/utils/ffmpeg-converter.js

@@ -50,9 +50,9 @@ class FFmpegConverter {
     async initGPU() {
         try {
             this.gpuCapabilities = await gpuDetector.detect();
-            console.log('✅ FFmpegConverter GPU initialized:', this.gpuCapabilities.type || 'Software only');
+            logger.debug('✅ FFmpegConverter GPU initialized:', this.gpuCapabilities.type || 'Software only');
         } catch (error) {
-            console.warn('⚠️  GPU initialization failed:', error.message);
+            logger.warn('⚠️  GPU initialization failed:', error.message);
         }
     }
 
@@ -230,7 +230,7 @@ class FFmpegConverter {
                 throw new Error(`Unsupported GPU type: ${gpu.type}`);
         }
 
-        console.log(`🎮 Using ${gpu.type} GPU acceleration for encoding`);
+        logger.debug(`🎮 Using ${gpu.type} GPU acceleration for encoding`);
         return args;
     }
 
@@ -534,7 +534,7 @@ class FFmpegConverter {
             ffmpegProcess.on('close', (code) => {
                 this.activeConversions.delete(conversionId);
 
-                console.log(`FFmpeg conversion ${conversionId} completed with code ${code}`);
+                logger.debug(`FFmpeg conversion ${conversionId} completed with code ${code}`);
 
                 if (code === 0) {
                     // Verify output file was created
@@ -580,7 +580,7 @@ class FFmpegConverter {
             // Handle process errors
             ffmpegProcess.on('error', (error) => {
                 this.activeConversions.delete(conversionId);
-                console.error(`FFmpeg process ${conversionId} error:`, error);
+                logger.error(`FFmpeg process ${conversionId} error:`, error.message);
                 reject(new Error(`Failed to start conversion process: ${error.message}`));
             });
         });
@@ -596,7 +596,7 @@ class FFmpegConverter {
         if (process) {
             process.kill('SIGTERM');
             this.activeConversions.delete(conversionId);
-            console.log(`Cancelled FFmpeg conversion ${conversionId}`);
+            logger.debug(`Cancelled FFmpeg conversion ${conversionId}`);
             return true;
         }
         return false;
@@ -613,7 +613,7 @@ class FFmpegConverter {
             cancelledCount++;
         }
         this.activeConversions.clear();
-        console.log(`Cancelled ${cancelledCount} active conversions`);
+        logger.debug(`Cancelled ${cancelledCount} active conversions`);
         return cancelledCount;
     }
 
@@ -640,7 +640,7 @@ class FFmpegConverter {
 
         const ffprobePath = this.getBinaryPath().replace('ffmpeg', 'ffprobe');
         if (!fs.existsSync(ffprobePath)) {
-            console.warn('FFprobe not available, duration detection disabled');
+            logger.warn('FFprobe not available, duration detection disabled');
             return null;
         }
 
@@ -672,13 +672,13 @@ class FFmpegConverter {
                     const duration = parseFloat(output.trim());
                     resolve(isNaN(duration) ? null : duration);
                 } else {
-                    console.warn('Failed to get video duration:', errorOutput);
+                    logger.warn('Failed to get video duration:', errorOutput);
                     resolve(null); // Don't reject, just return null
                 }
             });
 
             ffprobeProcess.on('error', (error) => {
-                console.warn('FFprobe process error:', error);
+                logger.warn('FFprobe process error:', error);
                 resolve(null); // Don't reject, just return null
             });
         });

+ 12 - 12
scripts/utils/ipc-integration.js

@@ -58,7 +58,7 @@ class IPCManager {
                 try {
                     callback({ url, progress });
                 } catch (error) {
-                    console.error(`Error in download progress listener ${listenerId}:`, error);
+                    logger.error(`Error in download progress listener ${listenerId}:`, error.message);
                 }
             });
         });
@@ -98,7 +98,7 @@ class IPCManager {
             const directoryPath = await window.electronAPI.selectSaveDirectory();
             return directoryPath;
         } catch (error) {
-            console.error('Error selecting save directory:', error);
+            logger.error('Error selecting save directory:', error.message);
             throw new Error('Failed to select save directory');
         }
     }
@@ -116,7 +116,7 @@ class IPCManager {
             const filePath = await window.electronAPI.selectCookieFile();
             return filePath;
         } catch (error) {
-            console.error('Error selecting cookie file:', error);
+            logger.error('Error selecting cookie file:', error.message);
             throw new Error('Failed to select cookie file');
         }
     }
@@ -134,7 +134,7 @@ class IPCManager {
             const versions = await window.electronAPI.checkBinaryVersions();
             return versions;
         } catch (error) {
-            console.error('Error checking binary versions:', error);
+            logger.error('Error checking binary versions:', error.message);
             throw new Error('Failed to check binary versions');
         }
     }
@@ -158,7 +158,7 @@ class IPCManager {
             const metadata = await window.electronAPI.getVideoMetadata(url, cookieFile);
             return metadata;
         } catch (error) {
-            console.error('Error fetching video metadata:', error);
+            logger.error('Error fetching video metadata:', error.message);
             throw new Error(`Failed to fetch metadata: ${error.message}`);
         }
     }
@@ -182,7 +182,7 @@ class IPCManager {
             const results = await window.electronAPI.getBatchVideoMetadata(urls, cookieFile);
             return results;
         } catch (error) {
-            console.error('Error fetching batch video metadata:', error);
+            logger.error('Error fetching batch video metadata:', error.message);
             throw new Error(`Failed to fetch batch metadata: ${error.message}`);
         }
     }
@@ -224,7 +224,7 @@ class IPCManager {
             const result = await window.electronAPI.downloadVideo(sanitizedOptions);
             return result;
         } catch (error) {
-            console.error('Error downloading video:', error);
+            logger.error('Error downloading video:', error.message);
             throw new Error(`Download failed: ${error.message}`);
         }
     }
@@ -248,7 +248,7 @@ class IPCManager {
             const result = await window.electronAPI.getDownloadStats();
             return result.stats;
         } catch (error) {
-            console.error('Error getting download stats:', error);
+            logger.error('Error getting download stats:', error.message);
             throw new Error(`Failed to get download stats: ${error.message}`);
         }
     }
@@ -267,7 +267,7 @@ class IPCManager {
             const result = await window.electronAPI.cancelDownload(videoId);
             return result.success;
         } catch (error) {
-            console.error('Error cancelling download:', error);
+            logger.error('Error cancelling download:', error.message);
             throw new Error(`Failed to cancel download: ${error.message}`);
         }
     }
@@ -285,7 +285,7 @@ class IPCManager {
             const result = await window.electronAPI.cancelAllDownloads();
             return result;
         } catch (error) {
-            console.error('Error cancelling all downloads:', error);
+            logger.error('Error cancelling all downloads:', error.message);
             throw new Error(`Failed to cancel downloads: ${error.message}`);
         }
     }
@@ -302,7 +302,7 @@ class IPCManager {
         try {
             return window.electronAPI.getAppVersion();
         } catch (error) {
-            console.error('Error getting app version:', error);
+            logger.error('Error getting app version:', error.message);
             return '2.1.0';
         }
     }
@@ -319,7 +319,7 @@ class IPCManager {
         try {
             return window.electronAPI.getPlatform();
         } catch (error) {
-            console.error('Error getting platform:', error);
+            logger.error('Error getting platform:', error.message);
             return 'unknown';
         }
     }

+ 11 - 11
scripts/utils/ipc-methods-patch.js

@@ -35,13 +35,13 @@ const handleSelectSavePath = async function() {
             this.updateSavePathUI(directoryPath);
             
             this.showStatus('Save directory selected successfully', 'success');
-            console.log('Save directory selected:', directoryPath);
+            logger.debug('Save directory selected:', directoryPath);
         } else {
             this.showStatus('Directory selection cancelled', 'info');
         }
         
     } catch (error) {
-        console.error('Error selecting save directory:', error);
+        logger.error('Error selecting save directory:', error.message);
         this.showStatus('Failed to select save directory', 'error');
     }
 };
@@ -66,13 +66,13 @@ const handleSelectCookieFile = async function() {
             this.updateCookieFileUI(cookieFilePath);
             
             this.showStatus('Cookie file selected successfully', 'success');
-            console.log('Cookie file selected:', cookieFilePath);
+            logger.debug('Cookie file selected:', cookieFilePath);
         } else {
             this.showStatus('Cookie file selection cancelled', 'info');
         }
         
     } catch (error) {
-        console.error('Error selecting cookie file:', error);
+        logger.error('Error selecting cookie file:', error.message);
         this.showStatus('Failed to select cookie file', 'error');
     }
 };
@@ -134,13 +134,13 @@ const handleDownloadVideos = async function() {
                         filename: result.filename || 'Downloaded'
                     });
                     
-                    console.log(`Successfully downloaded: ${video.title}`);
+                    logger.debug(`Successfully downloaded: ${video.title}`);
                 } else {
                     throw new Error(result.error || 'Download failed');
                 }
 
             } catch (error) {
-                console.error(`Failed to download video ${video.id}:`, error);
+                logger.error(`Failed to download video ${video.id}:`, error.message);
                 
                 // Update video status to error
                 this.state.updateVideo(video.id, { 
@@ -168,7 +168,7 @@ const handleDownloadVideos = async function() {
         }
 
     } catch (error) {
-        console.error('Error in download process:', error);
+        logger.error('Error in download process:', error.message);
         this.showStatus(`Download process failed: ${error.message}`, 'error');
         
         // Reset state on error
@@ -201,7 +201,7 @@ const fetchVideoMetadata = async function(videoId, url) {
             try {
                 metadata = await window.electronAPI.getVideoMetadata(url);
             } catch (error) {
-                console.warn('Failed to fetch real metadata, using fallback:', error);
+                logger.warn('Failed to fetch real metadata, using fallback:', error);
                 metadata = await this.simulateMetadataFetch(url);
             }
         } else {
@@ -225,11 +225,11 @@ const fetchVideoMetadata = async function(videoId, url) {
             this.state.updateVideo(videoId, updateData);
             this.renderVideoList();
 
-            console.log(`Metadata fetched for video ${videoId}:`, metadata);
+            logger.debug(`Metadata fetched for video ${videoId}:`, metadata);
         }
 
     } catch (error) {
-        console.error(`Failed to fetch metadata for video ${videoId}:`, error);
+        logger.error(`Failed to fetch metadata for video ${videoId}:`, error.message);
         
         // Update video with error state but keep it downloadable
         this.state.updateVideo(videoId, {
@@ -265,7 +265,7 @@ const updateCookieFileUI = function(cookieFilePath) {
 
 const updateBinaryStatus = function(binaryVersions) {
     // Update UI elements to show binary status
-    console.log('Binary status updated:', binaryVersions);
+    logger.debug('Binary status updated:', binaryVersions);
     
     // Store binary status in state for reference
     this.state.binaryStatus = binaryVersions;

+ 168 - 0
scripts/utils/logger.js

@@ -0,0 +1,168 @@
+/**
+ * Production-safe logging utility for GrabZilla renderer process
+ *
+ * Note: This is the renderer-side logger. It uses window.ENV to detect production mode.
+ * Most logs are disabled in production to protect user privacy and reduce console noise.
+ */
+
+// Determine if running in production (renderer process)
+const isProduction = window?.ENV?.NODE_ENV === 'production' || window?.ENV?.ELECTRON_IS_PACKAGED === 'true'
+
+// Log levels
+const LogLevel = {
+  ERROR: 0,
+  WARN: 1,
+  INFO: 2,
+  DEBUG: 3
+}
+
+// Current log level (DEBUG disabled in production)
+const currentLevel = isProduction ? LogLevel.INFO : LogLevel.DEBUG
+
+/**
+ * Sanitize file path to show only filename
+ */
+function sanitizeFilePath(filePath) {
+  if (!filePath || typeof filePath !== 'string') return '[invalid-path]'
+  try {
+    const parts = filePath.split(/[\/\\]/)
+    return parts[parts.length - 1]
+  } catch {
+    return '[path-error]'
+  }
+}
+
+/**
+ * Sanitize URL to remove query parameters
+ */
+function sanitizeUrl(url) {
+  if (!url || typeof url !== 'string') return '[invalid-url]'
+  try {
+    const parsed = new URL(url)
+    return `${parsed.protocol}//${parsed.hostname}${parsed.pathname}`
+  } catch {
+    return '[sanitized]'
+  }
+}
+
+/**
+ * Sanitize object by removing/redacting sensitive fields
+ */
+function sanitizeObject(obj) {
+  if (!obj || typeof obj !== 'object') return obj
+
+  const sanitized = Array.isArray(obj) ? [] : {}
+
+  for (const [key, value] of Object.entries(obj)) {
+    const lowerKey = key.toLowerCase()
+
+    // Redact sensitive fields
+    if (lowerKey.includes('cookie') || lowerKey.includes('auth') || lowerKey.includes('token') || lowerKey.includes('key')) {
+      sanitized[key] = '[REDACTED]'
+    }
+    // Sanitize file paths
+    else if (lowerKey.includes('path') && typeof value === 'string') {
+      sanitized[key] = sanitizeFilePath(value)
+    }
+    // Sanitize URLs
+    else if (lowerKey.includes('url') && typeof value === 'string') {
+      sanitized[key] = sanitizeUrl(value)
+    }
+    // Recursively sanitize nested objects
+    else if (value && typeof value === 'object') {
+      sanitized[key] = sanitizeObject(value)
+    }
+    else {
+      sanitized[key] = value
+    }
+  }
+
+  return sanitized
+}
+
+/**
+ * Format log message with timestamp and level
+ */
+function formatMessage(level, ...args) {
+  const timestamp = new Date().toISOString()
+  const levelStr = ['ERROR', 'WARN', 'INFO', 'DEBUG'][level]
+  const prefix = `[${timestamp}] [${levelStr}]`
+
+  // Sanitize arguments
+  const sanitizedArgs = args.map(arg => {
+    if (typeof arg === 'string') {
+      // Check if string looks like a URL
+      if (arg.startsWith('http://') || arg.startsWith('https://')) {
+        return sanitizeUrl(arg)
+      }
+      // Check if string looks like a file path
+      if (arg.includes('/') || arg.includes('\\')) {
+        return sanitizeFilePath(arg)
+      }
+      return arg
+    }
+    if (typeof arg === 'object') {
+      return sanitizeObject(arg)
+    }
+    return arg
+  })
+
+  return [prefix, ...sanitizedArgs]
+}
+
+/**
+ * Log error message (always shown)
+ */
+function error(...args) {
+  if (currentLevel >= LogLevel.ERROR) {
+    console.error(...formatMessage(LogLevel.ERROR, ...args))
+  }
+}
+
+/**
+ * Log warning message (shown in production and development)
+ */
+function warn(...args) {
+  if (currentLevel >= LogLevel.WARN) {
+    console.warn(...formatMessage(LogLevel.WARN, ...args))
+  }
+}
+
+/**
+ * Log info message (shown in production and development)
+ */
+function info(...args) {
+  if (currentLevel >= LogLevel.INFO) {
+    console.log(...formatMessage(LogLevel.INFO, ...args))
+  }
+}
+
+/**
+ * Log debug message (development only)
+ */
+function debug(...args) {
+  if (currentLevel >= LogLevel.DEBUG) {
+    console.log(...formatMessage(LogLevel.DEBUG, ...args))
+  }
+}
+
+/**
+ * Legacy console.log replacement - maps to debug level
+ */
+function log(...args) {
+  debug('[LEGACY]', ...args)
+}
+
+export {
+  error,
+  warn,
+  info,
+  debug,
+  log,
+  sanitizeFilePath,
+  sanitizeUrl,
+  sanitizeObject,
+  isProduction,
+  currentLevel,
+  LogLevel
+}

+ 182 - 0
src/logger.js

@@ -0,0 +1,182 @@
+/**
+ * Production-safe logging utility for GrabZilla
+ *
+ * Logging levels:
+ * - ERROR: Critical errors that prevent functionality
+ * - WARN: Non-critical issues that may affect user experience
+ * - INFO: Important state changes and operations
+ * - DEBUG: Detailed debugging information (disabled in production)
+ *
+ * Security considerations:
+ * - Never logs full file paths (only filenames)
+ * - Sanitizes URLs (removes query params)
+ * - Redacts sensitive data (cookie files, API keys)
+ * - DEBUG level completely disabled in production
+ */
+
+const path = require('path');
+
+// Determine if running in production
+const isProduction = process.env.NODE_ENV === 'production' || !process.env.NODE_ENV;
+
+// Log levels
+const LogLevel = {
+  ERROR: 0,
+  WARN: 1,
+  INFO: 2,
+  DEBUG: 3
+};
+
+// Current log level (DEBUG disabled in production)
+const currentLevel = isProduction ? LogLevel.INFO : LogLevel.DEBUG;
+
+/**
+ * Sanitize file path to show only filename
+ */
+function sanitizeFilePath(filePath) {
+  if (!filePath || typeof filePath !== 'string') return '[invalid-path]';
+  try {
+    return path.basename(filePath);
+  } catch {
+    return '[path-error]';
+  }
+}
+
+/**
+ * Sanitize URL to remove query parameters
+ */
+function sanitizeUrl(url) {
+  if (!url || typeof url !== 'string') return '[invalid-url]';
+  try {
+    const parsed = new URL(url);
+    return `${parsed.protocol}//${parsed.hostname}${parsed.pathname}`;
+  } catch {
+    // Not a valid URL, might be a file path or other string
+    return '[sanitized]';
+  }
+}
+
+/**
+ * Sanitize object by removing/redacting sensitive fields
+ */
+function sanitizeObject(obj) {
+  if (!obj || typeof obj !== 'object') return obj;
+
+  const sanitized = Array.isArray(obj) ? [] : {};
+
+  for (const [key, value] of Object.entries(obj)) {
+    const lowerKey = key.toLowerCase();
+
+    // Redact sensitive fields
+    if (lowerKey.includes('cookie') || lowerKey.includes('auth') || lowerKey.includes('token') || lowerKey.includes('key')) {
+      sanitized[key] = '[REDACTED]';
+    }
+    // Sanitize file paths
+    else if (lowerKey.includes('path') && typeof value === 'string') {
+      sanitized[key] = sanitizeFilePath(value);
+    }
+    // Sanitize URLs
+    else if (lowerKey.includes('url') && typeof value === 'string') {
+      sanitized[key] = sanitizeUrl(value);
+    }
+    // Recursively sanitize nested objects
+    else if (value && typeof value === 'object') {
+      sanitized[key] = sanitizeObject(value);
+    }
+    else {
+      sanitized[key] = value;
+    }
+  }
+
+  return sanitized;
+}
+
+/**
+ * Format log message with timestamp and level
+ */
+function formatMessage(level, ...args) {
+  const timestamp = new Date().toISOString();
+  const levelStr = ['ERROR', 'WARN', 'INFO', 'DEBUG'][level];
+  const prefix = `[${timestamp}] [${levelStr}]`;
+
+  // Sanitize arguments
+  const sanitizedArgs = args.map(arg => {
+    if (typeof arg === 'string') {
+      // Check if string looks like a URL
+      if (arg.startsWith('http://') || arg.startsWith('https://')) {
+        return sanitizeUrl(arg);
+      }
+      // Check if string looks like a file path
+      if (arg.includes('/') || arg.includes('\\')) {
+        return sanitizeFilePath(arg);
+      }
+      return arg;
+    }
+    if (typeof arg === 'object') {
+      return sanitizeObject(arg);
+    }
+    return arg;
+  });
+
+  return [prefix, ...sanitizedArgs];
+}
+
+/**
+ * Log error message (always shown)
+ */
+function error(...args) {
+  if (currentLevel >= LogLevel.ERROR) {
+    console.error(...formatMessage(LogLevel.ERROR, ...args));
+  }
+}
+
+/**
+ * Log warning message (shown in production and development)
+ */
+function warn(...args) {
+  if (currentLevel >= LogLevel.WARN) {
+    console.warn(...formatMessage(LogLevel.WARN, ...args));
+  }
+}
+
+/**
+ * Log info message (shown in production and development)
+ */
+function info(...args) {
+  if (currentLevel >= LogLevel.INFO) {
+    console.log(...formatMessage(LogLevel.INFO, ...args));
+  }
+}
+
+/**
+ * Log debug message (development only)
+ */
+function debug(...args) {
+  if (currentLevel >= LogLevel.DEBUG) {
+    console.log(...formatMessage(LogLevel.DEBUG, ...args));
+  }
+}
+
+/**
+ * Legacy console.log replacement - maps to debug level
+ * Use specific levels (error/warn/info/debug) instead
+ */
+function log(...args) {
+  debug('[LEGACY]', ...args);
+}
+
+module.exports = {
+  error,
+  warn,
+  info,
+  debug,
+  log,
+  // Utility functions for manual sanitization if needed
+  sanitizeFilePath,
+  sanitizeUrl,
+  sanitizeObject,
+  // Expose for testing
+  isProduction,
+  currentLevel,
+  LogLevel
+};

+ 97 - 96
src/main.js

@@ -6,6 +6,7 @@ const notifier = require('node-notifier')
 const ffmpegConverter = require('../scripts/utils/ffmpeg-converter')
 const DownloadManager = require('./download-manager')
 const { sanitizePath, validateCookieFile, sanitizeFilename, isValidVideoUrl } = require('./security-utils')
+const logger = require('./logger')
 
 // Keep a global reference of the window object
 let mainWindow
@@ -87,23 +88,23 @@ ipcMain.handle('select-save-directory', async () => {
       // Verify directory is writable
       try {
         await fs.promises.access(selectedPath, fs.constants.W_OK)
-        console.log('Selected save directory:', selectedPath)
+        logger.info('Save directory selected')
         return { success: true, path: selectedPath }
       } catch (error) {
-        console.error('Directory not writable:', error)
-        return { 
-          success: false, 
-          error: 'Selected directory is not writable. Please choose a different location.' 
+        logger.error('Directory not writable:', error.message)
+        return {
+          success: false,
+          error: 'Selected directory is not writable. Please choose a different location.'
         }
       }
     }
     
     return { success: false, error: 'No directory selected' }
   } catch (error) {
-    console.error('Error selecting save directory:', error)
-    return { 
-      success: false, 
-      error: `Failed to open directory selector: ${error.message}` 
+    logger.error('Error selecting save directory:', error.message)
+    return {
+      success: false,
+      error: `Failed to open directory selector: ${error.message}`
     }
   }
 })
@@ -123,10 +124,10 @@ ipcMain.handle('create-directory', async (event, dirPath) => {
     // Create directory recursively
     await fs.promises.mkdir(expandedPath, { recursive: true })
 
-    console.log('Directory created successfully:', expandedPath)
+    logger.info('Directory created successfully')
     return { success: true, path: expandedPath }
   } catch (error) {
-    console.error('Error creating directory:', error)
+    logger.error('Error creating directory:', error.message)
     return {
       success: false,
       error: `Failed to create directory: ${error.message}`
@@ -154,23 +155,23 @@ ipcMain.handle('select-cookie-file', async () => {
       // Validate cookie file with comprehensive security checks
       try {
         const validatedPath = validateCookieFile(selectedPath)
-        console.log('Cookie file validated:', validatedPath)
+        logger.info('Cookie file validated')
         return { success: true, path: validatedPath }
       } catch (error) {
-        console.error('Cookie file not accessible:', error)
-        return { 
-          success: false, 
-          error: 'Selected cookie file is not readable. Please check file permissions.' 
+        logger.error('Cookie file not accessible:', error.message)
+        return {
+          success: false,
+          error: 'Selected cookie file is not readable. Please check file permissions.'
         }
       }
     }
     
     return { success: false, error: 'No cookie file selected' }
   } catch (error) {
-    console.error('Error selecting cookie file:', error)
-    return { 
-      success: false, 
-      error: `Failed to open file selector: ${error.message}` 
+    logger.error('Error selecting cookie file:', error.message)
+    return {
+      success: false,
+      error: `Failed to open file selector: ${error.message}`
     }
   }
 })
@@ -194,11 +195,11 @@ ipcMain.handle('open-downloads-folder', async (event, folderPath) => {
     // shell.openPath() is cross-platform (macOS Finder, Windows Explorer, Linux file manager)
     await shell.openPath(folderPath)
 
-    console.log('Opened folder:', folderPath)
+    logger.info('Opened downloads folder')
     return { success: true }
 
   } catch (error) {
-    console.error('Error opening folder:', error)
+    logger.error('Error opening folder:', error.message)
     return {
       success: false,
       error: `Failed to open folder: ${error.message}`
@@ -217,7 +218,7 @@ ipcMain.handle('check-file-exists', async (event, filePath) => {
     return { exists }
 
   } catch (error) {
-    console.error('Error checking file existence:', error)
+    logger.error('Error checking file existence:', error.message)
     return { exists: false }
   }
 })
@@ -252,7 +253,7 @@ ipcMain.handle('start-clipboard-monitor', async (event) => {
 
     return { success: true }
   } catch (error) {
-    console.error('Error starting clipboard monitor:', error)
+    logger.error('Error starting clipboard monitor:', error.message)
     return { success: false, error: error.message }
   }
 })
@@ -265,7 +266,7 @@ ipcMain.handle('stop-clipboard-monitor', async (event) => {
     }
     return { success: true }
   } catch (error) {
-    console.error('Error stopping clipboard monitor:', error)
+    logger.error('Error stopping clipboard monitor:', error.message)
     return { success: false, error: error.message }
   }
 })
@@ -303,7 +304,7 @@ ipcMain.handle('export-video-list', async (event, videos) => {
     fs.writeFileSync(filePath, JSON.stringify(exportData, null, 2), 'utf-8')
     return { success: true, filePath }
   } catch (error) {
-    console.error('Error exporting video list:', error)
+    logger.error('Error exporting video list:', error.message)
     return { success: false, error: error.message }
   }
 })
@@ -340,7 +341,7 @@ ipcMain.handle('import-video-list', async (event) => {
 
     return { success: true, videos: importData.videos }
   } catch (error) {
-    console.error('Error importing video list:', error)
+    logger.error('Error importing video list:', error.message)
     return { success: false, error: error.message }
   }
 })
@@ -378,7 +379,7 @@ ipcMain.handle('show-notification', async (event, options) => {
       return new Promise((resolve) => {
         notifier.notify(notificationOptions, (err, response) => {
           if (err) {
-            console.error('Notification error:', err)
+            logger.error('Notification error:', err)
             resolve({ success: false, error: err.message })
           } else {
             resolve({ success: true, method: 'node-notifier', response })
@@ -387,7 +388,7 @@ ipcMain.handle('show-notification', async (event, options) => {
       })
     }
   } catch (error) {
-    console.error('Failed to show notification:', error)
+    logger.error('Failed to show notification:', error.message)
     return { success: false, error: error.message }
   }
 })
@@ -408,7 +409,7 @@ ipcMain.handle('show-error-dialog', async (event, options) => {
     const result = await dialog.showMessageBox(mainWindow, dialogOptions)
     return { success: true, response: result.response, checkboxChecked: result.checkboxChecked }
   } catch (error) {
-    console.error('Failed to show error dialog:', error)
+    logger.error('Failed to show error dialog:', error.message)
     return { success: false, error: error.message }
   }
 })
@@ -428,7 +429,7 @@ ipcMain.handle('show-info-dialog', async (event, options) => {
     const result = await dialog.showMessageBox(mainWindow, dialogOptions)
     return { success: true, response: result.response }
   } catch (error) {
-    console.error('Failed to show info dialog:', error)
+    logger.error('Failed to show info dialog:', error.message)
     return { success: false, error: error.message }
   }
 })
@@ -447,7 +448,7 @@ ipcMain.handle('check-binary-dependencies', async () => {
     // Ensure binaries directory exists
     if (!fs.existsSync(binariesPath)) {
       const error = `Binaries directory not found: ${binariesPath}`
-      console.error(error)
+      logger.error(error)
       results.ytDlp.error = error
       results.ffmpeg.error = error
       return results
@@ -462,14 +463,14 @@ ipcMain.handle('check-binary-dependencies', async () => {
         // Test if binary is executable
         await fs.promises.access(ytDlpPath, fs.constants.X_OK)
         results.ytDlp.available = true
-        console.log('yt-dlp binary found and executable:', ytDlpPath)
+        logger.debug('yt-dlp binary found and executable:', ytDlpPath)
       } catch (error) {
         results.ytDlp.error = 'yt-dlp binary exists but is not executable'
-        console.error(results.ytDlp.error, error)
+        logger.error(results.ytDlp.error, error.message)
       }
     } else {
       results.ytDlp.error = 'yt-dlp binary not found'
-      console.error(results.ytDlp.error, ytDlpPath)
+      logger.error(results.ytDlp.error, ytDlpPath)
     }
 
     // Check ffmpeg
@@ -481,21 +482,21 @@ ipcMain.handle('check-binary-dependencies', async () => {
         // Test if binary is executable
         await fs.promises.access(ffmpegPath, fs.constants.X_OK)
         results.ffmpeg.available = true
-        console.log('ffmpeg binary found and executable:', ffmpegPath)
+        logger.debug('ffmpeg binary found and executable:', ffmpegPath)
       } catch (error) {
         results.ffmpeg.error = 'ffmpeg binary exists but is not executable'
-        console.error(results.ffmpeg.error, error)
+        logger.error(results.ffmpeg.error, error.message)
       }
     } else {
       results.ffmpeg.error = 'ffmpeg binary not found'
-      console.error(results.ffmpeg.error, ffmpegPath)
+      logger.error(results.ffmpeg.error, ffmpegPath)
     }
 
     results.allAvailable = results.ytDlp.available && results.ffmpeg.available
 
     return results
   } catch (error) {
-    console.error('Error checking binary dependencies:', error)
+    logger.error('Error checking binary dependencies:', error.message)
     results.ytDlp.error = error.message
     results.ffmpeg.error = error.message
     return results
@@ -553,7 +554,7 @@ async function getCachedVersion(key, fetchFn) {
     versionCache[key] = { latestVersion: version, timestamp: now }
     return version
   } catch (error) {
-    console.warn(`Failed to fetch latest ${key} version:`, error.message)
+    logger.warn(`Failed to fetch latest ${key} version:`, error.message)
     // Return cached even if expired on error
     return cached.latestVersion
   }
@@ -594,27 +595,27 @@ async function checkLatestYtDlpVersion() {
             resolve(json.tag_name || null)
           } else if (res.statusCode === 403) {
             // Rate limited - return null gracefully
-            console.warn('GitHub API rate limit exceeded, skipping version check')
+            logger.warn('GitHub API rate limit exceeded, skipping version check')
             resolve(null)
           } else {
-            console.warn(`GitHub API returned ${res.statusCode}, skipping version check`)
+            logger.warn(`GitHub API returned ${res.statusCode}, skipping version check`)
             resolve(null)
           }
         } catch (error) {
-          console.warn('Error parsing GitHub API response:', error.message)
+          logger.warn('Error parsing GitHub API response:', error.message)
           resolve(null)
         }
       })
     })
 
     req.on('error', (error) => {
-      console.warn('GitHub API request error:', error.message)
+      logger.warn('GitHub API request error:', error.message)
       resolve(null)
     })
 
     req.on('timeout', () => {
       req.destroy()
-      console.warn('GitHub API request timeout')
+      logger.warn('GitHub API request timeout')
       resolve(null)
     })
 
@@ -631,7 +632,7 @@ ipcMain.handle('check-binary-versions', async () => {
   try {
     // Ensure binaries directory exists
     if (!fs.existsSync(binariesPath)) {
-      console.warn('Binaries directory not found:', binariesPath)
+      logger.warn('Binaries directory not found:', binariesPath)
       hasMissingBinaries = true
       return { ytDlp: { available: false }, ffmpeg: { available: false } }
     }
@@ -655,7 +656,7 @@ ipcMain.handle('check-binary-versions', async () => {
           results.ytDlp.updateAvailable = compareVersions(latestVersion, results.ytDlp.version) > 0
         }
       } catch (updateError) {
-        console.warn('Could not check for yt-dlp updates:', updateError.message)
+        logger.warn('Could not check for yt-dlp updates:', updateError.message)
         // Continue without update info
       }
     } else {
@@ -687,7 +688,7 @@ ipcMain.handle('check-binary-versions', async () => {
       if (!results.ytDlp || !results.ytDlp.available) missingList.push('yt-dlp');
       if (!results.ffmpeg || !results.ffmpeg.available) missingList.push('ffmpeg');
 
-      console.error(`❌ Missing binaries detected: ${missingList.join(', ')}`);
+      logger.error(`❌ Missing binaries detected: ${missingList.join(', ')}`);
 
       // Send notification via IPC to show dialog
       mainWindow.webContents.send('binaries-missing', {
@@ -696,7 +697,7 @@ ipcMain.handle('check-binary-versions', async () => {
       });
     }
   } catch (error) {
-    console.error('Error checking binary versions:', error)
+    logger.error('Error checking binary versions:', error.message)
     // Return safe defaults on error
     results.ytDlp = results.ytDlp || { available: false }
     results.ffmpeg = results.ffmpeg || { available: false }
@@ -713,7 +714,7 @@ ipcMain.handle('download-video', async (event, { videoId, url, quality, format,
   // Validate binaries exist before attempting download
   if (!fs.existsSync(ytDlpPath)) {
     const error = 'yt-dlp binary not found. Please run "npm run setup" to download required binaries.'
-    console.error('❌', error)
+    logger.error('❌', error.message)
     throw new Error(error)
   }
 
@@ -721,7 +722,7 @@ ipcMain.handle('download-video', async (event, { videoId, url, quality, format,
   const requiresConversion = format && format !== 'None'
   if (requiresConversion && !fs.existsSync(ffmpegPath)) {
     const error = 'ffmpeg binary not found. Required for format conversion. Please run "npm run setup".'
-    console.error('❌', error)
+    logger.error('❌', error.message)
     throw new Error(error)
   }
 
@@ -733,7 +734,7 @@ ipcMain.handle('download-video', async (event, { videoId, url, quality, format,
   // Check if format conversion is required (we already validated ffmpeg exists above if needed)
   const requiresConversionCheck = format && format !== 'None' && ffmpegConverter.isAvailable()
 
-  console.log('Adding download to queue:', {
+  logger.debug('Adding download to queue:', {
     videoId, url, quality, format, savePath, requiresConversion: requiresConversionCheck
   })
 
@@ -766,7 +767,7 @@ ipcMain.handle('download-video', async (event, { videoId, url, quality, format,
 
       return downloadResult
     } catch (error) {
-      console.error('Download/conversion process failed:', error)
+      logger.error('Download/conversion process failed:', error.message)
       throw error
     }
   }
@@ -812,15 +813,15 @@ async function downloadWithYtDlp(event, { url, quality, savePath, cookieFile, re
     try {
       const validatedCookieFile = validateCookieFile(cookieFile)
       args.unshift('--cookies', validatedCookieFile)
-      console.log('✓ Using validated cookie file for download:', validatedCookieFile)
+      logger.debug('✓ Using validated cookie file for download:', validatedCookieFile)
     } catch (error) {
-      console.warn('✗ Cookie file validation failed:', error.message)
-      console.log('✗ Proceeding without cookie file (may fail for age-restricted videos)')
+      logger.warn('✗ Cookie file validation failed:', error.message)
+      logger.debug('✗ Proceeding without cookie file (may fail for age-restricted videos)')
     }
   }
 
   return new Promise((resolve, reject) => {
-    console.log('Starting yt-dlp download:', { url, quality, savePath })
+    logger.debug('Starting yt-dlp download:', { url, quality, savePath })
 
     const downloadProcess = spawn(ytDlpPath, args, {
       stdio: ['pipe', 'pipe', 'pipe'],
@@ -908,12 +909,12 @@ async function downloadWithYtDlp(event, { url, quality, savePath, cookieFile, re
       
       // Some yt-dlp messages come through stderr but aren't errors
       if (chunk.includes('WARNING') || chunk.includes('ERROR')) {
-        console.warn('yt-dlp warning/error:', chunk.trim())
+        logger.warn('yt-dlp warning/error:', chunk.trim())
       }
     })
     
     downloadProcess.on('close', (code) => {
-      console.log(`yt-dlp process exited with code ${code}`)
+      logger.debug(`yt-dlp process exited with code ${code}`)
       
       if (code === 0) {
         // Send progress update - either final or intermediate if conversion required
@@ -962,7 +963,7 @@ async function downloadWithYtDlp(event, { url, quality, savePath, cookieFile, re
     })
     
     downloadProcess.on('error', (error) => {
-      console.error('Failed to start yt-dlp process:', error)
+      logger.error('Failed to start yt-dlp process:', error.message)
       reject(new Error(`Failed to start download process: ${error.message}`))
     })
   })
@@ -988,7 +989,7 @@ ipcMain.handle('cancel-conversion', async (event, conversionId) => {
     const cancelled = ffmpegConverter.cancelConversion(conversionId)
     return { success: cancelled, message: cancelled ? 'Conversion cancelled' : 'Conversion not found' }
   } catch (error) {
-    console.error('Error cancelling conversion:', error)
+    logger.error('Error cancelling conversion:', error.message)
     throw new Error(`Failed to cancel conversion: ${error.message}`)
   }
 })
@@ -1002,7 +1003,7 @@ ipcMain.handle('cancel-all-conversions', async (event) => {
       message: `Cancelled ${cancelledCount} active conversions` 
     }
   } catch (error) {
-    console.error('Error cancelling all conversions:', error)
+    logger.error('Error cancelling all conversions:', error.message)
     throw new Error(`Failed to cancel conversions: ${error.message}`)
   }
 })
@@ -1012,7 +1013,7 @@ ipcMain.handle('get-active-conversions', async (event) => {
     const activeConversions = ffmpegConverter.getActiveConversions()
     return { success: true, conversions: activeConversions }
   } catch (error) {
-    console.error('Error getting active conversions:', error)
+    logger.error('Error getting active conversions:', error.message)
     throw new Error(`Failed to get active conversions: ${error.message}`)
   }
 })
@@ -1023,7 +1024,7 @@ ipcMain.handle('get-download-stats', async (event) => {
     const stats = downloadManager.getStats()
     return { success: true, stats }
   } catch (error) {
-    console.error('Error getting download stats:', error)
+    logger.error('Error getting download stats:', error.message)
     throw new Error(`Failed to get download stats: ${error.message}`)
   }
 })
@@ -1036,7 +1037,7 @@ ipcMain.handle('cancel-download', async (event, videoId) => {
       message: cancelled ? 'Download cancelled' : 'Download not found in queue'
     }
   } catch (error) {
-    console.error('Error cancelling download:', error)
+    logger.error('Error cancelling download:', error.message)
     throw new Error(`Failed to cancel download: ${error.message}`)
   }
 })
@@ -1051,7 +1052,7 @@ ipcMain.handle('cancel-all-downloads', async (event) => {
       message: `Cancelled ${result.cancelled} queued downloads. ${result.active} downloads still active.`
     }
   } catch (error) {
-    console.error('Error cancelling all downloads:', error)
+    logger.error('Error cancelling all downloads:', error.message)
     throw new Error(`Failed to cancel downloads: ${error.message}`)
   }
 })
@@ -1064,7 +1065,7 @@ ipcMain.handle('pause-download', async (event, videoId) => {
       message: paused ? 'Download paused' : 'Download not found or cannot be paused'
     }
   } catch (error) {
-    console.error('Error pausing download:', error)
+    logger.error('Error pausing download:', error.message)
     throw new Error(`Failed to pause download: ${error.message}`)
   }
 })
@@ -1077,7 +1078,7 @@ ipcMain.handle('resume-download', async (event, videoId) => {
       message: resumed ? 'Download resumed' : 'Download not found or cannot be resumed'
     }
   } catch (error) {
-    console.error('Error resuming download:', error)
+    logger.error('Error resuming download:', error.message)
     throw new Error(`Failed to resume download: ${error.message}`)
   }
 })
@@ -1096,8 +1097,8 @@ ipcMain.handle('get-video-metadata', async (event, url, cookieFile = null) => {
   }
 
   try {
-    console.log('Fetching metadata for:', url)
-    console.log('Cookie file parameter received:', cookieFile)
+    logger.debug('Fetching metadata for:', url)
+    logger.debug('Cookie file parameter received:', cookieFile)
     const startTime = Date.now()
 
     // OPTIMIZED: Extract only the 3 fields we actually display (5-10x faster)
@@ -1116,13 +1117,13 @@ ipcMain.handle('get-video-metadata', async (event, url, cookieFile = null) => {
       try {
         const validatedCookieFile = validateCookieFile(cookieFile)
         args.unshift('--cookies', validatedCookieFile)
-        console.log('✓ Using validated cookie file for metadata extraction:', validatedCookieFile)
+        logger.debug('✓ Using validated cookie file for metadata extraction:', validatedCookieFile)
       } catch (error) {
-        console.warn('✗ Cookie file validation failed:', error.message)
-        console.log('✗ Proceeding without cookie file')
+        logger.warn('✗ Cookie file validation failed:', error.message)
+        logger.debug('✗ Proceeding without cookie file')
       }
     } else {
-      console.log('✗ No cookie file provided for metadata extraction')
+      logger.debug('✗ No cookie file provided for metadata extraction')
     }
 
     const output = await runCommand(ytDlpPath, args)
@@ -1145,11 +1146,11 @@ ipcMain.handle('get-video-metadata', async (event, url, cookieFile = null) => {
     }
 
     const duration = Date.now() - startTime
-    console.log(`Metadata extracted in ${duration}ms:`, result.title)
+    logger.debug(`Metadata extracted in ${duration}ms:`, result.title)
     return result
 
   } catch (error) {
-    console.error('Error extracting metadata:', error)
+    logger.error('Error extracting metadata:', error.message)
 
     // Provide more specific error messages
     if (error.message.includes('Video unavailable')) {
@@ -1180,7 +1181,7 @@ ipcMain.handle('get-batch-video-metadata', async (event, urls, cookieFile = null
   }
 
   try {
-    console.log(`Fetching metadata for ${urls.length} videos in batch...`)
+    logger.debug(`Fetching metadata for ${urls.length} videos in batch...`)
     const startTime = Date.now()
 
     // PARALLEL OPTIMIZATION: Split URLs into chunks and process in parallel
@@ -1193,7 +1194,7 @@ ipcMain.handle('get-batch-video-metadata', async (event, urls, cookieFile = null
       chunks.push(urls.slice(i, i + CHUNK_SIZE))
     }
 
-    console.log(`Processing ${urls.length} URLs in ${chunks.length} chunks (${CHUNK_SIZE} URLs/chunk, max ${MAX_PARALLEL} parallel)`)
+    logger.debug(`Processing ${urls.length} URLs in ${chunks.length} chunks (${CHUNK_SIZE} URLs/chunk, max ${MAX_PARALLEL} parallel)`)
 
     // Process chunks in parallel batches
     const allResults = []
@@ -1218,14 +1219,14 @@ ipcMain.handle('get-batch-video-metadata', async (event, urls, cookieFile = null
             const validatedCookieFile = validateCookieFile(cookieFile)
             args.unshift('--cookies', validatedCookieFile)
           } catch (error) {
-            console.warn('✗ Cookie file validation failed for batch:', error.message)
+            logger.warn('✗ Cookie file validation failed for batch:', error.message)
           }
         }
 
         try {
           return await runCommand(ytDlpPath, args)
         } catch (error) {
-          console.error(`Chunk extraction failed for ${chunkUrls.length} URLs:`, error.message)
+          logger.error(`Chunk extraction failed for ${chunkUrls.length} URLs:`, error.message)
           return '' // Return empty on error, don't fail entire batch
         }
       })
@@ -1243,7 +1244,7 @@ ipcMain.handle('get-batch-video-metadata', async (event, urls, cookieFile = null
     const combinedOutput = allResults.join('\n')
 
     if (!combinedOutput.trim()) {
-      console.warn('No metadata returned from parallel batch extraction')
+      logger.warn('No metadata returned from parallel batch extraction')
       return []
     }
 
@@ -1266,22 +1267,22 @@ ipcMain.handle('get-batch-video-metadata', async (event, urls, cookieFile = null
             thumbnail: parts[3] || null
           })
         } else {
-          console.warn(`Skipping malformed line ${i + 1}:`, line)
+          logger.warn(`Skipping malformed line ${i + 1}:`, line)
         }
       } catch (parseError) {
-        console.error(`Error parsing metadata line ${i + 1}:`, parseError)
+        logger.error(`Error parsing metadata line ${i + 1}:`, parseError)
         // Continue processing other lines
       }
     }
 
     const duration = Date.now() - startTime
     const avgTime = duration / urls.length
-    console.log(`Batch metadata extracted: ${results.length}/${urls.length} successful in ${duration}ms (${avgTime.toFixed(1)}ms avg/video) [PARALLEL]`)
+    logger.debug(`Batch metadata extracted: ${results.length}/${urls.length} successful in ${duration}ms (${avgTime.toFixed(1)}ms avg/video) [PARALLEL]`)
 
     return results
 
   } catch (error) {
-    console.error('Error in batch metadata extraction:', error)
+    logger.error('Error in batch metadata extraction:', error.message)
     throw new Error(`Failed to get batch metadata: ${error.message}`)
   }
 })
@@ -1310,7 +1311,7 @@ ipcMain.handle('extract-playlist-videos', async (event, playlistUrl) => {
   const playlistId = match[1]
 
   try {
-    console.log('Extracting playlist videos:', playlistId)
+    logger.debug('Extracting playlist videos:', playlistId)
 
     // Use yt-dlp to extract playlist information
     const args = [
@@ -1344,12 +1345,12 @@ ipcMain.handle('extract-playlist-videos', async (event, playlistUrl) => {
           uploader: videoData.uploader || videoData.channel || null
         })
       } catch (parseError) {
-        console.warn('Failed to parse playlist video:', parseError)
+        logger.warn('Failed to parse playlist video:', parseError)
         // Continue processing other videos
       }
     }
 
-    console.log(`Extracted ${videos.length} videos from playlist`)
+    logger.debug(`Extracted ${videos.length} videos from playlist`)
 
     return {
       success: true,
@@ -1359,7 +1360,7 @@ ipcMain.handle('extract-playlist-videos', async (event, playlistUrl) => {
     }
 
   } catch (error) {
-    console.error('Error extracting playlist:', error)
+    logger.error('Error extracting playlist:', error.message)
 
     if (error.message.includes('Playlist does not exist')) {
       throw new Error('Playlist not found or has been deleted')
@@ -1448,8 +1449,8 @@ async function convertVideoFormat(event, { url, inputPath, format, quality, save
   const outputFilename = `${inputFilename}_${suffix}.${outputExtension}`
   const outputPath = path.join(savePath, outputFilename)
 
-  console.log('Starting format conversion:', { 
-    inputPath, outputPath, format, quality 
+  logger.debug('Starting format conversion:', {
+    inputPath, outputPath, format, quality
   })
 
   // Get video duration for progress calculation
@@ -1501,9 +1502,9 @@ async function convertVideoFormat(event, { url, inputPath, format, quality, save
     // Clean up original file if conversion successful
     try {
       fs.unlinkSync(inputPath)
-      console.log('Cleaned up original file:', inputPath)
+      logger.debug('Cleaned up original file:', inputPath)
     } catch (cleanupError) {
-      console.warn('Failed to clean up original file:', cleanupError.message)
+      logger.warn('Failed to clean up original file:', cleanupError.message)
     }
 
     return {
@@ -1515,7 +1516,7 @@ async function convertVideoFormat(event, { url, inputPath, format, quality, save
     }
 
   } catch (error) {
-    console.error('Format conversion failed:', error)
+    logger.error('Format conversion failed:', error.message)
     throw new Error(`Format conversion failed: ${error.message}`)
   }
 }
@@ -1663,12 +1664,12 @@ function notifyDownloadComplete(filename, success, errorMessage = null) {
       // Fallback to node-notifier
       notifier.notify(notificationOptions, (err) => {
         if (err) {
-          console.error('Notification error:', err)
+          logger.error('Notification error:', err)
         }
       })
     }
   } catch (error) {
-    console.error('Failed to send notification:', error)
+    logger.error('Failed to send notification:', error.message)
   }
 }
 

+ 9 - 1
tests/error-handling.test.js

@@ -45,6 +45,14 @@ describe('Error Handling System', () => {
     // Mock APIs
     window.electronAPI = mockElectronAPI
 
+    // Mock logger for error-handler.js
+    window.logger = {
+      error: vi.fn(),
+      warn: vi.fn(),
+      info: vi.fn(),
+      debug: vi.fn()
+    }
+
     // Load the error handler
     const fs = require('fs')
     const path = require('path')
@@ -52,7 +60,7 @@ describe('Error Handling System', () => {
       path.join(__dirname, '../scripts/utils/error-handler.js'),
       'utf8'
     )
-    
+
     // Execute the script in the window context
     const script = new window.Function(errorHandlerScript)
     script.call(window)