const { ipcRenderer } = require('electron'); const path = require('path'); // DOM Elements const selectDirBtn = document.getElementById('select-dir-btn'); const selectedDirEl = document.getElementById('selected-dir'); const searchInput = document.getElementById('search-input'); const searchResultsEl = document.getElementById('search-results'); const fileListEl = document.getElementById('file-list'); const showDetailsEl = document.getElementById('show-details'); // Current state let currentDirectory = null; let currentFiles = []; let currentShow = null; let currentSeasons = []; let currentEpisodes = []; // Event Listeners selectDirBtn.addEventListener('click', selectDirectory); searchInput.addEventListener('input', debounce(searchShows, 300)); document.getElementById('test-api-btn').addEventListener('click', testTVDBAPI); // Select directory function async function selectDirectory() { try { // Use IPC to handle directory selection const result = await ipcRenderer.invoke('select-directory'); if (result.success) { const directory = result.directory; currentDirectory = directory; selectedDirEl.textContent = `Selected: ${directory}`; // Scan the directory for media files await scanDirectory(directory); } else { throw new Error(result.error); } } catch (error) { console.error('Error selecting directory:', error); alert(`Error: ${error.message}`); } } // Scan directory for media files async function scanDirectory(directoryPath) { try { const result = await ipcRenderer.invoke('scan-directory', directoryPath); if (result.success) { currentFiles = result.files; displayFiles(currentFiles); } else { throw new Error(result.error); } } catch (error) { console.error('Error scanning directory:', error); alert(`Error scanning directory: ${error.message}`); } } // Display files in the UI 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); }); // Add hover effects for tags const tagIcons = fileItem.querySelectorAll('.tag-icon'); tagIcons.forEach(icon => { icon.addEventListener('mouseenter', function() { this.style.opacity = '1'; this.style.transform = 'scale(1.1)'; }); icon.addEventListener('mouseleave', function() { this.style.opacity = '0.7'; this.style.transform = 'scale(1)'; }); // Add click handlers for tagging icon.addEventListener('click', function(e) { e.stopPropagation(); // Prevent event bubbling const filePath = this.dataset.filePath; const tagType = this.classList.contains('extra-tag') ? 'extra' : 'commentary'; // Check if file is already tagged with this type const fileItem = this.closest('.file-item'); const isTagged = fileItem.hasAttribute('data-tagged-' + tagType); if (isTagged) { // Untag the file untagFile(filePath, tagType); } else { // If tagging this type, untag any existing tag of the other type const otherTagType = tagType === 'extra' ? 'commentary' : 'extra'; if (fileItem.hasAttribute('data-tagged-' + otherTagType)) { untagFile(filePath, otherTagType); } // Tag the file addTagToEpisode(filePath, tagType); } }); }); fileListEl.appendChild(fileItem); }); } // Make a file name editable function makeEditable(element) { // Prevent editing if already in edit mode if (element.contentEditable === 'true') return; // Store original text const originalText = element.textContent; // Make element editable element.contentEditable = 'true'; element.focus(); element.classList.add('editing'); // Select all text when editing starts const range = document.createRange(); range.selectNodeContents(element); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); // Handle saving when user finishes editing const saveEdit = async function() { if (element.textContent.trim() !== originalText.trim()) { // Send request to main process to rename the file try { const result = await ipcRenderer.invoke('rename-file', { oldPath: element.dataset.filePath, newName: element.textContent.trim() }); if (result.success) { console.log('File renamed successfully:', result.message); // Update the file name in the UI to reflect the change element.textContent = element.textContent.trim(); } else { console.error('Failed to rename file:', result.error); // Revert to original name on failure element.textContent = originalText; } } catch (error) { console.error('Error renaming file:', error); // Revert to original name on error element.textContent = originalText; } } // Clean up element.contentEditable = 'false'; element.classList.remove('editing'); }; // Save on Enter key or blur element.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); saveEdit(); } }); element.addEventListener('blur', saveEdit); } // Function to handle tagging function addTagToEpisode(filePath, tagType) { console.log(`Tagging file ${filePath} as ${tagType}`); // Find the file item in the UI const fileItems = document.querySelectorAll('.file-item'); fileItems.forEach(item => { const fileNameElement = item.querySelector('.file-name'); if (fileNameElement && fileNameElement.dataset.filePath === filePath) { // Add visual indication of tagging using data attribute for CSS styling const tagIcon = item.querySelector(`.${tagType}-tag`); if (tagIcon) { // Make the icon fully saturated and highlight in bright yellow with very strong visual effect tagIcon.style.opacity = '1'; tagIcon.style.filter = 'none'; tagIcon.style.color = tagType === 'extra' ? '#FFD700' : '#17a2b8'; // Brighter yellow or teal color tagIcon.style.textShadow = '0 0 15px rgba(255, 215, 0, 1)'; // Very bright yellow glow (for extra tags) tagIcon.style.transform = 'scale(1.3)'; // More pronounced enlargement tagIcon.style.boxShadow = '0 0 10px rgba(255, 215, 0, 0.8)'; // Additional glow effect // Add a data attribute to track that this file is tagged item.setAttribute('data-tagged-' + tagType, 'true'); } } }); // In a real implementation, this would save to a database or file // For now, we'll just log to console console.log(`File ${filePath} tagged as ${tagType} - would be saved in real implementation`); } // Function to handle untagging function untagFile(filePath, tagType) { console.log(`Untagging file ${filePath} from ${tagType}`); // Find the file item in the UI const fileItems = document.querySelectorAll('.file-item'); fileItems.forEach(item => { const fileNameElement = item.querySelector('.file-name'); if (fileNameElement && fileNameElement.dataset.filePath === filePath) { // Remove visual indication of tagging const tagIcon = item.querySelector(`.${tagType}-tag`); if (tagIcon) { // Reset to original appearance tagIcon.style.opacity = '0.7'; tagIcon.style.filter = 'none'; tagIcon.style.color = ''; // Reset to default color tagIcon.style.textShadow = 'none'; tagIcon.style.transform = 'scale(1)'; tagIcon.style.boxShadow = 'none'; // Remove the data attribute item.removeAttribute('data-tagged-' + tagType); } } }); // In a real implementation, this would remove from database or file // For now, we'll just log to console console.log(`File ${filePath} untagged from ${tagType} - would be removed in real implementation`); } // Search shows function async function searchShows() { const query = searchInput.value.trim(); if (query.length < 2) { searchResultsEl.innerHTML = ''; return; } try { const result = await ipcRenderer.invoke('search-tvdb', query); if (result.success) { displaySearchResults(result.results); } else { throw new Error(result.error); } } catch (error) { console.error('Error searching shows:', error); searchResultsEl.innerHTML = `

Error: ${error.message}

`; } } // Clear search results when a show is selected function clearSearchResults() { searchResultsEl.innerHTML = ''; } // 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); alert('TVDB API Test Successful!\n' + result.message); } else { console.error('TVDB API test failed:', result.error); alert('TVDB API Test Failed!\n' + result.error); } } catch (error) { console.error('Error testing TVDB API:', error); alert('Error testing TVDB API: ' + error.message); } } // Add a test button to the UI (you can add this to the HTML or call it manually) // For now, we'll just make it available in the console console.log('TVDB API test function available: testTVDBAPI()'); // 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 = ''; if (results.length === 0) { searchResultsEl.innerHTML = '

No shows found.

'; return; } results.forEach(show => { const resultItem = document.createElement('div'); resultItem.className = 'search-result-item'; // Create a more detailed display let resultText = show.seriesName || show.name; if (show.firstAired) { resultText += ` (${show.firstAired})`; } resultItem.textContent = resultText; resultItem.addEventListener('click', () => selectShow(show)); searchResultsEl.appendChild(resultItem); }); } // Select a show and display more details async function selectShow(show) { console.log('Selected show:', show); currentShow = show; // Clear search results when a show is selected clearSearchResults(); // Display show title document.getElementById('show-title').textContent = show.seriesName || show.name; // Fetch show details including seasons try { const result = await ipcRenderer.invoke('get-show-details', show.id); if (result.success && result.data) { const showData = result.data; currentSeasons = showData.seasons || []; // Display seasons - since we're not getting seasons from the API directly, // we'll show a message indicating we need to fetch seasons separately if (currentSeasons.length === 0) { document.getElementById('seasons-container').innerHTML = '

Seasons will be loaded when you click on a season.

'; } else { displaySeasons(currentSeasons); } } else { console.error('Failed to fetch show details:', result.error); document.getElementById('seasons-container').innerHTML = '

Error loading seasons: ' + (result.error || 'Unknown error') + '

'; } } catch (error) { console.error('Error fetching show details:', error); document.getElementById('seasons-container').innerHTML = '

Error loading seasons: ' + error.message + '

'; } } // Display seasons with episode counts function displaySeasons(seasons) { const seasonsContainer = document.getElementById('seasons-container'); seasonsContainer.innerHTML = ''; if (!seasons || seasons.length === 0) { seasonsContainer.innerHTML = '

No seasons available.

'; return; } // Create a container for seasons const seasonsList = document.createElement('div'); seasonsList.style.marginTop = '10px'; seasons.forEach(season => { const seasonItem = document.createElement('div'); seasonItem.className = 'season-item'; seasonItem.style.padding = '12px'; seasonItem.style.margin = '8px 0'; seasonItem.style.border = '1px solid #ddd'; seasonItem.style.borderRadius = '6px'; seasonItem.style.cursor = 'pointer'; seasonItem.style.backgroundColor = '#f8f9fa'; seasonItem.style.display = 'flex'; seasonItem.style.justifyContent = 'space-between'; seasonItem.style.alignItems = 'center'; // Format season display with episode count let seasonDisplay = `Season ${season.number}`; if (season.type && season.type !== 'Unknown') { seasonDisplay += ` (${season.type})`; } const episodeCount = season.episodeCount || 0; seasonItem.innerHTML = ` ${seasonDisplay} ${episodeCount} episodes `; // Add click event to fetch episodes seasonItem.addEventListener('click', () => { fetchAndDisplayEpisodes(currentShow.id, season.number); }); seasonsList.appendChild(seasonItem); }); seasonsContainer.appendChild(seasonsList); } // Fetch and display episodes for a season async function fetchAndDisplayEpisodes(showId, seasonNumber) { try { // Show loading message while fetching document.getElementById('seasons-container').innerHTML = '

Loading episodes...

'; const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber); if (result.success && result.data) { const episodes = result.data.episodes || []; displayEpisodes(episodes); } else { console.error('Failed to fetch episodes:', result.error); document.getElementById('seasons-container').innerHTML = '

Error loading episodes: ' + (result.error || 'Unknown error') + '

'; } } catch (error) { console.error('Error fetching episodes:', error); // Show a more user-friendly error message document.getElementById('seasons-container').innerHTML = `

Error loading episodes: ${error.message || 'Failed to load episodes'}

Note: This may be due to API limitations with the TVDB v4 API.

`; } } // Display episodes in clean table format function displayEpisodes(episodes) { const seasonsContainer = document.getElementById('seasons-container'); seasonsContainer.innerHTML = ''; if (!episodes || episodes.length === 0) { seasonsContainer.innerHTML = '

No episodes available.

'; return; } // Create a container for episodes table const episodesTable = document.createElement('div'); episodesTable.style.marginTop = '10px'; episodesTable.style.border = '1px solid #ddd'; episodesTable.style.borderRadius = '6px'; episodesTable.style.overflow = 'hidden'; // Create table header const tableHeader = document.createElement('div'); tableHeader.style.display = 'grid'; tableHeader.style.gridTemplateColumns = '1fr 1fr'; tableHeader.style.backgroundColor = '#f8f9fa'; tableHeader.style.padding = '10px'; tableHeader.style.fontWeight = 'bold'; tableHeader.style.borderBottom = '1px solid #ddd'; tableHeader.innerHTML = `
Episode Name
Runtime
`; episodesTable.appendChild(tableHeader); episodes.forEach(episode => { const episodeRow = document.createElement('div'); episodeRow.className = 'episode-item'; episodeRow.style.display = 'grid'; episodeRow.style.gridTemplateColumns = '1fr 1fr'; episodeRow.style.padding = '10px'; episodeRow.style.borderBottom = '1px solid #eee'; episodeRow.style.backgroundColor = '#fff'; episodeRow.style.cursor = 'pointer'; episodeRow.style.transition = 'background-color 0.2s'; episodeRow.addEventListener('mouseenter', () => { episodeRow.style.backgroundColor = '#f5f5f5'; }); episodeRow.addEventListener('mouseleave', () => { episodeRow.style.backgroundColor = '#fff'; }); // Format episode display const episodeName = episode.name || 'Untitled'; const episodeRuntime = episode.runtime ? `${episode.runtime} min` : 'N/A'; episodeRow.innerHTML = `
Episode ${episode.number}: ${episodeName}
${episodeRuntime}
`; // Add click event (could show more details) episodeRow.addEventListener('click', () => { alert(`Episode: ${episode.name || 'Untitled'}\nRuntime: ${episode.runtime || 'N/A'} min\nAired: ${episode.aired || 'N/A'}`); }); episodesTable.appendChild(episodeRow); }); 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 = `

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); } } } // Debounce function for search input function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Initialize the application console.log('Movie Mapper application initialized');