Implement enhanced TVDB v4 API functionality: show seasons and episodes with clickable navigation
This commit is contained in:
parent
8fd6df7d3e
commit
1cd7fab414
@ -178,6 +178,11 @@
|
|||||||
<div id="search-results"></div>
|
<div id="search-results"></div>
|
||||||
<button id="test-api-btn" style="margin-top: 10px; padding: 8px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; width: 100%;">Test TVDB API</button>
|
<button id="test-api-btn" style="margin-top: 10px; padding: 8px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; width: 100%;">Test TVDB API</button>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
|
|||||||
157
main.js
157
main.js
@ -184,6 +184,163 @@ ipcMain.handle('search-tvdb', async (event, query) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// IPC handler for getting show details including seasons
|
||||||
|
ipcMain.handle('get-show-details', async (event, showId) => {
|
||||||
|
try {
|
||||||
|
// Import required modules
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TVDB v4 API, we need to first get an authentication token
|
||||||
|
let token = null;
|
||||||
|
|
||||||
|
// Try to get token from cache or login
|
||||||
|
try {
|
||||||
|
const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', {
|
||||||
|
apikey: apiKey
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
token = loginResponse.data.data.token;
|
||||||
|
} catch (loginError) {
|
||||||
|
console.error('TVDB Login Error:', loginError.message);
|
||||||
|
throw new Error('Failed to authenticate with TVDB API: ' + loginError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make API request to get show extended details including seasons
|
||||||
|
const response = await axios.get(`https://api4.thetvdb.com/v4/series/${showId}/extended`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
const showData = response.data.data;
|
||||||
|
|
||||||
|
// Process seasons - TVDB v4 API returns seasons in the show data
|
||||||
|
const seasons = (showData.seasons || []).map(season => ({
|
||||||
|
id: season.id,
|
||||||
|
name: season.name,
|
||||||
|
number: season.number,
|
||||||
|
type: season.type?.name,
|
||||||
|
episodeCount: season.episodeCount
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: showData.id,
|
||||||
|
name: showData.name,
|
||||||
|
status: showData.status?.name,
|
||||||
|
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);
|
||||||
|
console.error('Error data:', error.response?.data);
|
||||||
|
|
||||||
|
return { success: false, error: 'Failed to fetch show details from TVDB API: ' + error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// IPC handler for getting season episodes
|
||||||
|
ipcMain.handle('get-season-episodes', async (event, showId, seasonNumber) => {
|
||||||
|
try {
|
||||||
|
// Import required modules
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TVDB v4 API, we need to first get an authentication token
|
||||||
|
let token = null;
|
||||||
|
|
||||||
|
// Try to get token from cache or login
|
||||||
|
try {
|
||||||
|
const loginResponse = await axios.post('https://api4.thetvdb.com/v4/login', {
|
||||||
|
apikey: apiKey
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
token = loginResponse.data.data.token;
|
||||||
|
} catch (loginError) {
|
||||||
|
console.error('TVDB Login Error:', loginError.message);
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
} 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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Test function to verify API connectivity
|
// Test function to verify API connectivity
|
||||||
ipcMain.handle('test-tvdb-api', async () => {
|
ipcMain.handle('test-tvdb-api', async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
144
renderer.js
144
renderer.js
@ -7,10 +7,14 @@ const selectedDirEl = document.getElementById('selected-dir');
|
|||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const searchResultsEl = document.getElementById('search-results');
|
const searchResultsEl = document.getElementById('search-results');
|
||||||
const fileListEl = document.getElementById('file-list');
|
const fileListEl = document.getElementById('file-list');
|
||||||
|
const showDetailsEl = document.getElementById('show-details');
|
||||||
|
|
||||||
// Current state
|
// Current state
|
||||||
let currentDirectory = null;
|
let currentDirectory = null;
|
||||||
let currentFiles = [];
|
let currentFiles = [];
|
||||||
|
let currentShow = null;
|
||||||
|
let currentSeasons = [];
|
||||||
|
let currentEpisodes = [];
|
||||||
|
|
||||||
// Event Listeners
|
// Event Listeners
|
||||||
selectDirBtn.addEventListener('click', selectDirectory);
|
selectDirBtn.addEventListener('click', selectDirectory);
|
||||||
@ -241,10 +245,144 @@ function displaySearchResults(results) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select a show and display more details
|
// Select a show and display more details
|
||||||
function selectShow(show) {
|
async function selectShow(show) {
|
||||||
console.log('Selected show:', show);
|
console.log('Selected show:', show);
|
||||||
// In a real implementation, this would fetch show details and display seasons
|
currentShow = show;
|
||||||
alert(`Selected show: ${show.seriesName}\nStatus: ${show.status || 'N/A'}\nFirst Aired: ${show.firstAired || 'N/A'}`);
|
|
||||||
|
// Display show title
|
||||||
|
document.getElementById('show-title').textContent = 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;
|
||||||
|
currentSeasons = showData.seasons || [];
|
||||||
|
|
||||||
|
// Display seasons
|
||||||
|
displaySeasons(currentSeasons);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch show details:', result.error);
|
||||||
|
document.getElementById('seasons-container').innerHTML = '<p>Error loading seasons: ' + (result.error || 'Unknown error') + '</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching show details:', error);
|
||||||
|
document.getElementById('seasons-container').innerHTML = '<p>Error loading seasons: ' + error.message + '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display seasons
|
||||||
|
function displaySeasons(seasons) {
|
||||||
|
const seasonsContainer = document.getElementById('seasons-container');
|
||||||
|
seasonsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (!seasons || seasons.length === 0) {
|
||||||
|
seasonsContainer.innerHTML = '<p>No seasons available.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a container for seasons
|
||||||
|
const seasonsList = document.createElement('div');
|
||||||
|
seasonsList.style.marginTop = '10px';
|
||||||
|
|
||||||
|
seasons.forEach(season => {
|
||||||
|
const seasonItem = document.createElement('div');
|
||||||
|
seasonItem.className = 'season-item';
|
||||||
|
seasonItem.style.padding = '8px';
|
||||||
|
seasonItem.style.margin = '5px 0';
|
||||||
|
seasonItem.style.border = '1px solid #ddd';
|
||||||
|
seasonItem.style.borderRadius = '4px';
|
||||||
|
seasonItem.style.cursor = 'pointer';
|
||||||
|
seasonItem.style.backgroundColor = '#f8f9fa';
|
||||||
|
|
||||||
|
// Format season display
|
||||||
|
let seasonDisplay = `Season ${season.number}`;
|
||||||
|
if (season.name && season.name !== `Season ${season.number}`) {
|
||||||
|
seasonDisplay += ` - ${season.name}`;
|
||||||
|
}
|
||||||
|
if (season.type) {
|
||||||
|
seasonDisplay += ` (${season.type})`;
|
||||||
|
}
|
||||||
|
if (season.episodeCount) {
|
||||||
|
seasonDisplay += ` (${season.episodeCount} episodes)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
seasonItem.textContent = seasonDisplay;
|
||||||
|
|
||||||
|
// Add click event to fetch episodes
|
||||||
|
seasonItem.addEventListener('click', () => {
|
||||||
|
fetchAndDisplayEpisodes(currentShow.id, season.number);
|
||||||
|
});
|
||||||
|
|
||||||
|
seasonsList.appendChild(seasonItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
seasonsContainer.appendChild(seasonsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch and display episodes for a season
|
||||||
|
async function fetchAndDisplayEpisodes(showId, seasonNumber) {
|
||||||
|
try {
|
||||||
|
const result = await ipcRenderer.invoke('get-season-episodes', showId, seasonNumber);
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const episodes = result.data.episodes || [];
|
||||||
|
displayEpisodes(episodes);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch episodes:', result.error);
|
||||||
|
document.getElementById('seasons-container').innerHTML = '<p>Error loading episodes: ' + (result.error || 'Unknown error') + '</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching episodes:', error);
|
||||||
|
document.getElementById('seasons-container').innerHTML = '<p>Error loading episodes: ' + error.message + '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display episodes
|
||||||
|
function displayEpisodes(episodes) {
|
||||||
|
const seasonsContainer = document.getElementById('seasons-container');
|
||||||
|
seasonsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (!episodes || episodes.length === 0) {
|
||||||
|
seasonsContainer.innerHTML = '<p>No episodes available.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a container for episodes
|
||||||
|
const episodesList = document.createElement('div');
|
||||||
|
episodesList.style.marginTop = '10px';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Format episode display
|
||||||
|
let episodeDisplay = `Episode ${episode.number}: ${episode.name || 'Untitled'}`;
|
||||||
|
if (episode.aired) {
|
||||||
|
episodeDisplay += ` (${episode.aired})`;
|
||||||
|
}
|
||||||
|
if (episode.runtime) {
|
||||||
|
episodeDisplay += ` (${episode.runtime} min)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
episodeItem.textContent = episodeDisplay;
|
||||||
|
|
||||||
|
// Add click event (could show more details)
|
||||||
|
episodeItem.addEventListener('click', () => {
|
||||||
|
alert(`Episode: ${episode.name || 'Untitled'}\nRuntime: ${episode.runtime || 'N/A'} min\nAired: ${episode.aired || 'N/A'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
episodesList.appendChild(episodeItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
seasonsContainer.appendChild(episodesList);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debounce function for search input
|
// Debounce function for search input
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user