From 92320cc908817c503e61f2f44883e3d835544ce2 Mon Sep 17 00:00:00 2001 From: Jarian Cottingham Date: Sun, 22 Feb 2026 02:33:41 -0600 Subject: [PATCH] Add delete tag, episode count matching indicator, fix season episode counts --- index.html | 88 ++++++++++++- main.js | 36 ++++- renderer.js | 372 +++++++++++++++++++++++++++++++++++----------------- 3 files changed, 373 insertions(+), 123 deletions(-) diff --git a/index.html b/index.html index 546b40a..e95fe91 100644 --- a/index.html +++ b/index.html @@ -126,7 +126,47 @@ #seasons-container { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; + } + + .season-item { + padding: 12px; + background-color: #1a1a2e; + border: 1px solid #16213e; + border-radius: 6px; + color: #fff; + cursor: pointer; + transition: all 0.2s; + } + + .season-item:hover { + background-color: #e94560; + border-color: #e94560; + } + + .season-item.selected { + background-color: #0f3460; + border-color: #e94560; + } + + #episodes-container { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 4px; + } + + .episode-item { + padding: 10px; + background-color: #0f3460; + border-radius: 4px; + color: #ccc; + font-size: 13px; + } + + .episode-item:hover { + background-color: #16213e; + color: #fff; } /* Main Content - Files */ @@ -218,13 +258,53 @@ padding: 12px 15px; background-color: #16213e; border-radius: 8px; - transition: background-color 0.2s; + transition: all 0.2s; + cursor: grab; } .file-item:hover { background-color: #0f3460; } + .file-item.dragging { + opacity: 0.5; + cursor: grabbing; + } + + .file-item.drag-over { + border: 2px dashed #e94560; + background-color: #0f3460; + } + + .file-item.folder-item { + cursor: pointer; + } + + .episode-number { + min-width: 40px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background-color: #e94560; + color: white; + font-weight: 600; + font-size: 12px; + border-radius: 4px; + margin-right: 12px; + } + + .drag-handle { + color: #666; + margin-right: 10px; + cursor: grab; + font-size: 16px; + } + + .drag-handle:hover { + color: #e94560; + } + .file-name { flex: 1; cursor: pointer; @@ -273,6 +353,10 @@ transform: scale(1.2); } + .delete-tag:hover { + color: #dc3545; + } + .play-button { background: none; border: none; diff --git a/main.js b/main.js index 69b2cbc..c7dff73 100644 --- a/main.js +++ b/main.js @@ -213,14 +213,20 @@ ipcMain.handle('move-file-to-folder', async (event, { filePath, folderName }) => // Validate folder name and map to actual folder names // "extra" maps to "extras" for Jellyfin compatibility - if (folderName !== 'extra' && folderName !== 'commentary') { - const error = 'Invalid folder name. Must be "extra" or "commentary"'; + const validFolders = ['extra', 'commentary', 'delete']; + if (!validFolders.includes(folderName)) { + const error = 'Invalid folder name. Must be "extra", "commentary", or "delete"'; writeLog(`ERROR: ${error}`); return { success: false, error: error }; } // Map folder names to actual directory names - const actualFolderName = folderName === 'extra' ? 'extras' : 'commentary'; + let actualFolderName; + if (folderName === 'extra') { + actualFolderName = 'extras'; + } else { + actualFolderName = folderName; // 'commentary' and 'delete' stay as-is + } // Get directory containing the file const fileDir = path.dirname(filePath); @@ -421,6 +427,28 @@ ipcMain.handle('get-show-details', async (event, showId) => { if (response.data && response.data.data) { const showData = response.data.data; + // Fetch episodes to count per season + let episodeCounts = {}; + try { + 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?.data?.episodes) { + episodesResponse.data.data.episodes.forEach(ep => { + const sn = ep.seasonNumber; + episodeCounts[sn] = (episodeCounts[sn] || 0) + 1; + }); + } + } catch (epError) { + console.log('Could not fetch episodes for count:', epError.message); + } + // Extract season information properly let seasons = []; if (showData.seasons && Array.isArray(showData.seasons)) { @@ -428,7 +456,7 @@ ipcMain.handle('get-show-details', async (event, showId) => { id: season.id, number: season.number, type: season.type?.name || 'Unknown', - episodeCount: season.episodeCount || (season.episodes ? season.episodes.length : 0) + episodeCount: episodeCounts[season.number] || 0 })); } diff --git a/renderer.js b/renderer.js index 3268c04..0258d66 100644 --- a/renderer.js +++ b/renderer.js @@ -18,6 +18,7 @@ let currentFiles = []; let currentShow = null; let currentSeasons = []; let currentEpisodes = []; +let selectedSeasonEpisodeCount = 0; // Event Listeners selectDirBtn.addEventListener('click', selectDirectory); @@ -177,31 +178,41 @@ function displayFiles(files) { return; } - files.forEach(file => { + // Separate folders from media files + const folders = files.filter(f => f.isFolder); + const mediaFiles = files.filter(f => !f.isFolder); + + // Track episode number for media files only + let episodeNumber = 1; + + // Display folders first (not draggable, no episode number) + folders.forEach(file => { + const fileItem = document.createElement('div'); + fileItem.className = 'file-item folder-item'; + fileItem.innerHTML = ` +
📁
+
${file.name}
+
+
+
+
+ `; + + // Add click handler to navigate into folder + fileItem.addEventListener('click', function() { + openDirectory(file.path); + }); + + fileListEl.appendChild(fileItem); + }); + + // Display media files with episode numbers and drag/drop + mediaFiles.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; - - // Handle folders differently from media files - if (file.isFolder) { - fileItem.classList.add('folder-item'); - fileItem.style.cursor = 'pointer'; - fileItem.innerHTML = ` -
📁
-
${file.name}
-
-
-
-
- `; - - // Add click handler to navigate into folder - fileItem.addEventListener('click', function() { - openDirectory(file.path); - }); - - fileListEl.appendChild(fileItem); - return; - } + fileItem.draggable = true; + fileItem.dataset.index = index; + fileItem.dataset.filePath = file.path; // Use actual duration from file metadata const duration = file.duration || '00:00'; @@ -212,6 +223,8 @@ function displayFiles(files) { const problemLabel = file.isProblematic ? '⚠️ ' : ''; fileItem.innerHTML = ` +
⋮⋮
+
${episodeNumber}
${file.name}
${duration}
${quality}
@@ -219,14 +232,23 @@ function displayFiles(files) {
🏷️ 💬 + 🗑️ 🎬
`; + // Add drag and drop event listeners + fileItem.addEventListener('dragstart', handleDragStart); + fileItem.addEventListener('dragend', handleDragEnd); + fileItem.addEventListener('dragover', handleDragOver); + fileItem.addEventListener('dragleave', handleDragLeave); + fileItem.addEventListener('drop', handleDrop); + // Add click event to make file name editable const fileNameElement = fileItem.querySelector('.file-name'); fileNameElement.addEventListener('click', function(e) { + e.stopPropagation(); makeEditable(e.target); }); @@ -247,7 +269,16 @@ function displayFiles(files) { icon.addEventListener('click', function(e) { e.stopPropagation(); // Prevent event bubbling const filePath = this.dataset.filePath; - const tagType = this.classList.contains('extra-tag') ? 'extra' : 'commentary'; + let tagType; + if (this.classList.contains('extra-tag')) { + tagType = 'extra'; + } else if (this.classList.contains('commentary-tag')) { + tagType = 'commentary'; + } else if (this.classList.contains('delete-tag')) { + tagType = 'delete'; + } else { + return; + } // Check if file is already tagged with this type const fileItem = this.closest('.file-item'); @@ -257,11 +288,12 @@ function displayFiles(files) { // 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); - } + // If tagging this type, untag any existing tag of other types + ['extra', 'commentary', 'delete'].forEach(otherType => { + if (otherType !== tagType && fileItem.hasAttribute('data-tagged-' + otherType)) { + untagFile(filePath, otherType); + } + }); // Tag the file addTagToEpisode(filePath, tagType); } @@ -284,8 +316,14 @@ function displayFiles(files) { // Get the file item and tag type const fileItemEl = this.closest('.file-item'); - const tagType = fileItemEl.hasAttribute('data-tagged-extra') ? 'extra' : - fileItemEl.hasAttribute('data-tagged-commentary') ? 'commentary' : null; + let tagType = null; + if (fileItemEl.hasAttribute('data-tagged-extra')) { + tagType = 'extra'; + } else if (fileItemEl.hasAttribute('data-tagged-commentary')) { + tagType = 'commentary'; + } else if (fileItemEl.hasAttribute('data-tagged-delete')) { + tagType = 'delete'; + } console.log('Tag type determined:', tagType); if (tagType) { @@ -295,6 +333,8 @@ function displayFiles(files) { // Remove the item from the file list after successful move fileItemEl.remove(); updateTaggedCount(); + updateEpisodeNumbers(); + checkEpisodeCountMatch(); } } else { console.log('No tag type found for file'); @@ -302,6 +342,81 @@ function displayFiles(files) { }); fileListEl.appendChild(fileItem); + episodeNumber++; + }); + + // Check if episode count matches after displaying files + checkEpisodeCountMatch(); +} + +// Drag and drop handlers +let draggedItem = null; + +function handleDragStart(e) { + draggedItem = this; + this.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', this.dataset.index); +} + +function handleDragEnd(e) { + this.classList.remove('dragging'); + // Remove drag-over class from all items + document.querySelectorAll('.file-item').forEach(item => { + item.classList.remove('drag-over'); + }); + draggedItem = null; +} + +function handleDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + // Only show drag-over for non-folder items + if (!this.classList.contains('folder-item') && this !== draggedItem) { + this.classList.add('drag-over'); + } +} + +function handleDragLeave(e) { + this.classList.remove('drag-over'); +} + +function handleDrop(e) { + e.preventDefault(); + this.classList.remove('drag-over'); + + if (this === draggedItem || this.classList.contains('folder-item')) return; + + // Get all media file items (not folders) + const fileItems = Array.from(fileListEl.querySelectorAll('.file-item:not(.folder-item)')); + const draggedIndex = fileItems.indexOf(draggedItem); + const dropIndex = fileItems.indexOf(this); + + if (draggedIndex === -1 || dropIndex === -1) return; + + // Move the dragged item in the DOM + if (draggedIndex < dropIndex) { + this.parentNode.insertBefore(draggedItem, this.nextSibling); + } else { + this.parentNode.insertBefore(draggedItem, this); + } + + // Update episode numbers + updateEpisodeNumbers(); +} + +function updateEpisodeNumbers() { + const fileItems = fileListEl.querySelectorAll('.file-item:not(.folder-item)'); + let episodeNum = 1; + + fileItems.forEach(item => { + const episodeEl = item.querySelector('.episode-number'); + if (episodeEl) { + episodeEl.textContent = episodeNum; + episodeEl.dataset.episode = episodeNum; + episodeNum++; + } }); } @@ -384,13 +499,22 @@ function addTagToEpisode(filePath, tagType) { // 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 + // Set color based on tag type + let tagColor; + if (tagType === 'extra') { + tagColor = '#FFD700'; // Yellow + } else if (tagType === 'commentary') { + tagColor = '#17a2b8'; // Teal + } else if (tagType === 'delete') { + tagColor = '#dc3545'; // Red + } + + // Make the icon fully saturated and highlight 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 + tagIcon.style.color = tagColor; + tagIcon.style.textShadow = `0 0 15px ${tagColor}`; + tagIcon.style.transform = 'scale(1.3)'; // Add a data attribute to track that this file is tagged item.setAttribute('data-tagged-' + tagType, 'true'); @@ -448,7 +572,7 @@ function moveTaggedFile(filePath, tagType) { // Function to move all tagged files at once async function moveAllTaggedFiles() { - const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary]'); + const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary], .file-item[data-tagged-delete]'); if (taggedItems.length === 0) { console.log('[RENDERER] No tagged files to move'); @@ -467,7 +591,14 @@ async function moveAllTaggedFiles() { const results = []; for (const item of taggedItems) { const filePath = item.querySelector('.file-name').dataset.filePath; - const tagType = item.hasAttribute('data-tagged-extra') ? 'extra' : 'commentary'; + let tagType; + if (item.hasAttribute('data-tagged-extra')) { + tagType = 'extra'; + } else if (item.hasAttribute('data-tagged-commentary')) { + tagType = 'commentary'; + } else if (item.hasAttribute('data-tagged-delete')) { + tagType = 'delete'; + } const result = await moveTaggedFile(filePath, tagType); results.push(result); @@ -486,6 +617,8 @@ async function moveAllTaggedFiles() { // Update the tagged count after all moves updateTaggedCount(); + updateEpisodeNumbers(); + checkEpisodeCountMatch(); // Summary of results const successful = results.filter(r => r.success).length; @@ -517,9 +650,20 @@ function displayCreatedFolder(folderName) { folderItem.className = 'folder-item'; folderItem.setAttribute('data-folder-name', folderName); - const displayName = folderName === 'extra' ? 'extras' : 'commentary'; - const folderIcon = folderName === 'extra' ? '📁' : '💬'; - const folderColor = folderName === 'extra' ? '#FFD700' : '#17a2b8'; + let displayName, folderIcon, folderColor; + if (folderName === 'extra') { + displayName = 'extras'; + folderIcon = '📁'; + folderColor = '#FFD700'; + } else if (folderName === 'commentary') { + displayName = 'commentary'; + folderIcon = '💬'; + folderColor = '#17a2b8'; + } else if (folderName === 'delete') { + displayName = 'delete'; + folderIcon = '🗑️'; + folderColor = '#dc3545'; + } folderItem.innerHTML = `
${folderIcon}
@@ -533,7 +677,7 @@ function displayCreatedFolder(folderName) { gap: 10px; padding: 12px; margin-bottom: 8px; - background-color: #f8f9fa; + background-color: #16213e; border: 2px solid ${folderColor}; border-radius: 8px; cursor: pointer; @@ -579,13 +723,18 @@ function untagFile(filePath, tagType) { // Remove the data attribute item.removeAttribute('data-tagged-' + tagType); - // Disable the play button when untagging - const playButton = item.querySelector('.play-button'); - if (playButton) { - playButton.style.opacity = '0.3'; - playButton.style.cursor = 'default'; - playButton.disabled = true; - playButton.style.pointerEvents = 'none'; + // Disable the play button when untagging (only if no other tags) + const hasOtherTags = item.hasAttribute('data-tagged-extra') || + item.hasAttribute('data-tagged-commentary') || + item.hasAttribute('data-tagged-delete'); + if (!hasOtherTags) { + const playButton = item.querySelector('.play-button'); + if (playButton) { + playButton.style.opacity = '0.3'; + playButton.style.cursor = 'default'; + playButton.disabled = true; + playButton.style.pointerEvents = 'none'; + } } } } @@ -601,7 +750,7 @@ function untagFile(filePath, tagType) { // Function to update the tagged items count display function updateTaggedCount() { - const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary]'); + const taggedItems = document.querySelectorAll('.file-item[data-tagged-extra], .file-item[data-tagged-commentary], .file-item[data-tagged-delete]'); const count = taggedItems.length; const taggedCircle = document.getElementById('tagged-circle'); const taggedCount = document.getElementById('tagged-count'); @@ -624,6 +773,24 @@ function updateTaggedCount() { } } +// Function to check if media file count matches selected season episode count +function checkEpisodeCountMatch() { + const fileList = document.getElementById('file-list'); + // Count only actual media files (not folders) + const mediaFiles = document.querySelectorAll('.file-item:not(.folder-item)'); + const mediaCount = mediaFiles.length; + + if (selectedSeasonEpisodeCount > 0 && mediaCount === selectedSeasonEpisodeCount) { + // Perfect match - add green outline + fileList.style.border = '3px solid #28a745'; + fileList.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.4)'; + } else { + // No match or no season selected - remove green outline + fileList.style.border = ''; + fileList.style.boxShadow = ''; + } +} + // Initialize tagged count on page load document.addEventListener('DOMContentLoaded', function() { updateTaggedCount(); @@ -769,26 +936,13 @@ function displaySeasons(seasons) { seasonsContainer.innerHTML = ''; if (!seasons || seasons.length === 0) { - seasonsContainer.innerHTML = '

No seasons available.

'; + 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}`; @@ -799,21 +953,25 @@ function displaySeasons(seasons) { const episodeCount = season.episodeCount || 0; seasonItem.innerHTML = ` - ${seasonDisplay} - - ${episodeCount} episodes - +
+ ${seasonDisplay} + + ${episodeCount} eps + +
`; // Add click event to fetch episodes seasonItem.addEventListener('click', () => { + // Remove selected class from all seasons + document.querySelectorAll('.season-item').forEach(item => item.classList.remove('selected')); + // Add selected class to clicked season + seasonItem.classList.add('selected'); fetchAndDisplayEpisodes(currentShow.id, season.number); }); - seasonsList.appendChild(seasonItem); + seasonsContainer.appendChild(seasonItem); }); - - seasonsContainer.appendChild(seasonsList); } // Fetch and display episodes for a season @@ -849,70 +1007,50 @@ function displayEpisodes(episodes) { seasonsContainer.innerHTML = ''; if (!episodes || episodes.length === 0) { - seasonsContainer.innerHTML = '

No episodes available.

'; + seasonsContainer.innerHTML = '

No episodes available.

'; + selectedSeasonEpisodeCount = 0; + checkEpisodeCountMatch(); 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'; + // Store the episode count for this season + selectedSeasonEpisodeCount = episodes.length; + checkEpisodeCountMatch(); - // 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'; + // Create a back button to return to seasons + const backButton = document.createElement('div'); + backButton.className = 'season-item'; + backButton.innerHTML = '← Back to Seasons'; + backButton.addEventListener('click', () => { + selectedSeasonEpisodeCount = 0; + checkEpisodeCountMatch(); + displaySeasons(currentSeasons); + }); + seasonsContainer.appendChild(backButton); - tableHeader.innerHTML = ` -
Episode Name
-
Runtime
- `; - - episodesTable.appendChild(tableHeader); + // Create episodes container + const episodesContainer = document.createElement('div'); + episodesContainer.id = 'episodes-container'; 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'; + const episodeRuntime = episode.runtime ? `${episode.runtime}m` : ''; episodeRow.innerHTML = ` -
Episode ${episode.number}: ${episodeName}
-
${episodeRuntime}
+
+ E${episode.number} ${episodeName} + ${episodeRuntime ? `${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); + episodesContainer.appendChild(episodeRow); }); - seasonsContainer.appendChild(episodesTable); + seasonsContainer.appendChild(episodesContainer); // Also update the main file list to show episode matching information updateFileListWithEpisodeInfo(episodes);