diff --git a/index.html b/index.html index e95fe91..c5e2586 100644 --- a/index.html +++ b/index.html @@ -365,6 +365,38 @@ padding: 4px; } + /* Begin Mapping Button */ + .begin-mapping-btn { + width: 100%; + padding: 16px 24px; + margin-top: 20px; + background: linear-gradient(135deg, #28a745, #20c997); + color: white; + border: none; + border-radius: 8px; + font-size: 18px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3); + } + + .begin-mapping-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(40, 167, 69, 0.5); + } + + .begin-mapping-btn:active { + transform: translateY(0); + } + + .begin-mapping-btn:disabled { + background: #555; + cursor: not-allowed; + transform: none; + box-shadow: none; + } + /* Floating Action Button */ .floating-circle { position: fixed; @@ -454,6 +486,8 @@

Click "Select Directory" to start browsing media files.

+ + diff --git a/main.js b/main.js index c7dff73..6c359c7 100644 --- a/main.js +++ b/main.js @@ -275,6 +275,100 @@ ipcMain.handle('move-file-to-folder', async (event, { filePath, folderName }) => } }); +// IPC handler for beginning the mapping process +ipcMain.handle('begin-mapping', async (event, { directory, files, tvdbId }) => { + writeLog(`[BEGIN-MAPPING] Starting mapping for ${files.length} files in ${directory}`); + + try { + const fs = require('fs'); + const path = require('path'); + + // Get season folder name (direct parent of files) + const seasonFolder = path.basename(directory); + + // Get show folder (parent of season folder) + const showFolderPath = path.dirname(directory); + const showFolderName = path.basename(showFolderPath); + + // Extract season number from folder name (e.g., "Season 01" -> 1, "S01" -> 1) + const seasonMatch = seasonFolder.match(/(?:season\s*|s)(\d+)/i); + const seasonNumber = seasonMatch ? parseInt(seasonMatch[1]) : 1; + + writeLog(`[BEGIN-MAPPING] Show folder: ${showFolderName}, Season: ${seasonNumber}`); + + // Extract clean show name (without tvdbid bracket if present) + // Matches [tvdbid-XXXXX] or [tvdbid-series-XXXXX] formats + const showName = showFolderName.replace(/\s*\[tvdbid-[^\]]+\]/, '').trim(); + + // Rename each file first (using original paths) + let successCount = 0; + let errorCount = 0; + + for (const file of files) { + try { + const oldPath = file.filePath; + 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'); + + // Build new filename: "ShowName S01E01 - 1080p.ext" + let newFileName = `${showName} S${seasonNum}E${epNum}`; + if (file.quality && file.quality !== 'N/A' && file.quality !== '-') { + newFileName += ` - ${file.quality}`; + } + newFileName += ext; + + const newPath = path.join(path.dirname(oldPath), newFileName); + + // Skip if file already has the correct name + if (oldPath === newPath) { + writeLog(`[BEGIN-MAPPING] File already mapped: ${path.basename(oldPath)}`); + successCount++; + continue; + } + + // 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: ${path.basename(oldPath)} -> ${newFileName}`); + successCount++; + } catch (fileErr) { + writeLog(`[BEGIN-MAPPING] Error renaming file: ${fileErr.message}`); + errorCount++; + } + } + + // Now rename show folder at the end (if needed) + let newDirectory = directory; + if (tvdbId && !showFolderName.includes('[tvdbid-')) { + const newShowFolderName = `${showFolderName} [tvdbid-${tvdbId}]`; + const newShowFolderPath = path.join(path.dirname(showFolderPath), newShowFolderName); + + try { + fs.renameSync(showFolderPath, newShowFolderPath); + writeLog(`[BEGIN-MAPPING] Renamed show folder to: ${newShowFolderName}`); + newDirectory = path.join(newShowFolderPath, seasonFolder); + writeLog(`[BEGIN-MAPPING] New directory path: ${newDirectory}`); + } catch (renameErr) { + writeLog(`[BEGIN-MAPPING] Could not rename show folder: ${renameErr.message}`); + } + } + + writeLog(`[BEGIN-MAPPING] Complete. Success: ${successCount}, Errors: ${errorCount}`); + return { success: true, renamed: successCount, errors: errorCount, newDirectory: newDirectory }; + } catch (error) { + writeLog(`[BEGIN-MAPPING] Critical error: ${error.message}`); + return { success: false, error: error.message }; + } +}); + // Function to test moving files (for debugging purposes) function testMoveFunction() { // This is for testing the function directly from main process diff --git a/renderer.js b/renderer.js index 0258d66..b47daf4 100644 --- a/renderer.js +++ b/renderer.js @@ -27,6 +27,9 @@ searchInput.addEventListener('input', debounce(searchShows, 300)); // Add click handler for the floating circle (play button) to move all tagged files document.getElementById('tagged-circle').addEventListener('click', moveAllTaggedFiles); +// Add click handler for Begin Mapping button +document.getElementById('begin-mapping-btn').addEventListener('click', beginMapping); + // Listen for scan progress updates from main process ipcRenderer.on('scan-progress', (event, { current, total, fileName }) => { updateProgress(current, total, fileName); @@ -631,6 +634,72 @@ async function moveAllTaggedFiles() { } } +// Function to begin mapping files to Jellyfin naming convention +async function beginMapping() { + const btn = document.getElementById('begin-mapping-btn'); + + // Get all media file items (not folders) + const fileItems = document.querySelectorAll('.file-item:not(.folder-item)'); + + if (fileItems.length === 0) { + alert('No media files to map. Please select a directory first.'); + return; + } + + if (!currentDirectory) { + alert('No directory selected.'); + return; + } + + // Disable button during processing + btn.disabled = true; + btn.textContent = 'Mapping...'; + + // Collect file data with episode numbers from UI order + const filesToMap = []; + fileItems.forEach((item, index) => { + const fileNameEl = item.querySelector('.file-name'); + const qualityEl = item.querySelector('.file-quality'); + const episodeNumEl = item.querySelector('.episode-number'); + + if (fileNameEl && fileNameEl.dataset.filePath) { + filesToMap.push({ + filePath: fileNameEl.dataset.filePath, + quality: qualityEl ? qualityEl.textContent : '', + episodeNumber: episodeNumEl ? parseInt(episodeNumEl.textContent) : (index + 1) + }); + } + }); + + // Get TVDB ID from current show if selected + const tvdbId = currentShow ? currentShow.id : null; + + try { + const result = await ipcRenderer.invoke('begin-mapping', { + directory: currentDirectory, + files: filesToMap, + tvdbId: tvdbId + }); + + if (result.success) { + alert('Mapping Complete!'); + // Update currentDirectory if it changed (show folder was renamed) + const newDir = result.newDirectory || currentDirectory; + currentDirectory = newDir; + // Refresh the file list to show new names + openDirectory(newDir); + } else { + alert('Mapping failed: ' + (result.error || 'Unknown error')); + } + } catch (error) { + console.error('Mapping error:', error); + alert('Mapping failed: ' + error.message); + } finally { + btn.disabled = false; + btn.textContent = 'Begin Mapping'; + } +} + // Function to display created folders in UI function displayCreatedFolder(folderName) { // Check if folder entry already exists