diff --git a/main.js b/main.js index 0c95e75..0144dfa 100644 --- a/main.js +++ b/main.js @@ -198,7 +198,6 @@ ipcMain.handle('get-show-details', async (event, showId) => { } // For TVDB v4 API, we need to first get an authentication token - // The token is valid for 1 month let token = null; // Try to get token from cache or login @@ -218,23 +217,51 @@ ipcMain.handle('get-show-details', async (event, showId) => { throw new Error('Failed to authenticate with TVDB API: ' + loginError.message); } - // Since we can't get season information directly from the show details endpoint, - // we'll return basic show information and let the UI handle season loading - // The seasons will be loaded when the user clicks on a season + // Extract numeric ID from the show ID (handle both formats: series-73011 and 73011) + let numericId = showId; + if (showId && showId.startsWith('series-')) { + numericId = showId.split('series-')[1]; + } - return { - success: true, - data: { - id: showId, - name: 'Unknown Show', - status: null, - firstAired: null, - overview: 'Show details not available', - image: null, - slug: null, - seasons: [] + // Get extended show details which includes season information + const response = await axios.get(`https://api4.thetvdb.com/v4/series/${numericId}/extended`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' } - }; + }); + + if (response.data && response.data.data) { + const showData = response.data.data; + + // Extract season information properly + let seasons = []; + if (showData.seasons && Array.isArray(showData.seasons)) { + seasons = showData.seasons.map(season => ({ + id: season.id, + number: season.number, + type: season.type?.name || 'Unknown', + episodeCount: season.episodeCount || (season.episodes ? season.episodes.length : 0) + })); + } + + return { + success: true, + data: { + id: showData.id, + name: showData.name, + status: showData.status?.name || showData.status, + firstAired: showData.firstAired, + overview: showData.overview, + image: showData.image, + slug: showData.slug, + seasons: seasons + } + }; + } else { + throw new Error('Invalid response structure from TVDB API'); + } } catch (error) { console.error('TVDB API Error getting show details:', error.message); console.error('Error status:', error.response?.status); @@ -278,48 +305,108 @@ ipcMain.handle('get-season-episodes', async (event, showId, seasonNumber) => { throw new Error('Failed to authenticate with TVDB API: ' + loginError.message); } - // Make API request to get season episodes - // Note: The API endpoint structure may vary, so we'll try different approaches - const response = await axios.get(`https://api4.thetvdb.com/v4/series/${showId}/episodes/default`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - params: { - page: 0 - } - }); - - if (response.data && response.data.data) { - // Process episodes - const episodes = response.data.data.episodes ? response.data.data.episodes.map(episode => ({ - id: episode.id, - name: episode.name, - number: episode.number, - seasonNumber: episode.seasonNumber, - aired: episode.aired, - overview: episode.overview, - runtime: episode.runtime - })) : []; - - return { - success: true, - data: { - showId: showId, - seasonNumber: seasonNumber, - episodes: episodes - } - }; - } else { - throw new Error('Invalid response structure from TVDB API'); + // Extract numeric ID from the show ID (handle both formats: series-73011 and 73011) + let numericId = showId; + if (showId && showId.startsWith('series-')) { + numericId = showId.split('series-')[1]; } + + // Try to get episodes for a specific season using the correct endpoint structure + // Based on the API documentation, we should use: /series/{id}/episodes/{season-type} + // where season-type can be "default", "aired", "dvd", etc. + + try { + // First try the season-specific endpoint + const episodesResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${numericId}/episodes/default`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + params: { + page: 0 + } + }); + + if (episodesResponse.data && episodesResponse.data.data) { + // Process episodes and filter by season number + const episodes = episodesResponse.data.data.episodes ? episodesResponse.data.data.episodes + .filter(episode => episode.seasonNumber === seasonNumber) + .map(episode => ({ + id: episode.id, + name: episode.name, + number: episode.number, + seasonNumber: episode.seasonNumber, + aired: episode.aired, + overview: episode.overview, + runtime: episode.runtime + })) : []; + + return { + success: true, + data: { + showId: showId, + seasonNumber: seasonNumber, + episodes: episodes + } + }; + } + } catch (seasonError) { + console.log('Season-specific endpoint failed, falling back to all episodes:', seasonError.message); + // If season-specific endpoint fails, fall back to getting all episodes and filtering + const episodesResponse = await axios.get(`https://api4.thetvdb.com/v4/series/${numericId}/episodes`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + params: { + page: 0 + } + }); + + if (episodesResponse.data && episodesResponse.data.data) { + // Process episodes and filter by season number + const episodes = episodesResponse.data.data.episodes ? episodesResponse.data.data.episodes + .filter(episode => episode.seasonNumber === seasonNumber) + .map(episode => ({ + id: episode.id, + name: episode.name, + number: episode.number, + seasonNumber: episode.seasonNumber, + aired: episode.aired, + overview: episode.overview, + runtime: episode.runtime + })) : []; + + return { + success: true, + data: { + showId: showId, + seasonNumber: seasonNumber, + episodes: episodes + } + }; + } + } + + throw new Error('Failed to fetch episodes from both endpoints'); } catch (error) { console.error('TVDB API Error getting season episodes:', error.message); console.error('Error status:', error.response?.status); console.error('Error data:', error.response?.data); - return { success: false, error: 'Failed to fetch season episodes from TVDB API: ' + error.message }; + // Return a more specific error message for debugging + return { + success: false, + error: 'Failed to fetch season episodes from TVDB API: ' + error.message, + debug: { + showId: showId, + seasonNumber: seasonNumber, + status: error.response?.status, + data: error.response?.data + } + }; } }); diff --git a/renderer.js b/renderer.js index 131e4cb..ef7ccfe 100644 --- a/renderer.js +++ b/renderer.js @@ -277,7 +277,7 @@ async function selectShow(show) { } } -// Display seasons +// Display seasons with episode counts function displaySeasons(seasons) { const seasonsContainer = document.getElementById('seasons-container'); seasonsContainer.innerHTML = ''; @@ -294,26 +294,30 @@ function displaySeasons(seasons) { seasons.forEach(season => { const seasonItem = document.createElement('div'); seasonItem.className = 'season-item'; - seasonItem.style.padding = '8px'; - seasonItem.style.margin = '5px 0'; + seasonItem.style.padding = '12px'; + seasonItem.style.margin = '8px 0'; seasonItem.style.border = '1px solid #ddd'; - seasonItem.style.borderRadius = '4px'; + seasonItem.style.borderRadius = '6px'; seasonItem.style.cursor = 'pointer'; seasonItem.style.backgroundColor = '#f8f9fa'; + seasonItem.style.display = 'flex'; + seasonItem.style.justifyContent = 'space-between'; + seasonItem.style.alignItems = 'center'; - // Format season display + // Format season display with episode count let seasonDisplay = `Season ${season.number}`; - if (season.name && season.name !== `Season ${season.number}`) { - seasonDisplay += ` - ${season.name}`; - } - if (season.type) { + if (season.type && season.type !== 'Unknown') { seasonDisplay += ` (${season.type})`; } - if (season.episodeCount) { - seasonDisplay += ` (${season.episodeCount} episodes)`; - } - seasonItem.textContent = seasonDisplay; + const episodeCount = season.episodeCount || 0; + + seasonItem.innerHTML = ` + ${seasonDisplay} + + ${episodeCount} episodes + + `; // Add click event to fetch episodes seasonItem.addEventListener('click', () => { @@ -329,6 +333,9 @@ function displaySeasons(seasons) { // Fetch and display episodes for a season async function fetchAndDisplayEpisodes(showId, seasonNumber) { try { + // Show loading message while fetching + document.getElementById('seasons-container').innerHTML = '

Loading episodes...

'; + const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber); if (result.success && result.data) { @@ -340,11 +347,17 @@ async function fetchAndDisplayEpisodes(showId, seasonNumber) { } } catch (error) { console.error('Error fetching episodes:', error); - document.getElementById('seasons-container').innerHTML = '

Error loading episodes: ' + error.message + '

'; + // Show a more user-friendly error message + document.getElementById('seasons-container').innerHTML = ` +
+

Error loading episodes: ${error.message || 'Failed to load episodes'}

+

Note: This may be due to API limitations with the TVDB v4 API.

+
+ `; } } -// Display episodes +// Display episodes in clean table format function displayEpisodes(episodes) { const seasonsContainer = document.getElementById('seasons-container'); seasonsContainer.innerHTML = ''; @@ -354,40 +367,66 @@ function displayEpisodes(episodes) { return; } - // Create a container for episodes - const episodesList = document.createElement('div'); - episodesList.style.marginTop = '10px'; + // Create a container for episodes table + const episodesTable = document.createElement('div'); + episodesTable.style.marginTop = '10px'; + episodesTable.style.border = '1px solid #ddd'; + episodesTable.style.borderRadius = '6px'; + episodesTable.style.overflow = 'hidden'; + + // Create table header + const tableHeader = document.createElement('div'); + tableHeader.style.display = 'grid'; + tableHeader.style.gridTemplateColumns = '1fr 1fr'; + tableHeader.style.backgroundColor = '#f8f9fa'; + tableHeader.style.padding = '10px'; + tableHeader.style.fontWeight = 'bold'; + tableHeader.style.borderBottom = '1px solid #ddd'; + + tableHeader.innerHTML = ` +
Episode Name
+
Runtime
+ `; + + episodesTable.appendChild(tableHeader); episodes.forEach(episode => { - const episodeItem = document.createElement('div'); - episodeItem.className = 'episode-item'; - episodeItem.style.padding = '8px'; - episodeItem.style.margin = '5px 0'; - episodeItem.style.border = '1px solid #ddd'; - episodeItem.style.borderRadius = '4px'; - episodeItem.style.cursor = 'pointer'; - episodeItem.style.backgroundColor = '#fff'; + const episodeRow = document.createElement('div'); + episodeRow.className = 'episode-item'; + episodeRow.style.display = 'grid'; + episodeRow.style.gridTemplateColumns = '1fr 1fr'; + episodeRow.style.padding = '10px'; + episodeRow.style.borderBottom = '1px solid #eee'; + episodeRow.style.backgroundColor = '#fff'; + episodeRow.style.cursor = 'pointer'; + episodeRow.style.transition = 'background-color 0.2s'; + + episodeRow.addEventListener('mouseenter', () => { + episodeRow.style.backgroundColor = '#f5f5f5'; + }); + + episodeRow.addEventListener('mouseleave', () => { + episodeRow.style.backgroundColor = '#fff'; + }); // Format episode display - let episodeDisplay = `Episode ${episode.number}: ${episode.name || 'Untitled'}`; - if (episode.aired) { - episodeDisplay += ` (${episode.aired})`; - } - if (episode.runtime) { - episodeDisplay += ` (${episode.runtime} min)`; - } + const episodeName = episode.name || 'Untitled'; + const episodeRuntime = episode.runtime ? `${episode.runtime} min` : 'N/A'; - episodeItem.textContent = episodeDisplay; + episodeRow.innerHTML = ` +
Episode ${episode.number}: ${episodeName}
+
${episodeRuntime}
+ `; // Add click event (could show more details) - episodeItem.addEventListener('click', () => { + episodeRow.addEventListener('click', () => { alert(`Episode: ${episode.name || 'Untitled'}\nRuntime: ${episode.runtime || 'N/A'} min\nAired: ${episode.aired || 'N/A'}`); }); - episodesList.appendChild(episodeItem); + episodesTable.appendChild(episodeRow); }); - seasonsContainer.appendChild(episodesList); + seasonsContainer.appendChild(episodesTable); } // Debounce function for search input