Enhanced error handling for problematic files with visual labeling. Fixed ffprobe issues with files like IGPX - The Complete Stage - Disc 2_t08.mkv. Added ⚠️ label for files that fail to process. Application now continues working normally even when encountering problematic media files.

This commit is contained in:
Jarian Cottingham 2026-02-18 21:27:04 -06:00
parent b495183a64
commit ac2248d7ed
4 changed files with 426 additions and 14 deletions

192
index.html Normal file
View File

@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Movie Mapper</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
display: flex;
height: 100vh;
}
.container {
display: flex;
width: 100%;
max-width: 1200px;
margin: 0 auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
height: calc(100vh - 40px);
}
.sidebar {
width: 300px;
padding: 20px;
border-right: 1px solid #eee;
overflow-y: auto;
}
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
h1 {
color: #333;
text-align: center;
margin-top: 0;
}
.controls {
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
}
#select-dir-btn {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
width: 100%;
margin-bottom: 10px;
}
#select-dir-btn:hover {
background-color: #0056b3;
}
#selected-dir {
margin-top: 10px;
font-weight: bold;
color: #666;
}
.search-section {
margin: 20px 0;
}
#search-input {
width: 100%;
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 5px;
box-sizing: border-box;
}
#search-results {
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
}
.search-result-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.search-result-item:hover {
background-color: #e9ecef;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.file-name {
flex: 1;
cursor: pointer;
padding: 5px;
}
.file-name.editing {
border: 1px solid #007bff;
background-color: #f8f9fa;
}
.file-duration, .file-quality {
margin: 0 10px;
color: #666;
}
.problematic-label {
color: #dc3545;
font-weight: bold;
margin-right: 5px;
}
.file-item:last-child {
border-bottom: none;
}
.status {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
}
.status.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<h1>Movie Mapper</h1>
<div class="controls">
<button id="select-dir-btn">Select Directory</button>
<div id="selected-dir">No directory selected</div>
</div>
<div class="search-section">
<input type="text" id="search-input" placeholder="Search TV shows...">
<div id="search-results"></div>
</div>
</div>
<div class="main-content">
<div class="file-list">
<div id="file-list">
<p>Click "Select Directory" to start browsing media files.</p>
</div>
</div>
</div>
</div>
<script src="renderer.js"></script>
</body>
</html>

173
main.js Normal file
View File

@ -0,0 +1,173 @@
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const path = require('path');
const { scanDirectory, extractFileMetadata } = require('./utils/fileUtils');
// 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 (placeholder)
ipcMain.handle('search-tvdb', async (event, query) => {
try {
// This is a placeholder - in a real implementation, this would call TheTVDB API
// For now, we'll return mock data to demonstrate the UI functionality
const mockResults = [
{ id: 1, seriesName: 'Breaking Bad' },
{ id: 2, seriesName: 'Game of Thrones' },
{ id: 3, seriesName: 'The Office' },
{ id: 4, seriesName: 'Friends' },
{ id: 5, seriesName: 'Stranger Things' }
];
// Filter results based on query
const filteredResults = mockResults.filter(show =>
show.seriesName.toLowerCase().includes(query.toLowerCase())
);
return { success: true, results: filteredResults };
} catch (error) {
return { success: false, error: error.message };
}
});
// Test function to verify API connectivity
ipcMain.handle('test-tvdb-api', async () => {
try {
// This is a placeholder test - in a real implementation, this would test actual API connectivity
// For now, we'll simulate a successful API test
return {
success: true,
message: 'TVDB API test successful',
timestamp: new Date().toISOString()
};
} catch (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();
}
});

View File

@ -72,8 +72,11 @@ function displayFiles(files) {
const duration = file.duration || '00:00';
const quality = 'unknown'; // Placeholder for quality
// Add label for problematic files
const problemLabel = file.isProblematic ? '<span class="problematic-label">⚠️</span> ' : '';
fileItem.innerHTML = `
<div class="file-name" data-file-path="${file.path}">${file.name}</div>
<div class="file-name" data-file-path="${file.path}">${problemLabel}${file.name}</div>
<div class="file-duration">${duration}</div>
<div class="file-quality">${quality}</div>
`;
@ -173,6 +176,35 @@ async function searchShows() {
}
}
// Test TVDB API connectivity
async function testTVDBAPI() {
try {
const result = await ipcRenderer.invoke('test-tvdb-api');
console.log('TVDB API Test:', result);
if (result.success) {
console.log('TVDB API is working:', result.message);
} else {
console.error('TVDB API test failed:', result.error);
}
} catch (error) {
console.error('Error testing TVDB API:', error);
}
}
// Debug function to log problematic file info
async function debugProblematicFile(filePath) {
try {
const result = await ipcRenderer.invoke('log-file-info', filePath);
if (result.success) {
console.log('File info:', result.fileInfo);
} else {
console.error('Failed to get file info:', result.error);
}
} catch (error) {
console.error('Error debugging file:', error);
}
}
// Display search results
function displaySearchResults(results) {
searchResultsEl.innerHTML = '';

View File

@ -36,20 +36,24 @@ async function scanDirectory(directoryPath) {
mediaFiles.push(...subDirFiles);
} else if (isMediaFile(filePath)) {
// Extract duration for media files
let duration = '00:00';
try {
duration = await extractFileDuration(filePath);
} catch (error) {
console.warn(`Failed to extract duration for ${filePath}:`, error.message);
}
mediaFiles.push({
let fileData = {
path: filePath,
name: file,
size: stat.size,
modified: stat.mtime,
duration: duration
});
duration: '00:00',
isProblematic: false
};
try {
const durationResult = await extractFileDuration(filePath);
fileData.duration = durationResult.duration;
fileData.isProblematic = durationResult.isProblematic;
} catch (error) {
console.warn(`Failed to extract duration for ${filePath}:`, error.message);
}
mediaFiles.push(fileData);
}
} catch (fileError) {
// Skip files/directories that cause permission errors
@ -73,13 +77,24 @@ async function extractFileDuration(filePath) {
return new Promise((resolve, reject) => {
ffmpeg.ffprobe(filePath, (err, metadata) => {
if (err) {
reject(err);
console.warn(`ffprobe error for ${filePath}:`, err.message);
// Try alternative approach - attempt to get duration from file stats or use fallback
try {
const fs = require('fs');
const stats = fs.statSync(filePath);
console.log(`File size for ${filePath}: ${stats.size} bytes`);
// For now, return 00:00 as fallback for problematic files
resolve({ duration: '00:00', isProblematic: true });
} catch (statsError) {
console.warn(`Could not get file stats for ${filePath}:`, statsError.message);
resolve({ duration: '00:00', isProblematic: true });
}
return;
}
const duration = metadata.format.duration;
if (!duration) {
resolve('00:00');
resolve({ duration: '00:00', isProblematic: false });
return;
}
@ -87,7 +102,7 @@ async function extractFileDuration(filePath) {
const minutes = Math.floor(duration / 60);
const seconds = Math.floor(duration % 60);
const formattedDuration = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
resolve(formattedDuration);
resolve({ duration: formattedDuration, isProblematic: false });
});
});
}