import { app, BrowserWindow, Menu, Tray, nativeImage, ipcMain } from 'electron'; import * as path from 'path'; import { ConfigManager } from './services/configManager'; import { SyncManager, SyncConfig } from './services/syncManager'; class RekordboxSyncApp { private mainWindow: BrowserWindow | null = null; private tray: Tray | null = null; private configManager: ConfigManager; private syncManager: SyncManager | null = null; private isQuitting: boolean = false; constructor() { this.configManager = new ConfigManager(); // Set memory limits to prevent crashes process.setMaxListeners(50); // Handle uncaught exceptions to prevent EPIPE popups process.on('uncaughtException', (error) => { console.error('❌ Uncaught Exception:', error); this.handleUncaughtException(error); }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason); this.handleUnhandledRejection(reason, promise); }); // App event handlers app.whenReady().then(() => this.initialize()); app.on('window-all-closed', () => this.handleWindowAllClosed()); app.on('before-quit', () => this.handleBeforeQuit()); app.on('activate', () => this.handleActivate()); // IPC handlers this.setupIpcHandlers(); } /** * Initialize the application */ private async initialize(): Promise { await this.createMainWindow(); this.createTray(); this.setupMenu(); // Auto-start sync if configured const syncConfig = this.configManager.getSyncConfig(); if (syncConfig.autoStart) { this.startSync(); } // Start periodic status updates to keep UI informed this.startPeriodicStatusUpdates(); } /** * Create the main application window */ private async createMainWindow(): Promise { this.mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 1000, minHeight: 700, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), }, icon: path.join(__dirname, '../assets/icon.png'), show: false, }); // Load the main HTML file await this.mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); // Show window when ready this.mainWindow.once('ready-to-show', () => { this.mainWindow?.show(); }); // Handle window close this.mainWindow.on('close', (event) => { if (!this.isQuitting) { event.preventDefault(); this.mainWindow?.hide(); } }); // Handle window closed this.mainWindow.on('closed', () => { this.mainWindow = null; }); // Add error handling for the renderer process this.mainWindow.webContents.on('crashed', (event, killed) => { console.error('❌ Renderer process crashed:', { event, killed }); this.handleRendererCrash(); }); this.mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => { console.error('❌ Renderer failed to load:', { errorCode, errorDescription }); this.handleRendererFailure(); }); } /** * Create system tray icon */ private createTray(): void { const iconPath = path.join(__dirname, '../assets/icon.png'); const icon = nativeImage.createFromPath(iconPath); this.tray = new Tray(icon); this.tray.setToolTip('Rekordbox Sync'); this.tray.on('click', () => { if (this.mainWindow?.isVisible()) { this.mainWindow.hide(); } else { this.mainWindow?.show(); this.mainWindow?.focus(); } }); this.tray.on('right-click', () => { this.showTrayContextMenu(); }); } /** * Show tray context menu */ private showTrayContextMenu(): void { if (!this.tray) return; const contextMenu = Menu.buildFromTemplate([ { label: 'Show App', click: () => { this.mainWindow?.show(); this.mainWindow?.focus(); }, }, { label: 'Start Sync', click: () => this.startSync(), enabled: !this.syncManager?.getState().isRunning, }, { label: 'Stop Sync', click: () => this.stopSync(), enabled: this.syncManager?.getState().isRunning || false, }, { type: 'separator' }, { label: 'Quit', click: () => { this.isQuitting = true; app.quit(); }, }, ]); this.tray.popUpContextMenu(contextMenu); } /** * Setup application menu */ private setupMenu(): void { const template: Electron.MenuItemConstructorOptions[] = [ { label: 'File', submenu: [ { label: 'Settings', accelerator: 'CmdOrCtrl+,', click: () => this.showSettings(), }, { type: 'separator' }, { label: 'Quit', accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', click: () => { this.isQuitting = true; app.quit(); }, }, ], }, { label: 'Sync', submenu: [ { label: 'Start Sync', click: () => this.startSync(), enabled: !this.syncManager?.getState().isRunning, }, { label: 'Stop Sync', click: () => this.stopSync(), enabled: this.syncManager?.getState().isRunning || false, }, { type: 'separator' }, { label: 'Force Full Sync', click: () => this.forceFullSync(), }, ], }, { label: 'View', submenu: [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, ], }, ]; const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); } /** * Setup IPC handlers */ private setupIpcHandlers(): void { console.log('🔌 Setting up IPC handlers...'); // Sync control ipcMain.handle('sync:start', () => { return this.startSync(); }); ipcMain.handle('sync:stop', () => { return this.stopSync(); }); ipcMain.handle('sync:force-full', () => { return this.forceFullSync(); }); ipcMain.handle('sync:trigger-immediate', () => { return this.triggerImmediateSync(); }); ipcMain.handle('sync:get-status', () => { return this.syncManager?.getState() || null; }); // Configuration ipcMain.handle('config:get', () => this.configManager.getConfig()); ipcMain.handle('config:update-s3', (event, config) => this.configManager.updateS3Config(config)); ipcMain.handle('config:update-sync', (event, config) => this.configManager.updateSyncConfig(config)); ipcMain.handle('config:update-ui', (event, config) => this.configManager.updateUIConfig(config)); ipcMain.handle('config:test-s3', () => this.configManager.testS3Connection()); ipcMain.handle('config:export-env', async (event, exportPath) => { try { await this.configManager.exportToEnv(exportPath); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } }); // Window controls ipcMain.handle('window:minimize', () => { if (this.mainWindow) { this.mainWindow.minimize(); } }); ipcMain.handle('window:close', () => { if (this.mainWindow) { this.mainWindow.close(); } }); // File operations ipcMain.handle('dialog:select-folder', async () => { // This would need to be implemented with electron dialog return null; }); } /** * Start sync */ private async startSync(): Promise { try { // Force reload .env file to ensure latest changes are picked up this.configManager.reloadEnvironmentVariables(); // Always get fresh config and recreate service to ensure .env changes are picked up const appConfig = this.configManager.getConfig(); const syncConfig: SyncConfig = { s3: { endpoint: appConfig.s3.endpoint, region: appConfig.s3.region, accessKeyId: appConfig.s3.accessKeyId, secretAccessKey: appConfig.s3.secretAccessKey, bucketName: appConfig.s3.bucketName, useSSL: appConfig.s3.useSSL }, sync: { localPath: appConfig.sync.localPath, interval: appConfig.sync.syncInterval, autoStart: appConfig.sync.autoStart, conflictResolution: appConfig.sync.conflictResolution } }; // Recreate sync manager to ensure new config is used if (this.syncManager) { this.syncManager.destroy(); } this.syncManager = new SyncManager(syncConfig); this.setupSyncManagerEventHandlers(); await this.syncManager.startSync(); } catch (error) { console.error('❌ Failed to start sync:', error); throw error; } } /** * Stop sync */ private stopSync(): void { try { if (this.syncManager) { this.syncManager.stopSync(); } } catch (error) { console.error('❌ Failed to stop sync:', error); } } /** * Trigger an immediate sync to propagate local changes (including deletions) */ private async triggerImmediateSync(): Promise { try { if (this.syncManager) { await this.syncManager.triggerImmediateSync(); } } catch (error) { console.error('❌ Immediate sync failed:', error); } } /** * Force full sync */ private async forceFullSync(): Promise { try { if (this.syncManager) { await this.syncManager.forceFullSync(); console.log('✅ Force full sync completed'); } } catch (error) { console.error('❌ Force full sync failed:', error); } } /** * Setup sync manager event handlers */ private setupSyncManagerEventHandlers(): void { if (!this.syncManager) return; // Helper function to safely send messages const safeSend = (channel: string, data: any) => { try { if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.mainWindow.webContents.send(channel, data); } } catch (error) { console.warn(`âš ī¸ Failed to send message to renderer (${channel}):`, error); } }; // Sync manager events this.syncManager.on('stateChanged', (state: any) => { console.log('📊 Sync state changed:', state); // Ensure the state is properly formatted for the UI const uiState = { ...state, // Add actual file counts from the file system actualFileCount: this.getActualLocalFileCount(), lastUpdate: new Date().toISOString() }; console.log('📤 Sending to renderer:', uiState); safeSend('sync-status-changed', uiState); this.updateTrayTooltip(uiState); }); // Add more detailed sync events this.syncManager.on('fileChanged', (event: any) => { safeSend('file-changed', { path: event.path || event, type: event.type, timestamp: new Date().toISOString() }); }); this.syncManager.on('fileAdded', (filePath: string) => { safeSend('file-added', { path: filePath, timestamp: new Date().toISOString() }); }); this.syncManager.on('fileRemoved', (filePath: string) => { safeSend('file-removed', { path: filePath, timestamp: new Date().toISOString() }); // Trigger immediate sync to propagate deletion to S3 this.triggerImmediateSync(); }); this.syncManager.on('syncError', (error: any) => { console.log('đŸ’Ĩ Sync error:', error); safeSend('sync-error', error); }); this.syncManager.on('syncStarted', () => { console.log('🚀 Sync started'); safeSend('sync-started', {}); }); this.syncManager.on('syncStopped', () => { console.log('âšī¸ Sync stopped'); safeSend('sync-stopped', {}); }); this.syncManager.on('forceSyncCompleted', () => { console.log('🔄 Force sync completed'); safeSend('force-sync-completed', {}); }); this.syncManager.on('awsOutput', (output: any) => { console.log('🔍 AWS S3 output:', output); safeSend('aws-output', output); }); } /** * Update tray tooltip with sync status */ private updateTrayTooltip(status: any): void { if (!this.tray) return; const statusText = status.isRunning ? 'Running' : 'Stopped'; const phaseText = status.currentPhase ? ` - ${status.currentPhase}` : ''; this.tray.setToolTip(`Rekordbox Sync - ${statusText}${phaseText}`); } /** * Show settings dialog */ private showSettings(): void { if (this.mainWindow) { this.mainWindow.webContents.send('show-settings'); } } /** * Handle window all closed event */ private handleWindowAllClosed(): void { if (process.platform !== 'darwin') { app.quit(); } } /** * Handle before quit event */ private handleBeforeQuit(): void { this.isQuitting = true; this.stopSync(); } /** * Handle activate event (macOS) */ private handleActivate(): void { if (BrowserWindow.getAllWindows().length === 0) { this.createMainWindow(); } else { this.mainWindow?.show(); } } /** * Handle renderer process crash */ private handleRendererCrash(): void { console.log('🔄 Attempting to recover from renderer crash...'); // Stop sync to prevent further issues if (this.syncManager?.getState().isRunning) { console.log('âšī¸ Stopping sync due to renderer crash'); this.stopSync(); } // Try to reload the renderer setTimeout(() => { try { if (this.mainWindow && !this.mainWindow.isDestroyed()) { console.log('🔄 Reloading renderer...'); this.mainWindow.reload(); } } catch (error) { console.error('❌ Failed to reload renderer:', error); } }, 2000); } /** * Handle renderer process failure */ private handleRendererFailure(): void { console.log('🔄 Attempting to recover from renderer failure...'); // Try to reload the renderer setTimeout(() => { try { if (this.mainWindow && !this.mainWindow.isDestroyed()) { console.log('🔄 Reloading renderer...'); this.mainWindow.reload(); } } catch (error) { console.error('❌ Failed to reload renderer:', error); } }, 1000); } /** * Handle uncaught exceptions to prevent EPIPE popups */ private handleUncaughtException(error: Error): void { console.error('🚨 Handling uncaught exception:', error); // Don't show error popup for EPIPE errors if (error.message.includes('EPIPE') || error.message.includes('write EPIPE')) { console.log('🔇 Suppressing EPIPE error popup'); return; } // For other errors, try to recover gracefully console.log('🔄 Attempting to recover from uncaught exception...'); // Stop sync to prevent further issues if (this.syncManager?.getState().isRunning) { console.log('âšī¸ Stopping sync due to uncaught exception'); this.stopSync(); } } /** * Start periodic status updates to keep UI informed */ private startPeriodicStatusUpdates(): void { // Send status updates every 2 seconds to keep UI responsive setInterval(() => { if (this.syncManager) { const status = this.syncManager.getState(); // Add actual file counts to the status if (status.stats) { try { const fs = require('fs'); const path = require('path'); let localFileCount = 0; const countFiles = (dirPath: string) => { try { const items = fs.readdirSync(dirPath); for (const item of items) { const fullPath = path.join(dirPath, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { countFiles(fullPath); } else if (stat.isFile()) { const ext = path.extname(item).toLowerCase(); if (['.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.wma', '.aiff', '.alac', '.opus'].includes(ext)) { localFileCount++; } } } } catch (error) { // Ignore errors for individual files } }; const syncPath = this.configManager.getSyncConfig().localPath; if (fs.existsSync(syncPath)) { countFiles(syncPath); } // Update the stats with actual file count status.stats.totalFilesSynced = localFileCount; status.stats.filesDownloaded = localFileCount; } catch (error) { console.warn('âš ī¸ Error updating file count:', error); } } this.safeSendToRenderer('sync-status-changed', status); } }, 2000); } /** * Get actual file count from local sync folder */ private getActualLocalFileCount(): number { try { const fs = require('fs'); const path = require('path'); let fileCount = 0; const syncPath = this.configManager.getSyncConfig().localPath; if (!fs.existsSync(syncPath)) { return 0; } const countFiles = (dirPath: string) => { try { const items = fs.readdirSync(dirPath); for (const item of items) { const fullPath = path.join(dirPath, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { countFiles(fullPath); } else if (stat.isFile()) { const ext = path.extname(item).toLowerCase(); if (['.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.wma', '.aiff', '.alac', '.opus'].includes(ext)) { fileCount++; } } } } catch (error) { console.warn(`âš ī¸ Error counting files in ${dirPath}:`, error); } }; countFiles(syncPath); return fileCount; } catch (error) { console.error('❌ Error getting actual file count:', error); return 0; } } /** * Safe send to renderer with better error handling */ private safeSendToRenderer(channel: string, data: any): void { try { if (this.mainWindow && !this.mainWindow.isDestroyed()) { console.log(`📤 Periodic update to renderer (${channel}):`, data); this.mainWindow.webContents.send(channel, data); } } catch (error) { console.warn(`âš ī¸ Failed to send periodic update to renderer (${channel}):`, error); } } /** * Handle unhandled promise rejections */ private handleUnhandledRejection(reason: any, promise: Promise): void { console.error('🚨 Handling unhandled rejection:', { reason, promise }); // Don't show error popup for EPIPE-related rejections if (reason && reason.message && reason.message.includes('EPIPE')) { console.log('🔇 Suppressing EPIPE rejection popup'); return; } // For other rejections, log and continue console.log('🔄 Continuing after unhandled rejection...'); } } // Create and run the application new RekordboxSyncApp();