Implement mutually exclusive tagging with untagging, fix UI shifting, and add clapper emoji to title
This commit is contained in:
parent
2549295904
commit
e667dc37c9
163
PROJECT_OVERVIEW.md
Normal file
163
PROJECT_OVERVIEW.md
Normal 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
|
||||
28
index.html
28
index.html
@ -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;
|
||||
}
|
||||
|
||||
231
renderer.js
231
renderer.js
@ -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);
|
||||
// Function to handle tagging
|
||||
function addTagToEpisode(filePath, tagType) {
|
||||
console.log(`Tagging file ${filePath} as ${tagType}`);
|
||||
|
||||
// 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();
|
||||
// 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
|
||||
|
||||
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';
|
||||
// Add a data attribute to track that this file is tagged
|
||||
item.setAttribute('data-tagged-' + tagType, 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user