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);