attempts at move to extra

This commit is contained in:
Jarian Cottingham 2026-02-19 09:47:06 -06:00
parent 12035cc4d8
commit d3aac3261d
4 changed files with 470 additions and 23 deletions

View File

@ -164,34 +164,126 @@
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<h1>Movie Mapper</h1>
<div class="controls">
<button id="select-dir-btn">Select Directory</button>
<div id="selected-dir">No directory selected</div>
<div class="container">
<div class="sidebar">
<h1>Movie Mapper</h1>
<div class="controls">
<button id="select-dir-btn">Select Directory</button>
<div id="selected-dir">No directory selected</div>
</div>
<div class="search-section">
<input type="text" id="search-input" placeholder="Search TV shows...">
<div id="search-results"></div>
</div>
<div id="show-details" style="margin-top: 20px; padding: 15px; border-top: 1px solid #eee;">
<h3 id="show-title" style="margin-top: 0; color: #333;"></h3>
<div id="seasons-container"></div>
</div>
</div>
<div class="search-section">
<input type="text" id="search-input" placeholder="Search TV shows...">
<div id="search-results"></div>
</div>
<div id="show-details" style="margin-top: 20px; padding: 15px; border-top: 1px solid #eee;">
<h3 id="show-title" style="margin-top: 0; color: #333;"></h3>
<div id="seasons-container"></div>
</div>
</div>
<div class="main-content">
<div class="file-list">
<div id="file-list">
<p>Click "Select Directory" to start browsing media files.</p>
<div class="main-content">
<div class="file-list">
<div id="file-list">
<p>Click "Select Directory" to start browsing media files.</p>
</div>
</div>
</div>
</div>
</div>
<style>
/* Add some additional styling to better align the views */
.file-list-container {
display: flex;
flex-direction: column;
height: 100%;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.file-list-header h3 {
margin: 0;
color: #333;
}
.episode-info {
margin-top: 15px;
padding: 12px;
background-color: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 6px;
font-size: 14px;
}
.episode-info strong {
color: #007bff;
}
.file-item-with-episode {
position: relative;
}
.episode-match-indicator {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background-color: #28a745;
color: white;
border-radius: 12px;
padding: 2px 8px;
font-size: 10px;
display: none;
}
.file-item-with-episode:hover .episode-match-indicator {
display: block;
}
/* Tag styling */
.file-tags {
display: flex;
gap: 8px;
align-items: center;
opacity: 0.7;
transition: opacity 0.2s;
}
.file-item:hover .file-tags {
opacity: 1;
}
.tag-icon {
cursor: pointer;
font-size: 16px;
opacity: 0.7;
transition: all 0.2s;
padding: 4px;
border-radius: 4px;
}
.tag-icon:hover {
opacity: 1;
transform: scale(1.1);
}
.extra-tag {
color: #ffc107;
}
.commentary-tag {
color: #17a2b8;
}
</style>
<script src="renderer.js"></script>
</body>

41
main.js
View File

@ -454,6 +454,47 @@ ipcMain.handle('test-tvdb-api', async () => {
}
});
// IPC handler for moving file to extras folder
ipcMain.handle('move-to-extras', async (event, filePath) => {
try {
const fs = require('fs');
const path = require('path');
// Check if file exists
if (!fs.existsSync(filePath)) {
return { success: false, error: 'File does not exist' };
}
// Get the directory of the file
const fileDir = path.dirname(filePath);
const extrasDir = path.join(fileDir, 'extras');
// Create extras directory if it doesn't exist
if (!fs.existsSync(extrasDir)) {
fs.mkdirSync(extrasDir, { recursive: true });
}
// Get the filename
const fileName = path.basename(filePath);
// Construct the destination path
const destPath = path.join(extrasDir, fileName);
// Move the file
fs.renameSync(filePath, destPath);
return {
success: true,
message: 'File moved to extras folder successfully',
sourcePath: filePath,
destinationPath: destPath
};
} catch (error) {
console.error('Error moving file to extras:', error);
return { success: false, error: error.message };
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.whenReady().then(createWindow);

View File

@ -94,6 +94,13 @@ 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
});
fileListEl.appendChild(fileItem);
});
}
@ -160,6 +167,106 @@ 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();
}
}
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();
}
});
}
});
}
// Initialize context menu when the app loads
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded - adding context menu');
addFileContextMenu();
});
// Search shows function
async function searchShows() {
const query = searchInput.value.trim();
@ -435,6 +542,114 @@ function displayEpisodes(episodes) {
});
seasonsContainer.appendChild(episodesTable);
// Also update the main file list to show episode matching information
updateFileListWithEpisodeInfo(episodes);
}
// Enhanced function to create a more integrated view
function createIntegratedEpisodeFileView(episodes) {
// This function would create a more integrated view showing both files and episodes
// For now, we're enhancing the existing functionality to better align the views
// Add a section that shows how files might align with episodes
const fileListContainer = document.getElementById('file-list');
// Create a section showing the relationship
const relationshipSection = document.createElement('div');
relationshipSection.style.marginTop = '15px';
relationshipSection.style.padding = '12px';
relationshipSection.style.backgroundColor = '#fff8e1';
relationshipSection.style.border = '1px solid #ffd54f';
relationshipSection.style.borderRadius = '6px';
relationshipSection.innerHTML = `
<h4 style="margin-top: 0; color: #ff9800;">File-Episode Relationship</h4>
<p style="font-size: 14px; margin: 5px 0;">
<strong>Matching Strategy:</strong> Files are matched to episodes based on naming patterns.
</p>
<p style="font-size: 14px; margin: 5px 0;">
<strong>Example:</strong> "Show.S01E01.Title.mp4" matches Episode 1 of Season 1.
</p>
<p style="font-size: 14px; margin: 5px 0;">
<strong>Status:</strong> ${currentFiles.length} files in directory, ${episodes.length} episodes available.
</p>
`;
// Insert this section after the episode info
const episodeInfo = fileListContainer.querySelector('.episode-info');
if (episodeInfo) {
fileListContainer.insertBefore(relationshipSection, episodeInfo.nextSibling);
}
}
// Update file list to show episode matching information
function updateFileListWithEpisodeInfo(episodes) {
// When we have episodes, we want to show basic episode information
if (currentFiles && currentFiles.length > 0 && episodes && episodes.length > 0) {
console.log('Updating file list with episode info for', episodes.length, 'episodes');
// Create a simple note about episode availability
const fileListContainer = document.getElementById('file-list');
// Add a simple note about episodes
const episodeNote = document.createElement('div');
episodeNote.style.marginTop = '15px';
episodeNote.style.padding = '10px';
episodeNote.style.backgroundColor = '#e7f3ff';
episodeNote.style.border = '1px solid #b3d9ff';
episodeNote.style.borderRadius = '5px';
episodeNote.style.fontSize = '14px';
episodeNote.innerHTML = `
<strong>Episode Information:</strong>
${episodes.length} episodes available for this show.
<br><small>Episode details are displayed in the sidebar.</small>
`;
// Add this note to the file list container
if (fileListContainer.firstChild) {
fileListContainer.insertBefore(episodeNote, fileListContainer.firstChild);
} else {
fileListContainer.appendChild(episodeNote);
}
}
}
// 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

99
test-api.js Normal file
View File

@ -0,0 +1,99 @@
// Simple test script to verify TVDB API functionality
const axios = require('axios');
const dotenv = require('dotenv');
// Load environment variables
dotenv.config();
async function testTVDBAPI() {
try {
console.log('Testing TVDB API connectivity...');
// Get API key from environment
const apiKey = process.env.TVDB_API_KEY || process.env.API_KEY;
if (!apiKey) {
throw new Error('TVDB_API_KEY not found in environment variables');
}
// First get a token
const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', {
apikey: apiKey
}, {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
console.log('Login successful:', loginResponse.data.status);
if (loginResponse.data && loginResponse.data.data && loginResponse.data.data.token) {
const token = loginResponse.data.data.token;
console.log('Got token:', token.substring(0, 50) + '...');
// Test search with the token
const searchResponse = await axios.get('https://api4.thetvdb.com/v4/search', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
params: {
query: 'IGPX'
}
});
console.log('Search response:', searchResponse.data);
if (searchResponse.data && searchResponse.data.data) {
console.log('Found shows:', searchResponse.data.data.length);
searchResponse.data.data.forEach(show => {
console.log(`- ${show.name} (ID: ${show.id})`);
});
}
// Test getting show details with extended info
const showId = '73011'; // IGPX Immortal Grand Prix ID
const showResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${showId}/extended`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
console.log('Show details response:', showResponse.data);
// Test getting episodes for the show
const episodesResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${showId}/episodes`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
params: {
page: 0
}
});
console.log('Episodes response structure:', {
data: episodesResponse.data.data ? 'has data' : 'no data',
total: episodesResponse.data.data?.total,
episodes: episodesResponse.data.data?.episodes ? episodesResponse.data.data.episodes.length : 0
});
if (episodesResponse.data && episodesResponse.data.data && episodesResponse.data.data.episodes) {
console.log('First few episodes:');
episodesResponse.data.data.episodes.slice(0, 5).forEach(episode => {
console.log(`- Episode ${episode.number}: ${episode.name} (Season ${episode.seasonNumber})`);
});
}
}
} catch (error) {
console.error('API Test Error:', error.message);
console.error('Error details:', error.response?.data || error);
}
}
testTVDBAPI();