const { app, BrowserWindow, dialog, ipcMain } = require('electron'); const path = require('path'); const { scanDirectory, extractFileMetadata } = require('./utils/fileUtils'); const dotenv = require('dotenv'); // Load environment variables from .env file dotenv.config(); // Handle creating/removing shortcuts on Windows when installing/uninstalling. try { if (require('electron-squirrel-startup')) { app.quit(); } } catch (error) { // electron-squirrel-startup not available on non-Windows platforms console.log('electron-squirrel-startup not available (expected on non-Windows platforms)'); } let mainWindow; // Create the browser window. function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: true, contextIsolation: false, enableRemoteModule: true, }, }); // and load the index.html of the app. mainWindow.loadFile('index.html'); // Open the DevTools. // mainWindow.webContents.openDevTools(); } // IPC handler for selecting directory ipcMain.handle('select-directory', async () => { try { const result = await dialog.showOpenDialog({ properties: ['openDirectory'] }); if (result.canceled) { return { success: false, error: 'No directory selected' }; } return { success: true, directory: result.filePaths[0] }; } catch (error) { return { success: false, error: error.message }; } }); // IPC handler for scanning directory ipcMain.handle('scan-directory', async (event, directoryPath) => { try { if (!directoryPath) { return { success: false, error: 'No directory path provided' }; } const files = await scanDirectory(directoryPath); return { success: true, files: files }; } catch (error) { return { success: false, error: error.message }; } }); // Enhanced logging for debugging problematic files ipcMain.handle('log-file-info', async (event, filePath) => { try { const fs = require('fs'); const stats = fs.statSync(filePath); return { success: true, fileInfo: { path: filePath, size: stats.size, modified: stats.mtime, isFile: stats.isFile(), isDirectory: stats.isDirectory() } }; } catch (error) { return { success: false, error: error.message }; } }); // IPC handler for renaming file ipcMain.handle('rename-file', async (event, { oldPath, newName }) => { try { const fs = require('fs'); const newPath = path.join(path.dirname(oldPath), newName); // Check if file exists if (!fs.existsSync(oldPath)) { return { success: false, error: 'Original file does not exist' }; } // Check if file is writable try { fs.accessSync(oldPath, fs.constants.W_OK); } catch (err) { return { success: false, error: 'File is not writable' }; } // Rename the file fs.renameSync(oldPath, newPath); return { success: true, message: 'File renamed successfully' }; } catch (error) { return { success: false, error: error.message }; } }); // IPC handler for moving file to folder ipcMain.handle('move-file-to-folder', async (event, { filePath, folderName }) => { try { console.log('Move file request received:', { filePath, folderName }); const fs = require('fs'); const path = require('path'); // Check if file exists if (!fs.existsSync(filePath)) { const error = 'File does not exist'; console.error(error); return { success: false, error: error }; } // Validate folder name if (folderName !== 'extra' && folderName !== 'commentary') { const error = 'Invalid folder name. Must be "extra" or "commentary"'; console.error(error); return { success: false, error: error }; } // Get directory containing the file const fileDir = path.dirname(filePath); const targetFolder = path.join(fileDir, folderName); console.log('Moving file from:', filePath); console.log('Target folder:', targetFolder); // Create the target folder if it doesn't exist if (!fs.existsSync(targetFolder)) { console.log('Creating target folder:', targetFolder); fs.mkdirSync(targetFolder, { recursive: true }); } // Generate new file path const fileName = path.basename(filePath); const newFilePath = path.join(targetFolder, fileName); console.log('New file path:', newFilePath); // Check if file with same name already exists in target folder if (fs.existsSync(newFilePath)) { const error = 'File with same name already exists in target folder'; console.error(error); return { success: false, error: error }; } // Move the file console.log('Moving file...'); fs.renameSync(filePath, newFilePath); console.log('File moved successfully to:', newFilePath); return { success: true, message: `File moved to ${folderName} folder successfully` }; } catch (error) { console.error('Error in move-file-to-folder:', error); return { success: false, error: error.message }; } }); // IPC handler for searching TVDB ipcMain.handle('search-tvdb', async (event, query) => { try { // Import required modules const axios = require('axios'); // Get API key from environment const apiKey = process.env.TVDB_API_KEY || process.env.API_KEY; if (!apiKey) { throw new Error('TVDB_API_KEY not found in environment variables'); } // For TVDB v4 API, we need to first get an authentication token // The token is valid for 1 month let token = null; // Try to get token from cache or login try { const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', { apikey: apiKey }, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); token = loginResponse.data.data.token; } catch (loginError) { console.error('TVDB Login Error:', loginError.message); throw new Error('Failed to authenticate with TVDB API: ' + loginError.message); } // Make API request to TheTVDB v4 search endpoint const response = await axios.get('https://api4.thetvdb.com/v4/search', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }, params: { query: query } }); // Process the results - v4 API returns different structure const results = response.data.data.map(show => ({ id: show.id, seriesName: show.name, status: show.status?.name, firstAired: show.firstAired, overview: show.overview, image: show.image, slug: show.slug })); return { success: true, results: results }; } catch (error) { console.error('TVDB API Error:', error.message); console.error('Error status:', error.response?.status); console.error('Error data:', error.response?.data); // No more mock data - propagate the actual error return { success: false, error: 'Failed to fetch data from TVDB API: ' + error.message }; } }); // IPC handler for getting show details including seasons ipcMain.handle('get-show-details', async (event, showId) => { try { // Import required modules const axios = require('axios'); // Get API key from environment const apiKey = process.env.TVDB_API_KEY || process.env.API_KEY; if (!apiKey) { throw new Error('TVDB_API_KEY not found in environment variables'); } // For TVDB v4 API, we need to first get an authentication token let token = null; // Try to get token from cache or login try { const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', { apikey: apiKey }, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); token = loginResponse.data.data.token; } catch (loginError) { console.error('TVDB Login Error:', loginError.message); throw new Error('Failed to authenticate with TVDB API: ' + loginError.message); } // Extract numeric ID from the show ID (handle both formats: series-73011 and 73011) let numericId = showId; if (showId && showId.startsWith('series-')) { numericId = showId.split('series-')[1]; } // Get extended show details which includes season information const response = await axios.get(`https://api4.thetvdb.com/v4/series/${numericId}/extended`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }); if (response.data && response.data.data) { const showData = response.data.data; // Extract season information properly let seasons = []; if (showData.seasons && Array.isArray(showData.seasons)) { seasons = showData.seasons.map(season => ({ id: season.id, number: season.number, type: season.type?.name || 'Unknown', episodeCount: season.episodeCount || (season.episodes ? season.episodes.length : 0) })); } return { success: true, data: { id: showData.id, name: showData.name, status: showData.status?.name || showData.status, firstAired: showData.firstAired, overview: showData.overview, image: showData.image, slug: showData.slug, seasons: seasons } }; } else { throw new Error('Invalid response structure from TVDB API'); } } catch (error) { console.error('TVDB API Error getting show details:', error.message); console.error('Error status:', error.response?.status); console.error('Error data:', error.response?.data); // Return the error without trying fallbacks that cause scope issues return { success: false, error: 'Failed to fetch show details from TVDB API: ' + error.message }; } }); // IPC handler for getting season episodes ipcMain.handle('get-season-episodes', async (event, showId, seasonNumber) => { try { // Import required modules const axios = require('axios'); // Get API key from environment const apiKey = process.env.TVDB_API_KEY || process.env.API_KEY; if (!apiKey) { throw new Error('TVDB_API_KEY not found in environment variables'); } // For TVDB v4 API, we need to first get an authentication token let token = null; // Try to get token from cache or login try { const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', { apikey: apiKey }, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); token = loginResponse.data.data.token; } catch (loginError) { console.error('TVDB Login Error:', loginError.message); throw new Error('Failed to authenticate with TVDB API: ' + loginError.message); } // Extract numeric ID from the show ID (handle both formats: series-73011 and 73011) let numericId = showId; if (showId && showId.startsWith('series-')) { numericId = showId.split('series-')[1]; } // Try to get episodes for a specific season using the correct endpoint structure // Based on the API documentation, we should use: /series/{id}/episodes/{season-type} // where season-type can be "default", "aired", "dvd", etc. try { // First try the season-specific endpoint const episodesResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${numericId}/episodes/default`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }, params: { page: 0 } }); if (episodesResponse.data && episodesResponse.data.data) { // Process episodes and filter by season number const episodes = episodesResponse.data.data.episodes ? episodesResponse.data.data.episodes .filter(episode => episode.seasonNumber === seasonNumber) .map(episode => ({ id: episode.id, name: episode.name, number: episode.number, seasonNumber: episode.seasonNumber, aired: episode.aired, overview: episode.overview, runtime: episode.runtime })) : []; return { success: true, data: { showId: showId, seasonNumber: seasonNumber, episodes: episodes } }; } } catch (seasonError) { console.log('Season-specific endpoint failed, falling back to all episodes:', seasonError.message); // If season-specific endpoint fails, fall back to getting all episodes and filtering const episodesResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${numericId}/episodes`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }, params: { page: 0 } }); if (episodesResponse.data && episodesResponse.data.data) { // Process episodes and filter by season number const episodes = episodesResponse.data.data.episodes ? episodesResponse.data.data.episodes .filter(episode => episode.seasonNumber === seasonNumber) .map(episode => ({ id: episode.id, name: episode.name, number: episode.number, seasonNumber: episode.seasonNumber, aired: episode.aired, overview: episode.overview, runtime: episode.runtime })) : []; return { success: true, data: { showId: showId, seasonNumber: seasonNumber, episodes: episodes } }; } } throw new Error('Failed to fetch episodes from both endpoints'); } catch (error) { console.error('TVDB API Error getting season episodes:', error.message); console.error('Error status:', error.response?.status); console.error('Error data:', error.response?.data); // Return a more specific error message for debugging return { success: false, error: 'Failed to fetch season episodes from TVDB API: ' + error.message, debug: { showId: showId, seasonNumber: seasonNumber, status: error.response?.status, data: error.response?.data } }; } }); // Test function to verify API connectivity ipcMain.handle('test-tvdb-api', async () => { try { // Test actual API connectivity const axios = require('axios'); const apiKey = process.env.TVDB_API_KEY || process.env.API_KEY; if (!apiKey) { return { success: false, error: 'TVDB_API_KEY not found in environment variables' }; } // Test login endpoint const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', { apikey: apiKey }, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); if (loginResponse.data && loginResponse.data.data && loginResponse.data.data.token) { return { success: true, message: 'TVDB API test successful - Token obtained', timestamp: new Date().toISOString() }; } else { return { success: false, error: 'TVDB API test failed - No token received' }; } } catch (error) { return { success: false, error: 'TVDB API test failed: ' + error.message }; } }); // IPC handler for moving file to extras folder ipcMain.handle('move-to-extras', async (event, filePath) => { try { const fs = require('fs'); const path = require('path'); // Check if file exists if (!fs.existsSync(filePath)) { return { success: false, error: 'File does not exist' }; } // Get the directory of the file const fileDir = path.dirname(filePath); const extrasDir = path.join(fileDir, 'extras'); // Create extras directory if it doesn't exist if (!fs.existsSync(extrasDir)) { fs.mkdirSync(extrasDir, { recursive: true }); } // Get the filename const fileName = path.basename(filePath); // Construct the destination path const destPath = path.join(extrasDir, fileName); // Move the file fs.renameSync(filePath, destPath); return { success: true, message: 'File moved to extras folder successfully', sourcePath: filePath, destinationPath: destPath }; } catch (error) { console.error('Error moving file to extras:', error); return { success: false, error: error.message }; } }); // IPC handler for opening file in default player ipcMain.handle('open-file-in-player', async (event, filePath) => { try { const { exec } = require('child_process'); // Open file in default player based on OS let command; if (process.platform === 'darwin') { // macOS command = `open "${filePath}"`; } else if (process.platform === 'win32') { // Windows command = `start "" "${filePath}"`; } else { // Linux command = `xdg-open "${filePath}"`; } exec(command, (error, stdout, stderr) => { if (error) { console.error('Error opening file:', error); return { success: false, error: error.message }; } console.log('File opened successfully:', filePath); }); return { success: true, message: 'File opened in default player' }; } catch (error) { console.error('Error opening file in player:', error); return { success: false, error: error.message }; } }); // This method will be called when Electron has finished // initialization and is ready to create browser windows. app.whenReady().then(createWindow); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });