Implement mutually exclusive tagging with untagging, fix UI shifting, and add clapper emoji to title

This commit is contained in:
Jarian Cottingham 2026-02-19 10:08:11 -06:00
parent d3aac3261d
commit e3101006f2
3 changed files with 286 additions and 138 deletions

163
PROJECT_OVERVIEW.md Normal file
View File

@ -0,0 +1,163 @@
# MovieMapper Project Overview
## Project Description
MovieMapper is a desktop application designed for organizing and managing movie and TV show collections. It provides a user-friendly interface for browsing media files, searching TV shows using TheTVDB API, and managing file metadata including durations and quality information.
## Architecture
### Main Components
1. **Electron-based Desktop Application** - Cross-platform desktop application using Electron
2. **Main Process** (`main.js`) - Handles application lifecycle, IPC communication, and API interactions
3. **Renderer Process** (`renderer.js`) - Manages the user interface and user interactions
4. **File Utilities** (`utils/fileUtils.js`) - Handles file scanning, metadata extraction, and media processing
5. **Frontend Interface** (`index.html`) - HTML structure with CSS styling
## Core Features
### 1. Directory Browsing and Media Scanning
- **Directory Selection**: Users can select any directory containing media files
- **Recursive Scanning**: Automatically scans subdirectories for media files
- **Media File Detection**: Identifies media files based on supported extensions (.mp4, .mkv, .avi, .mov, .flv, .webm)
- **File Metadata Extraction**:
- Video duration in mm:ss format
- Video quality (4K, 1080p, 720p, etc.)
- Frame rate information
- File size and modification date
### 2. TV Show Search and Management
- **TheTVDB API Integration**: Search and retrieve TV show information
- **Show Details**: View show information including:
- Series name
- Status
- First aired date
- Overview/plot summary
- Image/artwork
- **Season Management**: Browse seasons with episode counts
- **Episode Information**: View episode details including:
- Episode names
- Episode numbers
- Runtime information
- Air dates
### 3. File Management
- **File Renaming**: Click on file names to rename them directly in the interface
- **File Tagging**: Mark files as extras or with commentary tags
- **File Untagging**: Click on already tagged files to remove the tag
- **File Organization**: Move files to "extras" folder for better organization
- **Problematic File Handling**: Identifies and flags files that cause issues during metadata extraction
### 4. Technical Features
- **Electron Framework**: Cross-platform desktop application
- **FFmpeg Integration**: For video metadata extraction (duration, quality, frame rate)
- **API Error Handling**: Robust error handling for API calls and file operations
- **IPC Communication**: Inter-process communication between main and renderer processes
- **Environment Configuration**: API keys and configuration loaded from .env file
## Technical Implementation
### Main Process (`main.js`)
- **IPC Handlers**:
- Directory selection and scanning
- File renaming
- TVDB API integration (search, show details, season episodes)
- File system operations (moving files to extras)
- Debugging utilities
- **Electron Integration**: Window management, application lifecycle
- **API Authentication**: Handles TVDB v4 API authentication with bearer tokens
### Renderer Process (`renderer.js`)
- **UI Management**: Handles all user interface interactions
- **Event Handling**: Click events, input handling, form submissions
- **File Display**: Renders file lists with metadata
- **Search Functionality**: Debounced search with API integration
- **Editing Support**: Makes file names editable with save functionality
- **Tagging System**: Visual indicators for tagged files
### File Utilities (`utils/fileUtils.js`)
- **Directory Scanning**: Recursively scans directories for media files
- **Metadata Extraction**:
- `extractFileDuration()`: Gets video duration using FFmpeg
- `extractVideoQuality()`: Determines video quality and frame rate
- `extractFileMetadata()`: Comprehensive metadata extraction
- **Error Handling**: Graceful handling of permission errors and file issues
## API Integration
### TheTVDB v4 API
- **Authentication**: Uses bearer token authentication
- **Endpoints**:
- Search: `https://api4.thetvdb.com/v4/search`
- Show Details: `https://api4.thetvdb.com/v4/series/{id}/extended`
- Episodes: `https://api4.thetvdb.com/v4/series/{id}/episodes`
- **Configuration**: API key loaded from environment variables
## User Interface
### Layout
- **Sidebar**:
- Directory selection controls
- TV show search input
- Search results display
- Show details and seasons display
- **Main Content Area**:
- File listing with metadata
- File renaming capability
- File tagging indicators
### Features
- **Responsive Design**: Clean, organized interface
- **Visual Feedback**:
- File renaming with editing support
- Tagging indicators with visual effects
- Problematic file highlighting
- **Search Integration**: Real-time search with debouncing
- **Episode Display**: Organized season and episode information
## Dependencies
### Core Dependencies
- **Electron**: Desktop application framework
- **fluent-ffmpeg**: Video metadata extraction
- **axios**: HTTP client for API requests
- **dotenv**: Environment variable management
### Development Dependencies
- **package.json**: Project metadata and dependencies
- **package-lock.json**: Exact dependency versions
## Installation and Setup
### Prerequisites
1. Node.js installed
2. FFmpeg installed (required for video metadata extraction)
3. TheTVDB API key (included in .env file)
### Setup Process
1. Clone the repository
2. Install dependencies: `npm install`
3. Run the application: `npm start` or `electron .`
## Usage Workflow
1. **Select Directory**: Click "Select Directory" to choose a folder containing media files
2. **Scan Files**: Application automatically scans the directory and displays media files with metadata
3. **Browse Files**: View files with duration, quality, and frame rate information
4. **Rename Files**: Click on any file name to rename it
5. **Search Shows**: Use the search functionality to find TV shows and their details
6. **View Seasons**: Click on seasons to see episode information
7. **Organize Files**: Use tagging and moving to extras functionality for better organization
## Error Handling and Debugging
- **File System Errors**: Graceful handling of permission errors and missing files
- **API Errors**: Comprehensive error handling for TVDB API calls
- **Metadata Extraction**: Fallbacks when FFmpeg fails to extract information
- **Debug Tools**: Built-in debugging utilities for problematic files
## Future Enhancements
- Enhanced file matching algorithms for better episode identification
- Database storage for persistent show and file information
- Additional tagging and categorization features
- Export functionality for organized collections
- Advanced search and filtering options

View File

@ -166,7 +166,7 @@
<body>
<div class="container">
<div class="sidebar">
<h1>Movie Mapper</h1>
<h1>Movie Mapper 🎬</h1>
<div class="controls">
<button id="select-dir-btn">Select Directory</button>
@ -269,6 +269,7 @@
transition: all 0.2s;
padding: 4px;
border-radius: 4px;
position: relative;
}
.tag-icon:hover {
@ -276,6 +277,31 @@
transform: scale(1.1);
}
/* For tagged state, use pseudo-elements to show the highlight */
.tag-icon[data-tagged-extra]::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 215, 0, 0.3);
border-radius: 4px;
z-index: -1;
}
.tag-icon[data-tagged-commentary]::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(25, 160, 184, 0.3);
border-radius: 4px;
z-index: -1;
}
.extra-tag {
color: #ffc107;
}

View File

@ -86,6 +86,10 @@ function displayFiles(files) {
<div class="file-duration">${duration}</div>
<div class="file-quality">${quality}</div>
<div class="file-fps">${fps}</div>
<div class="file-tags">
<span class="tag-icon extra-tag" data-file-path="${file.path}" title="Mark as Extra">🏷</span>
<span class="tag-icon commentary-tag" data-file-path="${file.path}" title="Add Commentary">💬</span>
</div>
`;
// Add click event to make file name editable
@ -94,11 +98,42 @@ 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
// 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);
@ -167,105 +202,67 @@ 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();
}
// 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');
}
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();
}
});
}
});
// 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`);
}
// Initialize context menu when the app loads
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded - adding context menu');
addFileContextMenu();
});
// 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() {
@ -614,44 +611,6 @@ function updateFileListWithEpisodeInfo(episodes) {
}
}
// Enhanced file display that can show episode matching info
function displayFiles(files) {
fileListEl.innerHTML = '';
if (files.length === 0) {
fileListEl.innerHTML = '<p>No media files found in this directory.</p>';
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 ? '<span class="problematic-label">⚠️</span> ' : '';
fileItem.innerHTML = `
<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>
<div class="file-fps">${fps}</div>
`;
// 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
function debounce(func, wait) {
let timeout;