Fix arrow button episode number cascading - all subsequent episodes now shift correctly

This commit is contained in:
Jarian Cottingham 2026-02-25 00:17:51 -06:00
parent 30eba3ae4e
commit 46bfba582d
4 changed files with 385 additions and 130 deletions

149
AGENTS.md
View File

@ -8,31 +8,37 @@ MovieMapper is an Electron-based desktop application for organizing and managing
### Build Commands ### Build Commands
- `npm install` - Install project dependencies - `npm install` - Install project dependencies
- `npm start` - Run the application using Electron - `npm start` - Run the application using Electron
- `electron .` - Alternative way to run the application
### Test Commands ### Test Commands
- `npm test` - Currently outputs "Error: no test specified" (default test script) - `node test-api.js` - Test TheTVDB API connectivity
- `node test-api.js` - Run API connectivity test for TheTVDB - `node test-command-line.js` - Test command-line argument parsing
- `node main.js` - Run main process directly for debugging (may require environment setup) - `node test-audit.js` - Run unit tests for audit logging (uses `node:test`)
- `node test-functional.js` - Run functional tests for UI elements
- `node test-core.js` - Verify core functionality implementation
- `node test-implementation.js` - Test implemented features
- `node test-audit-functionality.js` - Test audit file operations
### Syntax Checking ### Syntax Checking
- `node --check renderer.js` - Check renderer.js for syntax errors - `node --check renderer.js` - Check renderer.js for syntax errors
- `node --check main.js` - Check main.js for syntax errors - `node --check main.js` - Check main.js for syntax errors
- `node --check utils/fileUtils.js` - Check fileUtils.js for syntax errors - `node --check utils/fileUtils.js` - Check fileUtils.js for syntax errors
### Running Single Tests
Use `node <test-file>.js` to run individual test files. Tests use Node.js built-in `node:test` framework.
### Linting ### Linting
- No explicit linting tools configured in package.json - No explicit linting tools configured
- Code style follows JavaScript/Node.js conventions - Code style follows JavaScript/Node.js conventions
- Use JSDoc comments for documentation
## Architecture Overview ## Architecture Overview
### Main Process (`main.js`) ### Main Process (`main.js`)
- Handles all IPC communication with renderer - Electron main process handling application lifecycle
- File system operations (moving, renaming files) - IPC handlers for renderer communication (`ipcMain.handle()`)
- File system operations (moving, renaming, scanning)
- TheTVDB API integration with token authentication - TheTVDB API integration with token authentication
- Audit logging to `.audit` files in directories - Audit logging to `.audit` files in directories
- Progress reporting via IPC events - Progress reporting via `mainWindow.webContents.send()`
### Renderer Process (`renderer.js`) ### Renderer Process (`renderer.js`)
- UI state management and DOM manipulation - UI state management and DOM manipulation
@ -50,43 +56,41 @@ MovieMapper is an Electron-based desktop application for organizing and managing
## Code Style Guidelines ## Code Style Guidelines
### Imports ### Imports
- Use standard Node.js `require()` syntax for modules - Use standard Node.js `require()` syntax
- Import modules at the top of files - Import modules at the top of files
- Group imports by standard library, external modules, then local modules - Group imports: standard library → external modules → local modules
- Use descriptive names for modules (e.g., `const fs = require('fs')`) - Use descriptive names: `const fs = require('fs')`
### Formatting ### Formatting
- Use 2-space indentation - 2-space indentation
- Use single quotes for strings - Single quotes for strings
- Place opening braces on the same line as the statement - Opening braces on same line as statement
- Add spaces around operators and after commas - Spaces around operators and after commas
- No semicolons required (follows JavaScript convention) - No semicolons required
- Use consistent spacing around code blocks - Consistent spacing around code blocks
### Naming Conventions ### Naming Conventions
- Use camelCase for variables and functions - camelCase for variables and functions
- Use PascalCase for constructors - PascalCase for constructors
- Use UPPER_CASE for constants - UPPER_CASE for constants
- Use descriptive variable names
- Function names should be verbs (e.g., `scanDirectory`, `extractFileMetadata`) - Function names should be verbs (e.g., `scanDirectory`, `extractFileMetadata`)
- File names should be lowercase with hyphens (e.g., `file-utils.js`) - File names lowercase with hyphens (e.g., `file-utils.js`)
### Types ### Types
- This is a JavaScript project without TypeScript - JavaScript project without TypeScript
- Use JSDoc comments to document function parameters and return values - Use JSDoc comments for function parameters and return values
- Use descriptive parameter names
- Include type information in comments for complex objects - Include type information in comments for complex objects
### Error Handling ### Error Handling
- Use try/catch blocks for async operations - Use try/catch blocks for async operations
- Handle file system errors gracefully - Handle file system errors gracefully
- Log warnings for problematic files but don't crash the application - Log warnings for problematic files without crashing
- Return consistent error structures: `{ success: true/false, error?: string, ... }` - Return consistent error structures: `{ success: true/false, error?: string, ... }`
- Use specific error messages with context information - Use specific error messages with context information
- Implement proper error propagation throughout the application - Implement proper error propagation throughout the application
### IPC Communication Patterns ### IPC Communication Patterns
- Main process handlers use `ipcMain.handle()` for async operations - Main process uses `ipcMain.handle()` for async operations
- Renderer invokes with `ipcRenderer.invoke()` returning promises - Renderer invokes with `ipcRenderer.invoke()` returning promises
- Progress updates use `mainWindow.webContents.send()` for push events - Progress updates use `mainWindow.webContents.send()` for push events
- Renderer listens with `ipcRenderer.on()` for progress events - Renderer listens with `ipcRenderer.on()` for progress events
@ -108,13 +112,13 @@ MovieMapper is an Electron-based desktop application for organizing and managing
- Implement authentication token caching for performance - Implement authentication token caching for performance
### Code Patterns ### Code Patterns
- Use async/await for handling asynchronous operations - Use async/await for asynchronous operations
- Handle permission errors gracefully when scanning directories - Handle permission errors gracefully when scanning directories
- Provide user feedback through console warnings for problematic files - Provide user feedback through console warnings for problematic files
- Use descriptive variable names that reflect their purpose - Use descriptive variable names that reflect their purpose
- Implement comprehensive logging for debugging - Implement comprehensive logging for debugging
- Use consistent IPC patterns for communication between main and renderer processes - Use consistent IPC patterns for main/renderer communication
- Use `setImmediate` yields when sending IPC messages in loops to allow event loop processing - Use `setImmediate` yields when sending IPC messages in loops
### Debugging ### Debugging
- Built-in debugging utilities for problematic files - Built-in debugging utilities for problematic files
@ -125,17 +129,17 @@ MovieMapper is an Electron-based desktop application for organizing and managing
- Audit logs written to `.audit` files in each directory - Audit logs written to `.audit` files in each directory
### Environment Configuration ### Environment Configuration
- API keys stored in `.env` file - API keys stored in `.env` file (root level only)
- Environment variables should be validated on application startup - Environment variables validated on application startup
- Provide clear error messages when environment variables are missing - Clear error messages when environment variables missing
- Use `dotenv` package for loading environment variables - Use `dotenv` package for loading environment variables
## Key Features Implementation ## Key Features Implementation
### File Tagging System ### File Tagging System
- Files can be tagged as "extra" or "commentary" - Files tagged as "extra" or "commentary"
- Tags stored as data attributes on DOM elements: `data-tagged-extra`, `data-tagged-commentary` - Tags stored as data attributes: `data-tagged-extra`, `data-tagged-commentary`
- Visual feedback with color changes and glow effects when tagged - Visual feedback with color changes and glow effects
- Play button enables only when file is tagged - Play button enables only when file is tagged
- Individual play button moves single file - Individual play button moves single file
- Floating action button (FAB) moves all tagged files at once - Floating action button (FAB) moves all tagged files at once
@ -159,58 +163,6 @@ MovieMapper is an Electron-based desktop application for organizing and managing
- Audit log entry written for each move operation - Audit log entry written for each move operation
- Files removed from UI after successful move - Files removed from UI after successful move
## Recent Changes (Session 2026-02-22)
### Bug Fixes
1. **Fixed duplicate `moveTaggedFile` function** - Two definitions were overwriting each other
2. **Fixed missing click handler for FAB** - Added `moveAllTaggedFiles()` on floating circle click
3. **Fixed "extra" vs "extras" folder naming** - Now correctly uses "extras" for Jellyfin
4. **Fixed progressBar reference error** - Removed reference after switching to spinner
5. **Removed non-existent test-api-btn reference** - Was causing errors on load
### New Features
1. **Folder navigation** - Click folders to navigate into them
2. **Non-recursive scanning** - Only shows current directory contents
3. **Progress spinner** - Shows file being processed with count
4. **Batch file moving** - FAB moves all tagged files at once
5. **Visual feedback** - FAB turns yellow while processing
### UI Improvements
1. **Dark theme** - Modern dark color scheme (#1a1a2e, #16213e, #0f3460)
2. **Clean layout** - Search/shows on left sidebar, files on right
3. **Accent color** - Pink/red accent (#e94560)
4. **Better typography** - System font stack
5. **Smooth animations** - Hover effects and transitions
## Jellyfin Compatible Folder Names
### Extras Folders (place inside movie/show folder)
- `behind the scenes` - Behind-the-scenes content
- `deleted scenes` - Deleted scenes
- `interviews` - Cast/crew interviews
- `scenes` - Individual scenes
- `samples` - Sample clips
- `shorts` - Short films
- `featurettes` - Featurettes
- `clips` - Clips
- `other` - Generic catch-all for unknown extras
- `extras` - Generic catch-all for unknown extras (CURRENTLY IMPLEMENTED)
- `trailers` - Trailers and previews
- `theme-music` - Theme music audio files
- `backdrops` - Backdrop videos
### Special Single-File Names (in same folder as media)
- `trailer` - Single trailer file
- `sample` - Single sample file
- `theme` - Theme song audio file
### File Suffix Options (append to filename)
- `-trailer`, `.trailer`, `_trailer`, ` trailer`
- `-sample`, `.sample`, `_sample`, ` sample`
- `-scene`, `-clip`, `-interview`
- `-behindthescenes`, `-deleted`, `-deletedscene`
- `-featurette`, `-short`, `-other`, `-extra`
## Known Issues & Future Work ## Known Issues & Future Work
### Known Issues ### Known Issues
@ -228,6 +180,21 @@ MovieMapper is an Electron-based desktop application for organizing and managing
7. Add confirmation dialog before moving files 7. Add confirmation dialog before moving files
## Execution Constraints ## Execution Constraints
- All commands that might block indefinitely must be run with a timeout - All commands that might block indefinitely must run with timeout
- Example: `timeout 30 npm start` to run the application with 30-second timeout - Example: `timeout 30 npm start` to run with 30-second timeout
- Always run syntax checks after modifying JS files: `node --check <file>` - Always run syntax checks after modifying JS files: `node --check <file>`
## Jellyfin Compatible Folder Names
### Extras Folders (place inside movie/show folder)
- `behind the scenes`, `deleted scenes`, `interviews`, `scenes`, `samples`, `shorts`, `featurettes`, `clips`, `other`, `extras`, `trailers`, `theme-music`, `backdrops`
### Special Single-File Names (in same folder as media)
- `trailer`, `sample`, `theme`
### File Suffix Options (append to filename)
- `-trailer`, `.trailer`, `_trailer`, ` trailer`
- `-sample`, `.sample`, `_sample`, ` sample`
- `-scene`, `-clip`, `-interview`
- `-behindthescenes`, `-deleted`, `-deletedscene`
- `-featurette`, `-short`, `-other`, `-extra`

View File

@ -280,6 +280,13 @@
cursor: pointer; cursor: pointer;
} }
.episode-number-container {
display: flex;
align-items: center;
gap: 4px;
margin-right: 12px;
}
.episode-number { .episode-number {
min-width: 40px; min-width: 40px;
height: 28px; height: 28px;
@ -291,7 +298,36 @@
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
border-radius: 4px; border-radius: 4px;
margin-right: 12px; flex: 1;
}
.episode-arrow {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background-color: #0f3460;
color: #eee;
font-size: 14px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
flex-shrink: 0;
}
.episode-arrow:hover {
background-color: #1a1a2e;
}
.episode-arrow:active {
background-color: #16213e;
}
.episode-arrow:disabled {
opacity: 0.3;
cursor: not-allowed;
} }
.drag-handle { .drag-handle {
@ -487,7 +523,10 @@
</div> </div>
</div> </div>
<button id="begin-mapping-btn" class="begin-mapping-btn">Begin Mapping</button> <div style="display: flex; gap: 10px; margin-top: 10px;">
<button id="update-ranges-btn" class="begin-mapping-btn" style="flex: 1;">Update Ranges</button>
<button id="begin-mapping-btn" class="begin-mapping-btn" style="flex: 1;">Begin Mapping</button>
</div>
</div> </div>
</div> </div>

40
main.js
View File

@ -50,7 +50,7 @@ function createWindow() {
} }
// Open the DevTools. // Open the DevTools.
// mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} }
// IPC handler for selecting directory // IPC handler for selecting directory
@ -309,11 +309,42 @@ ipcMain.handle('begin-mapping', async (event, { directory, files, tvdbId }) => {
const oldPath = file.filePath; const oldPath = file.filePath;
const ext = path.extname(oldPath); const ext = path.extname(oldPath);
// Format episode number with leading zero
const epNum = String(file.episodeNumber).padStart(2, '0');
const seasonNum = String(seasonNumber).padStart(2, '0'); const seasonNum = String(seasonNumber).padStart(2, '0');
// Build new filename: "ShowName S01E01 - 1080p.ext" // Handle episode ranges (e.g., "1-3" for multiple episodes)
const episodeStart = file.episodeStart || file.episodeNumber || 1;
const episodeEnd = file.episodeEnd !== undefined ? file.episodeEnd : episodeStart;
// If range is valid (start < end), create multi-episode files
if (episodeEnd > episodeStart) {
// Multi-episode range (e.g., "ShowName S01E01-E03 - 1080p.ext")
const startEpNum = String(episodeStart).padStart(2, '0');
const endEpNum = String(episodeEnd).padStart(2, '0');
let newFileName = `${showName} S${seasonNum}E${startEpNum}-${endEpNum}`;
if (file.quality && file.quality !== 'N/A' && file.quality !== '-') {
newFileName += ` - ${file.quality}`;
}
newFileName += ext;
const newPath = path.join(path.dirname(oldPath), newFileName);
writeLog(`[BEGIN-MAPPING] Processing range: ${episodeStart}-${episodeEnd}, new filename: ${newFileName}`);
// Check if target already exists
if (fs.existsSync(newPath)) {
writeLog(`[BEGIN-MAPPING] Target exists, skipping: ${newFileName}`);
errorCount++;
continue;
}
fs.renameSync(oldPath, newPath);
writeLog(`[BEGIN-MAPPING] Renamed range: ${path.basename(oldPath)} -> ${newFileName}`);
successCount++;
} else {
// Single episode (original logic)
const epNum = String(episodeStart).padStart(2, '0');
let newFileName = `${showName} S${seasonNum}E${epNum}`; let newFileName = `${showName} S${seasonNum}E${epNum}`;
if (file.quality && file.quality !== 'N/A' && file.quality !== '-') { if (file.quality && file.quality !== 'N/A' && file.quality !== '-') {
newFileName += ` - ${file.quality}`; newFileName += ` - ${file.quality}`;
@ -339,6 +370,7 @@ ipcMain.handle('begin-mapping', async (event, { directory, files, tvdbId }) => {
fs.renameSync(oldPath, newPath); fs.renameSync(oldPath, newPath);
writeLog(`[BEGIN-MAPPING] Renamed: ${path.basename(oldPath)} -> ${newFileName}`); writeLog(`[BEGIN-MAPPING] Renamed: ${path.basename(oldPath)} -> ${newFileName}`);
successCount++; successCount++;
}
} catch (fileErr) { } catch (fileErr) {
writeLog(`[BEGIN-MAPPING] Error renaming file: ${fileErr.message}`); writeLog(`[BEGIN-MAPPING] Error renaming file: ${fileErr.message}`);
errorCount++; errorCount++;

View File

@ -11,6 +11,7 @@ const showDetailsEl = document.getElementById('show-details');
const progressContainer = document.getElementById('progress-container'); const progressContainer = document.getElementById('progress-container');
const progressText = document.getElementById('progress-text'); const progressText = document.getElementById('progress-text');
const progressCount = document.getElementById('progress-count'); const progressCount = document.getElementById('progress-count');
const updateRangesBtn = document.getElementById('update-ranges-btn');
// Current state // Current state
let currentDirectory = null; let currentDirectory = null;
@ -24,12 +25,28 @@ let selectedSeasonEpisodeCount = 0;
selectDirBtn.addEventListener('click', selectDirectory); selectDirBtn.addEventListener('click', selectDirectory);
searchInput.addEventListener('input', debounce(searchShows, 300)); searchInput.addEventListener('input', debounce(searchShows, 300));
// Add click handler for the floating circle (play button) to move all tagged files // Add click handler for episode number editing (ranges)
document.getElementById('tagged-circle').addEventListener('click', moveAllTaggedFiles); document.addEventListener('click', function(e) {
if (e.target.classList.contains('episode-number')) {
e.stopPropagation();
makeEpisodeRangeEditable(e.target);
}
});
// Add click handler for Update Ranges button
if (updateRangesBtn) {
updateRangesBtn.addEventListener('click', function() {
updateEpisodeNumbers();
checkEpisodeCountMatch();
});
}
// Add click handler for Begin Mapping button // Add click handler for Begin Mapping button
document.getElementById('begin-mapping-btn').addEventListener('click', beginMapping); document.getElementById('begin-mapping-btn').addEventListener('click', beginMapping);
// Add click handler for the floating circle (play button) to move all tagged files
document.getElementById('tagged-circle').addEventListener('click', moveAllTaggedFiles);
// Listen for scan progress updates from main process // Listen for scan progress updates from main process
ipcRenderer.on('scan-progress', (event, { current, total, fileName }) => { ipcRenderer.on('scan-progress', (event, { current, total, fileName }) => {
updateProgress(current, total, fileName); updateProgress(current, total, fileName);
@ -227,7 +244,11 @@ function displayFiles(files) {
fileItem.innerHTML = ` fileItem.innerHTML = `
<div class="drag-handle"></div> <div class="drag-handle"></div>
<div class="episode-number" data-episode="${episodeNumber}">${episodeNumber}</div> <div class="episode-number-container">
<button class="episode-arrow episode-arrow-left" data-file-path="${file.path}" title="Decrease episode range"></button>
<div class="episode-number" data-episode-start="${episodeNumber}" data-episode-end="${episodeNumber}">${episodeNumber}</div>
<button class="episode-arrow episode-arrow-right" data-file-path="${file.path}" title="Increase episode range"></button>
</div>
<div class="file-name" data-file-path="${file.path}">${file.name}</div> <div class="file-name" data-file-path="${file.path}">${file.name}</div>
<div class="file-duration">${duration}</div> <div class="file-duration">${duration}</div>
<div class="file-quality">${quality}</div> <div class="file-quality">${quality}</div>
@ -255,6 +276,24 @@ function displayFiles(files) {
makeEditable(e.target); makeEditable(e.target);
}); });
// Add click handlers for episode arrow buttons
const arrowLeft = fileItem.querySelector('.episode-arrow-left');
const arrowRight = fileItem.querySelector('.episode-arrow-right');
if (arrowLeft) {
arrowLeft.addEventListener('click', function(e) {
e.stopPropagation();
handleEpisodeArrowClick(this, 'left');
});
}
if (arrowRight) {
arrowRight.addEventListener('click', function(e) {
e.stopPropagation();
handleEpisodeArrowClick(this, 'right');
});
}
// Add hover effects for tags // Add hover effects for tags
const tagIcons = fileItem.querySelectorAll('.tag-icon'); const tagIcons = fileItem.querySelectorAll('.tag-icon');
tagIcons.forEach(icon => { tagIcons.forEach(icon => {
@ -413,16 +452,168 @@ function updateEpisodeNumbers() {
const fileItems = fileListEl.querySelectorAll('.file-item:not(.folder-item)'); const fileItems = fileListEl.querySelectorAll('.file-item:not(.folder-item)');
let episodeNum = 1; let episodeNum = 1;
fileItems.forEach(item => { console.log('[updateEpisodeNumbers] Starting with episodeNum:', episodeNum);
console.log('[updateEpisodeNumbers] Total items:', fileItems.length);
console.trace('[updateEpisodeNumbers] Call stack');
fileItems.forEach((item, index) => {
const episodeEl = item.querySelector('.episode-number'); const episodeEl = item.querySelector('.episode-number');
if (episodeEl) { if (episodeEl) {
episodeEl.textContent = episodeNum; const storedStart = episodeEl.dataset.episodeStart;
const storedEnd = episodeEl.dataset.episodeEnd;
console.log(`[updateEpisodeNumbers] Item ${index}: storedStart="${storedStart}", storedEnd="${storedEnd}"`);
const start = storedStart ? parseInt(storedStart) : episodeNum;
const end = storedEnd ? parseInt(storedEnd) : start;
const rangeText = start === end ? `${start}` : `${start}-${end}`;
episodeEl.textContent = rangeText;
episodeEl.dataset.episode = episodeNum; episodeEl.dataset.episode = episodeNum;
episodeNum++; console.log(`[updateEpisodeNumbers] Item ${index}: calculated start=${start}, end=${end}, display=${rangeText}, next episodeNum=${end + 1}`);
episodeNum = end + 1;
} }
}); });
} }
// Make an episode number editable for range editing
function makeEpisodeRangeEditable(element) {
if (element.contentEditable === 'true') return;
const originalText = element.textContent;
const originalStart = element.dataset.episodeStart;
const originalEnd = element.dataset.episodeEnd;
element.contentEditable = 'true';
element.focus();
element.classList.add('editing');
const range = document.createRange();
range.selectNodeContents(element);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
const saveEdit = function() {
const newText = element.textContent.trim();
if (newText !== originalText) {
let start, end;
if (newText.includes('-')) {
const parts = newText.split('-').map(p => parseInt(p.trim()));
start = parts[0];
end = parts[1] || start;
} else {
const num = parseInt(newText);
start = num;
end = num;
}
if (!isNaN(start) && !isNaN(end)) {
element.dataset.episodeStart = start;
element.dataset.episodeEnd = end;
console.log(`[makeEpisodeRangeEditable] Episode range updated: ${start}-${end}`);
updateEpisodeNumbers();
} else {
element.textContent = originalText;
element.dataset.episodeStart = originalStart;
element.dataset.episodeEnd = originalEnd;
}
}
element.contentEditable = 'false';
element.classList.remove('editing');
};
element.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
saveEdit();
}
}, { once: true });
element.addEventListener('blur', saveEdit, { once: true });
}
// Handle episode arrow button clicks
function handleEpisodeArrowClick(element, direction) {
const fileItem = element.closest('.file-item');
const episodeEl = fileItem.querySelector('.episode-number');
if (!episodeEl) return;
// Get current values before updating
let start = parseInt(episodeEl.dataset.episodeStart) || 1;
let end = parseInt(episodeEl.dataset.episodeEnd) || start;
const oldStart = start;
const oldEnd = end;
// Update the episode range
if (direction === 'left') {
// Decrease range - move start back by 1
if (start > 1) {
start--;
}
} else {
// Increase range - move end forward by 1
end++;
}
// Update this episode's data attributes
episodeEl.dataset.episodeStart = start;
episodeEl.dataset.episodeEnd = end;
// Get all media file items (not folders) in order
const allFileItems = Array.from(fileListEl.querySelectorAll('.file-item:not(.folder-item)'));
// Find current index by matching file path
const filePath = fileItem.dataset.filePath;
let currentIndex = -1;
for (let i = 0; i < allFileItems.length; i++) {
if (allFileItems[i].dataset.filePath === filePath) {
currentIndex = i;
break;
}
}
if (currentIndex === -1) return;
// For right arrow: shift all subsequent episodes forward by 1
if (direction === 'right') {
for (let i = currentIndex + 1; i < allFileItems.length; i++) {
const nextItem = allFileItems[i];
const nextEpisodeEl = nextItem.querySelector('.episode-number');
if (nextEpisodeEl) {
let nextStart = parseInt(nextEpisodeEl.dataset.episodeStart) || 1;
const nextEnd = parseInt(nextEpisodeEl.dataset.episodeEnd) || nextStart;
nextEpisodeEl.dataset.episodeStart = nextStart + 1;
nextEpisodeEl.dataset.episodeEnd = nextEnd + 1;
}
}
} else {
// For left arrow: all subsequent episodes shift down
const shiftAmount = oldStart - start;
for (let i = currentIndex + 1; i < allFileItems.length; i++) {
const nextItem = allFileItems[i];
const nextEpisodeEl = nextItem.querySelector('.episode-number');
if (nextEpisodeEl) {
let nextStart = parseInt(nextEpisodeEl.dataset.episodeStart) || 1;
const nextEnd = parseInt(nextEpisodeEl.dataset.episodeEnd) || nextStart;
nextEpisodeEl.dataset.episodeStart = nextStart - shiftAmount;
nextEpisodeEl.dataset.episodeEnd = nextEnd - shiftAmount;
}
}
}
// Force immediate DOM update
updateEpisodeNumbers();
}
// Make a file name editable // Make a file name editable
function makeEditable(element) { function makeEditable(element) {
// Prevent editing if already in edit mode // Prevent editing if already in edit mode
@ -655,7 +846,7 @@ async function beginMapping() {
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Mapping...'; btn.textContent = 'Mapping...';
// Collect file data with episode numbers from UI order // Collect file data with episode ranges from UI order
const filesToMap = []; const filesToMap = [];
fileItems.forEach((item, index) => { fileItems.forEach((item, index) => {
const fileNameEl = item.querySelector('.file-name'); const fileNameEl = item.querySelector('.file-name');
@ -663,10 +854,14 @@ async function beginMapping() {
const episodeNumEl = item.querySelector('.episode-number'); const episodeNumEl = item.querySelector('.episode-number');
if (fileNameEl && fileNameEl.dataset.filePath) { if (fileNameEl && fileNameEl.dataset.filePath) {
const episodeStart = episodeNumEl ? parseInt(episodeNumEl.dataset.episodeStart || (index + 1)) : (index + 1);
const episodeEnd = episodeNumEl ? parseInt(episodeNumEl.dataset.episodeEnd || episodeStart) : episodeStart;
filesToMap.push({ filesToMap.push({
filePath: fileNameEl.dataset.filePath, filePath: fileNameEl.dataset.filePath,
quality: qualityEl ? qualityEl.textContent : '', quality: qualityEl ? qualityEl.textContent : '',
episodeNumber: episodeNumEl ? parseInt(episodeNumEl.textContent) : (index + 1) episodeStart: episodeStart,
episodeEnd: episodeEnd
}); });
} }
}); });
@ -847,9 +1042,31 @@ function checkEpisodeCountMatch() {
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
// Count only actual media files (not folders) // Count only actual media files (not folders)
const mediaFiles = document.querySelectorAll('.file-item:not(.folder-item)'); const mediaFiles = document.querySelectorAll('.file-item:not(.folder-item)');
const mediaCount = mediaFiles.length;
if (selectedSeasonEpisodeCount > 0 && mediaCount === selectedSeasonEpisodeCount) { // Calculate total episode range (sum of all episode ranges)
let totalEpisodeCount = 0;
let lastEpisodeEnd = 0;
mediaFiles.forEach((item, index) => {
const episodeEl = item.querySelector('.episode-number');
if (episodeEl) {
const episodeStart = parseInt(episodeEl.dataset.episodeStart || (index + 1));
const episodeEnd = parseInt(episodeEl.dataset.episodeEnd || episodeStart);
// Calculate episodes in this range
const rangeSize = episodeEnd - episodeStart + 1;
totalEpisodeCount += rangeSize;
// Track the last episode end for sequential checking
if (index === 0 || episodeEnd > lastEpisodeEnd) {
lastEpisodeEnd = episodeEnd;
}
}
});
// Use lastEpisodeEnd for comparison (handles ranges properly)
// For example: if we have episodes 1-3, 4-5, the last episode is 5, not 2 files
if (selectedSeasonEpisodeCount > 0 && lastEpisodeEnd === selectedSeasonEpisodeCount) {
// Perfect match - add green outline // Perfect match - add green outline
fileList.style.border = '3px solid #28a745'; fileList.style.border = '3px solid #28a745';
fileList.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.4)'; fileList.style.boxShadow = '0 0 15px rgba(40, 167, 69, 0.4)';