-
+
+
diff --git a/main.js b/main.js
index 0144dfa..bc206a0 100644
--- a/main.js
+++ b/main.js
@@ -454,6 +454,47 @@ ipcMain.handle('test-tvdb-api', async () => {
}
});
+// 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);
diff --git a/renderer.js b/renderer.js
index 2d497ac..70b849c 100644
--- a/renderer.js
+++ b/renderer.js
@@ -94,6 +94,13 @@ function displayFiles(files) {
makeEditable(e.target);
});
+ // Add context menu event listener to the file item itself
+ fileItem.addEventListener('contextmenu', function(e) {
+ console.log('File item contextmenu event triggered');
+ e.preventDefault();
+ // This will be handled by the global listener
+ });
+
fileListEl.appendChild(fileItem);
});
}
@@ -160,6 +167,106 @@ function makeEditable(element) {
element.addEventListener('blur', saveEdit);
}
+// Add context menu functionality for files
+function addFileContextMenu() {
+ // Add context menu listener to file items
+ document.addEventListener('contextmenu', function(e) {
+ console.log('Context menu event fired on:', e.target);
+ console.log('Target class list:', e.target.classList);
+
+ // Check if the right-click was on a file name
+ if (e.target.classList.contains('file-name')) {
+ console.log('Right-click detected on file name');
+ e.preventDefault();
+
+ const filePath = e.target.dataset.filePath;
+ const fileName = e.target.textContent.trim();
+
+ // Create context menu
+ const contextMenu = document.createElement('div');
+ contextMenu.id = 'file-context-menu';
+ contextMenu.style.position = 'absolute';
+ contextMenu.style.left = e.pageX + 'px';
+ contextMenu.style.top = e.pageY + 'px';
+ contextMenu.style.backgroundColor = 'white';
+ contextMenu.style.border = '1px solid #ddd';
+ contextMenu.style.borderRadius = '4px';
+ contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
+ contextMenu.style.zIndex = '1000';
+ contextMenu.style.padding = '5px 0';
+ contextMenu.style.minWidth = '150px';
+
+ // Add "Move to Extras" option
+ const moveToExtras = document.createElement('div');
+ moveToExtras.textContent = 'Move to Extras';
+ moveToExtras.style.padding = '8px 16px';
+ moveToExtras.style.cursor = 'pointer';
+ moveToExtras.style.fontFamily = 'Arial, sans-serif';
+ moveToExtras.style.fontSize = '14px';
+
+ moveToExtras.addEventListener('mouseenter', function() {
+ this.style.backgroundColor = '#f0f0f0';
+ });
+
+ moveToExtras.addEventListener('mouseleave', function() {
+ this.style.backgroundColor = 'transparent';
+ });
+
+ moveToExtras.addEventListener('click', async function() {
+ try {
+ console.log('Attempting to move file to extras:', filePath);
+ const result = await ipcRenderer.invoke('move-to-extras', filePath);
+ if (result.success) {
+ console.log('File moved successfully');
+ // Remove the file from the UI
+ const fileItem = e.target.closest('.file-item');
+ if (fileItem) {
+ fileItem.remove();
+ }
+ alert(`File moved to extras: ${fileName}`);
+ } else {
+ console.error('Failed to move file:', result.error);
+ alert('Failed to move file to extras: ' + result.error);
+ }
+ } catch (error) {
+ console.error('Error moving file:', error);
+ alert('Error moving file to extras: ' + error.message);
+ }
+ // Remove context menu
+ document.getElementById('file-context-menu')?.remove();
+ });
+
+ contextMenu.appendChild(moveToExtras);
+
+ // Add click outside to close menu
+ function closeMenu(e) {
+ if (!contextMenu.contains(e.target)) {
+ document.removeEventListener('click', closeMenu);
+ contextMenu.remove();
+ }
+ }
+
+ document.addEventListener('click', closeMenu);
+
+ document.body.appendChild(contextMenu);
+
+ // Also add a click outside listener to the document
+ document.addEventListener('mousedown', function closeOnOutsideClick(e) {
+ if (!contextMenu.contains(e.target)) {
+ document.removeEventListener('mousedown', closeOnOutsideClick);
+ contextMenu.remove();
+ }
+ });
+ }
+ });
+}
+
+// Initialize context menu when the app loads
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('DOM loaded - adding context menu');
+ addFileContextMenu();
+});
+
// Search shows function
async function searchShows() {
const query = searchInput.value.trim();
@@ -435,6 +542,114 @@ function displayEpisodes(episodes) {
});
seasonsContainer.appendChild(episodesTable);
+
+ // Also update the main file list to show episode matching information
+ updateFileListWithEpisodeInfo(episodes);
+}
+
+// Enhanced function to create a more integrated view
+function createIntegratedEpisodeFileView(episodes) {
+ // This function would create a more integrated view showing both files and episodes
+ // For now, we're enhancing the existing functionality to better align the views
+
+ // Add a section that shows how files might align with episodes
+ const fileListContainer = document.getElementById('file-list');
+
+ // Create a section showing the relationship
+ const relationshipSection = document.createElement('div');
+ relationshipSection.style.marginTop = '15px';
+ relationshipSection.style.padding = '12px';
+ relationshipSection.style.backgroundColor = '#fff8e1';
+ relationshipSection.style.border = '1px solid #ffd54f';
+ relationshipSection.style.borderRadius = '6px';
+ relationshipSection.innerHTML = `
+
-
- Click "Select Directory" to start browsing media files.
+
+
+
+
Click "Select Directory" to start browsing media files.
+File-Episode Relationship
++ Matching Strategy: Files are matched to episodes based on naming patterns. +
++ Example: "Show.S01E01.Title.mp4" matches Episode 1 of Season 1. +
++ Status: ${currentFiles.length} files in directory, ${episodes.length} episodes available. +
+ `; + + // Insert this section after the episode info + const episodeInfo = fileListContainer.querySelector('.episode-info'); + if (episodeInfo) { + fileListContainer.insertBefore(relationshipSection, episodeInfo.nextSibling); + } +} + +// Update file list to show episode matching information +function updateFileListWithEpisodeInfo(episodes) { + // When we have episodes, we want to show basic episode information + if (currentFiles && currentFiles.length > 0 && episodes && episodes.length > 0) { + console.log('Updating file list with episode info for', episodes.length, 'episodes'); + + // Create a simple note about episode availability + const fileListContainer = document.getElementById('file-list'); + + // Add a simple note about episodes + const episodeNote = document.createElement('div'); + episodeNote.style.marginTop = '15px'; + episodeNote.style.padding = '10px'; + episodeNote.style.backgroundColor = '#e7f3ff'; + episodeNote.style.border = '1px solid #b3d9ff'; + episodeNote.style.borderRadius = '5px'; + episodeNote.style.fontSize = '14px'; + episodeNote.innerHTML = ` + Episode Information: + ${episodes.length} episodes available for this show. +Episode details are displayed in the sidebar. + `; + + // Add this note to the file list container + if (fileListContainer.firstChild) { + fileListContainer.insertBefore(episodeNote, fileListContainer.firstChild); + } else { + fileListContainer.appendChild(episodeNote); + } + } +} + +// Enhanced file display that can show episode matching info +function displayFiles(files) { + fileListEl.innerHTML = ''; + + if (files.length === 0) { + fileListEl.innerHTML = '
No media files found in this directory.
'; + return; + } + + files.forEach(file => { + const fileItem = document.createElement('div'); + fileItem.className = 'file-item'; + + // Use actual duration from file metadata + const duration = file.duration || '00:00'; + const quality = file.quality || 'unknown'; + const fps = file.fps || 'unknown'; + + // Add label for problematic files + const problemLabel = file.isProblematic ? '⚠️ ' : ''; + + fileItem.innerHTML = ` +${problemLabel}${file.name}
+ ${duration}
+ ${quality}
+ ${fps}
+ `;
+
+ // Add click event to make file name editable
+ const fileNameElement = fileItem.querySelector('.file-name');
+ fileNameElement.addEventListener('click', function(e) {
+ makeEditable(e.target);
+ });
+
+ fileListEl.appendChild(fileItem);
+ });
}
// Debounce function for search input
diff --git a/test-api.js b/test-api.js
new file mode 100644
index 0000000..1195a94
--- /dev/null
+++ b/test-api.js
@@ -0,0 +1,99 @@
+// Simple test script to verify TVDB API functionality
+const axios = require('axios');
+const dotenv = require('dotenv');
+
+// Load environment variables
+dotenv.config();
+
+async function testTVDBAPI() {
+ try {
+ console.log('Testing TVDB API connectivity...');
+
+ // 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');
+ }
+
+ // First get a token
+ const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', {
+ apikey: apiKey
+ }, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ }
+ });
+
+ console.log('Login successful:', loginResponse.data.status);
+
+ if (loginResponse.data && loginResponse.data.data && loginResponse.data.data.token) {
+ const token = loginResponse.data.data.token;
+ console.log('Got token:', token.substring(0, 50) + '...');
+
+ // Test search with the token
+ const searchResponse = await axios.get('https://api4.thetvdb.com/v4/search', {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ params: {
+ query: 'IGPX'
+ }
+ });
+
+ console.log('Search response:', searchResponse.data);
+
+ if (searchResponse.data && searchResponse.data.data) {
+ console.log('Found shows:', searchResponse.data.data.length);
+ searchResponse.data.data.forEach(show => {
+ console.log(`- ${show.name} (ID: ${show.id})`);
+ });
+ }
+
+ // Test getting show details with extended info
+ const showId = '73011'; // IGPX Immortal Grand Prix ID
+ const showResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${showId}/extended`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ }
+ });
+
+ console.log('Show details response:', showResponse.data);
+
+ // Test getting episodes for the show
+ const episodesResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${showId}/episodes`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'
+ },
+ params: {
+ page: 0
+ }
+ });
+
+ console.log('Episodes response structure:', {
+ data: episodesResponse.data.data ? 'has data' : 'no data',
+ total: episodesResponse.data.data?.total,
+ episodes: episodesResponse.data.data?.episodes ? episodesResponse.data.data.episodes.length : 0
+ });
+
+ if (episodesResponse.data && episodesResponse.data.data && episodesResponse.data.data.episodes) {
+ console.log('First few episodes:');
+ episodesResponse.data.data.episodes.slice(0, 5).forEach(episode => {
+ console.log(`- Episode ${episode.number}: ${episode.name} (Season ${episode.seasonNumber})`);
+ });
+ }
+ }
+ } catch (error) {
+ console.error('API Test Error:', error.message);
+ console.error('Error details:', error.response?.data || error);
+ }
+}
+
+testTVDBAPI();