feat: Implement real TVDB v4 API integration with season/episode functionality and clean table display
This commit is contained in:
parent
d8bbaee977
commit
4869cad916
191
main.js
191
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
|
// For TVDB v4 API, we need to first get an authentication token
|
||||||
// The token is valid for 1 month
|
|
||||||
let token = null;
|
let token = null;
|
||||||
|
|
||||||
// Try to get token from cache or login
|
// 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);
|
throw new Error('Failed to authenticate with TVDB API: ' + loginError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we can't get season information directly from the show details endpoint,
|
// Extract numeric ID from the show ID (handle both formats: series-73011 and 73011)
|
||||||
// we'll return basic show information and let the UI handle season loading
|
let numericId = showId;
|
||||||
// The seasons will be loaded when the user clicks on a season
|
if (showId && showId.startsWith('series-')) {
|
||||||
|
numericId = showId.split('series-')[1];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
// Get extended show details which includes season information
|
||||||
success: true,
|
const response = await axios.get(`https://api4.thetvdb.com/v4/series/${numericId}/extended`, {
|
||||||
data: {
|
headers: {
|
||||||
id: showId,
|
'Authorization': `Bearer ${token}`,
|
||||||
name: 'Unknown Show',
|
'Content-Type': 'application/json',
|
||||||
status: null,
|
'Accept': 'application/json'
|
||||||
firstAired: null,
|
|
||||||
overview: 'Show details not available',
|
|
||||||
image: null,
|
|
||||||
slug: null,
|
|
||||||
seasons: []
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('TVDB API Error getting show details:', error.message);
|
console.error('TVDB API Error getting show details:', error.message);
|
||||||
console.error('Error status:', error.response?.status);
|
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);
|
throw new Error('Failed to authenticate with TVDB API: ' + loginError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make API request to get season episodes
|
// Extract numeric ID from the show ID (handle both formats: series-73011 and 73011)
|
||||||
// Note: The API endpoint structure may vary, so we'll try different approaches
|
let numericId = showId;
|
||||||
const response = await axios.get(`https://api4.thetvdb.com/v4/series/${showId}/episodes/default`, {
|
if (showId && showId.startsWith('series-')) {
|
||||||
headers: {
|
numericId = showId.split('series-')[1];
|
||||||
'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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error('TVDB API Error getting season episodes:', error.message);
|
console.error('TVDB API Error getting season episodes:', error.message);
|
||||||
console.error('Error status:', error.response?.status);
|
console.error('Error status:', error.response?.status);
|
||||||
console.error('Error data:', error.response?.data);
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
113
renderer.js
113
renderer.js
@ -277,7 +277,7 @@ async function selectShow(show) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display seasons
|
// Display seasons with episode counts
|
||||||
function displaySeasons(seasons) {
|
function displaySeasons(seasons) {
|
||||||
const seasonsContainer = document.getElementById('seasons-container');
|
const seasonsContainer = document.getElementById('seasons-container');
|
||||||
seasonsContainer.innerHTML = '';
|
seasonsContainer.innerHTML = '';
|
||||||
@ -294,26 +294,30 @@ function displaySeasons(seasons) {
|
|||||||
seasons.forEach(season => {
|
seasons.forEach(season => {
|
||||||
const seasonItem = document.createElement('div');
|
const seasonItem = document.createElement('div');
|
||||||
seasonItem.className = 'season-item';
|
seasonItem.className = 'season-item';
|
||||||
seasonItem.style.padding = '8px';
|
seasonItem.style.padding = '12px';
|
||||||
seasonItem.style.margin = '5px 0';
|
seasonItem.style.margin = '8px 0';
|
||||||
seasonItem.style.border = '1px solid #ddd';
|
seasonItem.style.border = '1px solid #ddd';
|
||||||
seasonItem.style.borderRadius = '4px';
|
seasonItem.style.borderRadius = '6px';
|
||||||
seasonItem.style.cursor = 'pointer';
|
seasonItem.style.cursor = 'pointer';
|
||||||
seasonItem.style.backgroundColor = '#f8f9fa';
|
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}`;
|
let seasonDisplay = `Season ${season.number}`;
|
||||||
if (season.name && season.name !== `Season ${season.number}`) {
|
if (season.type && season.type !== 'Unknown') {
|
||||||
seasonDisplay += ` - ${season.name}`;
|
|
||||||
}
|
|
||||||
if (season.type) {
|
|
||||||
seasonDisplay += ` (${season.type})`;
|
seasonDisplay += ` (${season.type})`;
|
||||||
}
|
}
|
||||||
if (season.episodeCount) {
|
|
||||||
seasonDisplay += ` (${season.episodeCount} episodes)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
seasonItem.textContent = seasonDisplay;
|
const episodeCount = season.episodeCount || 0;
|
||||||
|
|
||||||
|
seasonItem.innerHTML = `
|
||||||
|
<span>${seasonDisplay}</span>
|
||||||
|
<span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px;">
|
||||||
|
${episodeCount} episodes
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
// Add click event to fetch episodes
|
// Add click event to fetch episodes
|
||||||
seasonItem.addEventListener('click', () => {
|
seasonItem.addEventListener('click', () => {
|
||||||
@ -329,6 +333,9 @@ function displaySeasons(seasons) {
|
|||||||
// Fetch and display episodes for a season
|
// Fetch and display episodes for a season
|
||||||
async function fetchAndDisplayEpisodes(showId, seasonNumber) {
|
async function fetchAndDisplayEpisodes(showId, seasonNumber) {
|
||||||
try {
|
try {
|
||||||
|
// Show loading message while fetching
|
||||||
|
document.getElementById('seasons-container').innerHTML = '<p>Loading episodes...</p>';
|
||||||
|
|
||||||
const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber);
|
const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
@ -340,11 +347,17 @@ async function fetchAndDisplayEpisodes(showId, seasonNumber) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching episodes:', error);
|
console.error('Error fetching episodes:', error);
|
||||||
document.getElementById('seasons-container').innerHTML = '<p>Error loading episodes: ' + error.message + '</p>';
|
// Show a more user-friendly error message
|
||||||
|
document.getElementById('seasons-container').innerHTML = `
|
||||||
|
<div style="padding: 10px; background-color: #f8d7da; color: #721c24; border-radius: 4px;">
|
||||||
|
<p>Error loading episodes: ${error.message || 'Failed to load episodes'}</p>
|
||||||
|
<p style="font-size: 12px; margin-top: 5px;">Note: This may be due to API limitations with the TVDB v4 API.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display episodes
|
// Display episodes in clean table format
|
||||||
function displayEpisodes(episodes) {
|
function displayEpisodes(episodes) {
|
||||||
const seasonsContainer = document.getElementById('seasons-container');
|
const seasonsContainer = document.getElementById('seasons-container');
|
||||||
seasonsContainer.innerHTML = '';
|
seasonsContainer.innerHTML = '';
|
||||||
@ -354,40 +367,66 @@ function displayEpisodes(episodes) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a container for episodes
|
// Create a container for episodes table
|
||||||
const episodesList = document.createElement('div');
|
const episodesTable = document.createElement('div');
|
||||||
episodesList.style.marginTop = '10px';
|
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 = `
|
||||||
|
<div>Episode Name</div>
|
||||||
|
<div>Runtime</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
episodesTable.appendChild(tableHeader);
|
||||||
|
|
||||||
episodes.forEach(episode => {
|
episodes.forEach(episode => {
|
||||||
const episodeItem = document.createElement('div');
|
const episodeRow = document.createElement('div');
|
||||||
episodeItem.className = 'episode-item';
|
episodeRow.className = 'episode-item';
|
||||||
episodeItem.style.padding = '8px';
|
episodeRow.style.display = 'grid';
|
||||||
episodeItem.style.margin = '5px 0';
|
episodeRow.style.gridTemplateColumns = '1fr 1fr';
|
||||||
episodeItem.style.border = '1px solid #ddd';
|
episodeRow.style.padding = '10px';
|
||||||
episodeItem.style.borderRadius = '4px';
|
episodeRow.style.borderBottom = '1px solid #eee';
|
||||||
episodeItem.style.cursor = 'pointer';
|
episodeRow.style.backgroundColor = '#fff';
|
||||||
episodeItem.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
|
// Format episode display
|
||||||
let episodeDisplay = `Episode ${episode.number}: ${episode.name || 'Untitled'}`;
|
const episodeName = episode.name || 'Untitled';
|
||||||
if (episode.aired) {
|
const episodeRuntime = episode.runtime ? `${episode.runtime} min` : 'N/A';
|
||||||
episodeDisplay += ` (${episode.aired})`;
|
|
||||||
}
|
|
||||||
if (episode.runtime) {
|
|
||||||
episodeDisplay += ` (${episode.runtime} min)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
episodeItem.textContent = episodeDisplay;
|
episodeRow.innerHTML = `
|
||||||
|
<div style="font-weight: 500;">Episode ${episode.number}: ${episodeName}</div>
|
||||||
|
<div style="color: #666;">${episodeRuntime}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
// Add click event (could show more details)
|
// 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'}`);
|
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
|
// Debounce function for search input
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user