MovieMapper/utils/renderer/SearchManager.js
Jarian Cottingham be887c3855 Refactor renderer.js into modular class-based structure
- Split large renderer.js file into 8 separate class files:
  - AppState.js: Manages application state
  - FileListManager.js: Handles file list display and manipulation
  - TagManager.js: Manages file tagging functionality
  - EpisodeManager.js: Handles episode number editing and highlighting
  - SearchManager.js: Manages TVDB search and show selection
  - FileManager.js: Handles file operations
  - ModalManager.js: Manages video preview modal
  - ProgressManager.js: Manages progress display
  - UIManager.js: Main coordinator for UI functionality
- Follows SOLID principles and single responsibility
- Improves code maintainability and testability
- All syntax verified with node --check
2026-02-25 02:32:08 -06:00

333 lines
11 KiB
JavaScript

const { ipcRenderer } = require('electron');
/**
* SearchManager - Manages TVDB search and show selection
*/
class SearchManager {
constructor(searchInputEl, searchResultsEl) {
this.searchInputEl = searchInputEl;
this.searchResultsEl = searchResultsEl;
this.currentShow = null;
}
/**
* Search shows by query
* @param {string} query - Search query
* @returns {Promise<Object>} Search results
*/
async searchShows(query) {
if (query.length < 2) {
this.clearSearchResults();
return { success: true, results: [] };
}
try {
const result = await ipcRenderer.invoke('search-tvdb', query);
if (result.success) {
this.displaySearchResults(result.results);
return result;
} else {
throw new Error(result.error);
}
} catch (error) {
console.error('Error searching shows:', error);
this.searchResultsEl.innerHTML = `<p>Error: ${error.message}</p>`;
return { success: false, error: error.message };
}
}
/**
* Display search results
* @param {Array} results - Search results array
*/
displaySearchResults(results) {
this.searchResultsEl.innerHTML = '';
if (results.length === 0) {
this.searchResultsEl.innerHTML = '<p style="color: #888; text-align: center; padding: 10px;">No shows found.</p>';
return;
}
results.forEach(show => {
const resultItem = document.createElement('div');
resultItem.className = 'search-result-item';
// Create a more detailed display with separate elements
const showName = show.seriesName || show.name;
const showYear = show.firstAired ? show.firstAired.split('-')[0] : '';
resultItem.innerHTML = `
<div class="show-name">${showName}</div>
${showYear ? `<div class="show-year">${showYear}</div>` : ''}
`;
resultItem.addEventListener('click', () => this.selectShow(show));
this.searchResultsEl.appendChild(resultItem);
});
}
/**
* Clear search results
*/
clearSearchResults() {
this.searchResultsEl.innerHTML = '';
}
/**
* Select a show
* @param {Object} show - Show object
* @param {Function} onShowTitleUpdate - Callback to update show title
* @param {Function} onSeasonsDisplay - Callback to display seasons
*/
async selectShow(show, onShowTitleUpdate, onSeasonsDisplay) {
console.log('Selected show:', show);
this.currentShow = show;
// Clear search results when a show is selected
this.clearSearchResults();
// Display show title
if (onShowTitleUpdate) {
onShowTitleUpdate(show.seriesName || show.name);
}
// Fetch show details including seasons
try {
const result = await ipcRenderer.invoke('get-show-details', show.id);
if (result.success && result.data) {
const showData = result.data;
const seasons = showData.seasons || [];
// Display seasons
if (onSeasonsDisplay) {
if (seasons.length === 0) {
onSeasonsDisplay('<p>Seasons will be loaded when you click on a season.</p>');
} else {
onSeasonsDisplay(seasons);
}
}
} else {
console.error('Failed to fetch show details:', result.error);
if (onSeasonsDisplay) {
onSeasonsDisplay('<p>Error loading seasons: ' + (result.error || 'Unknown error') + '</p>');
}
}
} catch (error) {
console.error('Error fetching show details:', error);
if (onSeasonsDisplay) {
onSeasonsDisplay('<p>Error loading seasons: ' + error.message + '</p>');
}
}
}
/**
* Display seasons
* @param {Array} seasons - Seasons array
* @param {Function} onEpisodeFetch - Callback to fetch episodes
* @param {Function} onBackToSeasons - Callback to go back to seasons
*/
displaySeasons(seasons, onEpisodeFetch, onBackToSeasons) {
const seasonsContainer = document.getElementById('seasons-container');
seasonsContainer.innerHTML = '';
if (!seasons || seasons.length === 0) {
seasonsContainer.innerHTML = '<p style="color: #888;">No seasons available.</p>';
return;
}
seasons.forEach(season => {
const seasonItem = document.createElement('div');
seasonItem.className = 'season-item';
// Format season display with episode count
let seasonDisplay = `Season ${season.number}`;
if (season.type && season.type !== 'Unknown') {
seasonDisplay += ` (${season.type})`;
}
const episodeCount = season.episodeCount || 0;
seasonItem.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>${seasonDisplay}</span>
<span style="background-color: #e94560; color: white; padding: 4px 8px; border-radius: 12px; font-size: 11px;">
${episodeCount} eps
</span>
</div>
`;
// Add click event to fetch episodes
seasonItem.addEventListener('click', () => {
// Remove selected class from all seasons
document.querySelectorAll('.season-item').forEach(item => item.classList.remove('selected'));
// Add selected class to clicked season
seasonItem.classList.add('selected');
if (onEpisodeFetch) {
onEpisodeFetch(season.number);
}
});
seasonsContainer.appendChild(seasonItem);
});
}
/**
* Fetch and display episodes
* @param {string} showId - Show ID
* @param {number} seasonNumber - Season number
* @param {Function} onEpisodesDisplay - Callback to display episodes
*/
async fetchAndDisplayEpisodes(showId, seasonNumber, onEpisodesDisplay) {
try {
// Show loading message while fetching
const seasonsContainer = document.getElementById('seasons-container');
seasonsContainer.innerHTML = '<p>Loading episodes...</p>';
const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber);
if (result.success && result.data) {
const episodes = result.data.episodes || [];
if (onEpisodesDisplay) {
onEpisodesDisplay(episodes);
}
} else {
console.error('Failed to fetch episodes:', result.error);
if (onEpisodesDisplay) {
onEpisodesDisplay(null, 'Error loading episodes: ' + (result.error || 'Unknown error'));
}
}
} catch (error) {
console.error('Error fetching episodes:', error);
// Show a more user-friendly error message
if (onEpisodesDisplay) {
onEpisodesDisplay(null, `Error loading episodes: ${error.message || 'Failed to load episodes'}`);
}
}
}
/**
* Display episodes
* @param {Array} episodes - Episodes array
* @param {number} episodeCount - Episode count
* @param {Function} onBackToSeasons - Callback to go back to seasons
* @param {Function} onFileListUpdate - Callback to update file list
*/
displayEpisodes(episodes, episodeCount, onBackToSeasons, onFileListUpdate) {
const seasonsContainer = document.getElementById('seasons-container');
seasonsContainer.innerHTML = '';
if (!episodes || episodes.length === 0) {
seasonsContainer.innerHTML = '<p style="color: #888;">No episodes available.</p>';
if (onBackToSeasons) onBackToSeasons(0);
return;
}
if (onBackToSeasons) {
onBackToSeasons(episodes.length);
}
// Create a back button to return to seasons
const backButton = document.createElement('div');
backButton.className = 'season-item';
backButton.innerHTML = '← Back to Seasons';
backButton.addEventListener('click', () => {
if (onBackToSeasons) onBackToSeasons(0);
if (onEpisodesDisplay) {
// Re-display seasons
}
});
seasonsContainer.appendChild(backButton);
// Create episodes container
const episodesContainer = document.createElement('div');
episodesContainer.id = 'episodes-container';
episodes.forEach(episode => {
const episodeRow = document.createElement('div');
episodeRow.className = 'episode-item';
// Format episode display
const episodeName = episode.name || 'Untitled';
const episodeRuntime = episode.runtime ? `${episode.runtime}m` : '';
episodeRow.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span><strong>E${episode.number}</strong> ${episodeName}</span>
${episodeRuntime ? `<span style="color: #888; font-size: 11px;">${episodeRuntime}</span>` : ''}
</div>
`;
episodesContainer.appendChild(episodeRow);
});
seasonsContainer.appendChild(episodesContainer);
// Also update the main file list to show episode matching information
if (onFileListUpdate) {
onFileListUpdate(episodes);
}
}
/**
* Update file list with episode info
* @param {Array} episodes - Episodes array
* @param {Array} currentFiles - Current files array
*/
updateFileListWithEpisodeInfo(episodes, currentFiles) {
// 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);
}
}
}
/**
* Test TVDB API connectivity
* @returns {Promise<Object>} Test result
*/
async testTVDBAPI() {
try {
const result = await ipcRenderer.invoke('test-tvdb-api');
console.log('TVDB API Test:', result);
if (result.success) {
console.log('TVDB API is working:', result.message);
alert('TVDB API Test Successful!\n' + result.message);
} else {
console.error('TVDB API test failed:', result.error);
alert('TVDB API Test Failed!\n' + result.error);
}
return result;
} catch (error) {
console.error('Error testing TVDB API:', error);
alert('Error testing TVDB API: ' + error.message);
return { success: false, error: error.message };
}
}
}
module.exports = SearchManager;