Add Begin Mapping feature with Jellyfin naming, fix tvdbid regex

This commit is contained in:
Jarian Cottingham 2026-02-22 03:08:51 -06:00
parent f9aee85e4f
commit 14c1bac5e8
3 changed files with 197 additions and 0 deletions

View File

@ -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 @@
<p>Click "Select Directory" to start browsing media files.</p>
</div>
</div>
<button id="begin-mapping-btn" class="begin-mapping-btn">Begin Mapping</button>
</div>
</div>

94
main.js
View File

@ -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

View File

@ -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