MovieMapper/main.js
2026-02-19 09:47:06 -06:00

517 lines
16 KiB
JavaScript

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 new name is different
if (oldPath === newPath) {
return { success: true, message: 'File name unchanged' };
}
// 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 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 };
}
});
// 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();
}
});