Last active
February 1, 2026 16:41
-
-
Save lennyRBLX/822b66bc2833f7998b78186888db5c3a to your computer and use it in GitHub Desktop.
Edit your own TVDB & TMDB API Keys into it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const TMDB_ACCESS_TOKEN = "ENTER YOUR OWN TMDB API READ ACCESS TOKEN HERE"; | |
| // Cache expiration times (in milliseconds) | |
| const CACHE_DURATION = { | |
| EPISODE_GROUPS: 7 * 24 * 60 * 60 * 1000, // 7 days | |
| EPISODE_GROUP_DETAILS: 7 * 24 * 60 * 60 * 1000, // 7 days | |
| SEASON_TEMPLATE: 7 * 24 * 60 * 60 * 1000 // 7 days | |
| }; | |
| // Parse the request URL to determine endpoint type | |
| let requestUrl = $request.url; | |
| // Check if this is a recommendations/videos/images endpoint - just pass through | |
| if (requestUrl.includes("/recommendations") || requestUrl.includes("/videos") || requestUrl.includes("/images")) { | |
| console.log("📋 Recommendations/Videos/Images endpoint detected - returning original response"); | |
| $done({}); | |
| } | |
| let urlParts = requestUrl.match(/\/tv\/(\d+)(?:\/season\/(\d+)(\/credits)?)?/); | |
| if (!urlParts) { | |
| console.log("❌ URL doesn't match expected pattern"); | |
| $done({}); | |
| } | |
| let tvShowId = urlParts[1]; | |
| let seasonNumber = urlParts[2] ? parseInt(urlParts[2]) : null; | |
| let isCreditsEndpoint = urlParts[3] === "/credits"; | |
| console.log(`📡 Request: TV Show ${tvShowId}, Season ${seasonNumber}, Credits: ${isCreditsEndpoint}`); | |
| // Handle credits endpoint | |
| if (isCreditsEndpoint && seasonNumber !== null) { | |
| handleCreditsEndpoint(tvShowId, seasonNumber); | |
| } | |
| // Handle season endpoint | |
| else if (seasonNumber !== null) { | |
| handleSeasonEndpoint(tvShowId, seasonNumber); | |
| } | |
| // Handle main TV show endpoint | |
| else { | |
| handleMainTVEndpoint(tvShowId); | |
| } | |
| // ==================== UTILITIES ==================== | |
| function episodeGroupIdToNumber(groupId) { | |
| if (groupId == null) { | |
| return null; | |
| } | |
| // Take first 6 hex characters and convert to decimal | |
| let shortHex = groupId.substring(0, 6); | |
| let decimal = parseInt(shortHex, 16); | |
| // Constrain to 6 digits (000000-999999) | |
| return parseInt((decimal % 1000000).toString().padStart(6, '0')); | |
| } | |
| function getCachedData(key) { | |
| let cached = $persistentStore.read(key); | |
| if (!cached) return null; | |
| try { | |
| let data = JSON.parse(cached); | |
| if (data.expires && Date.now() > data.expires) { | |
| console.log(` 🗑️ Cache expired for ${key}`); | |
| $persistentStore.write(null, key); | |
| return null; | |
| } | |
| return data.value; | |
| } catch (e) { | |
| return null; | |
| } | |
| } | |
| function setCachedData(key, value, duration) { | |
| let data = { | |
| value: value, | |
| expires: duration ? Date.now() + duration : null | |
| }; | |
| $persistentStore.write(JSON.stringify(data), key); | |
| } | |
| function removeSpecialsFromResponse(tmdbResponse) { | |
| console.log(`\n🧹 Cleaning up response - removing Season 0 and below...`); | |
| if (!tmdbResponse.seasons || !Array.isArray(tmdbResponse.seasons)) { | |
| console.log(` ⚠️ No seasons array found`); | |
| return tmdbResponse; | |
| } | |
| let originalCount = tmdbResponse.seasons.length; | |
| // Filter out Season 0 and below | |
| tmdbResponse.seasons = tmdbResponse.seasons.filter(season => { | |
| if (season.season_number <= 0) { | |
| console.log(` 🗑️ Removed Season ${season.season_number}: ${season.name || 'Unnamed'} (${season.episode_count} episodes)`); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| let newCount = tmdbResponse.seasons.length; | |
| let removedCount = originalCount - newCount; | |
| if (removedCount > 0) { | |
| // Recalculate episode count | |
| let totalEpisodes = 0; | |
| tmdbResponse.seasons.forEach(season => { | |
| totalEpisodes += season.episode_count || 0; | |
| }); | |
| tmdbResponse.number_of_episodes = totalEpisodes; | |
| tmdbResponse.number_of_seasons = newCount; | |
| console.log(` ✅ Removed ${removedCount} season(s)`); | |
| console.log(` Updated counts: ${newCount} seasons, ${totalEpisodes} episodes`); | |
| } else { | |
| console.log(` ℹ️ No seasons to remove`); | |
| } | |
| return tmdbResponse; | |
| } | |
| // ==================== CREDITS ENDPOINT ==================== | |
| function handleCreditsEndpoint(tvShowId, seasonNumber) { | |
| // Check if response is valid (200 status and has body) | |
| let hasValidResponse = false; | |
| let responseData = null; | |
| try { | |
| if ($response.status === 200 && $response.body && $response.body.length > 0) { | |
| responseData = JSON.parse($response.body); | |
| if (responseData && (responseData.cast || responseData.crew)) { | |
| hasValidResponse = true; | |
| console.log(`✅ Credits response successful`); | |
| console.log(` Cast: ${responseData.cast ? responseData.cast.length : 0} members`); | |
| console.log(` Crew: ${responseData.crew ? responseData.crew.length : 0} members`); | |
| $done({}); | |
| return; | |
| } | |
| } | |
| } catch (e) { | |
| console.log(`⚠️ Failed to parse response: ${e}`); | |
| } | |
| if (!hasValidResponse) { | |
| console.log(`⚠️ Credits response failed (status: ${$response.status || 'unknown'})`); | |
| if (seasonNumber !== 1) { | |
| console.log(`🔄 Redirecting to season 1 credits...`); | |
| let season1Url = `https://forwardinfo.vvebo.vip/tv/${tvShowId}/season/1/credits`; | |
| $done({ | |
| status: 302, | |
| headers: { | |
| "Location": season1Url | |
| } | |
| }); | |
| return; | |
| } | |
| console.log(`❌ No credits data available for season 1`); | |
| $done({}); | |
| } | |
| } | |
| // ==================== SEASON ENDPOINT ==================== | |
| function handleSeasonEndpoint(tvShowId, requestedSeasonNumber) { | |
| console.log(`\n🔍 Processing season ${requestedSeasonNumber} request...`); | |
| // Don't process season 0 (Specials) - return 404 or redirect to season 1 | |
| if (requestedSeasonNumber === 0) { | |
| console.log(`⚠️ Season 0 (Specials) not supported, redirecting to season 1...`); | |
| let season1Url = `https://forwardinfo.vvebo.vip/tv/${tvShowId}/season/1`; | |
| $done({ | |
| status: 302, | |
| headers: { | |
| "Location": season1Url | |
| } | |
| }); | |
| return; | |
| } | |
| fetchEpisodeGroups(tvShowId, requestedSeasonNumber); | |
| } | |
| function fetchEpisodeGroups(tvShowId, targetSeasonNumber) { | |
| let cacheKey = `episode_groups_${tvShowId}`; | |
| let cachedGroups = getCachedData(cacheKey); | |
| if (cachedGroups) { | |
| console.log(`📦 Using cached episode groups`); | |
| processEpisodeGroups(tvShowId, cachedGroups, targetSeasonNumber); | |
| return; | |
| } | |
| let url = `https://api.themoviedb.org/3/tv/${tvShowId}/episode_groups`; | |
| console.log(`🔄 Fetching episode groups from TMDB...`); | |
| $httpClient.get({ | |
| url: url, | |
| headers: { | |
| "Authorization": `Bearer ${TMDB_ACCESS_TOKEN}`, | |
| "Content-Type": "application/json" | |
| }, | |
| timeout: 5000 | |
| }, function(error, response, data) { | |
| if (error) { | |
| console.log(`⚠️ Episode groups fetch failed: ${error}`); | |
| console.log(` Falling back to standard season data...`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| return; | |
| } | |
| try { | |
| let groupsData = JSON.parse(data); | |
| if (groupsData && groupsData.results && groupsData.results.length > 0) { | |
| console.log(`✅ Found ${groupsData.results.length} episode groups`); | |
| setCachedData(cacheKey, groupsData, CACHE_DURATION.EPISODE_GROUPS); | |
| processEpisodeGroups(tvShowId, groupsData, targetSeasonNumber); | |
| } else { | |
| console.log(`⚠️ No episode groups found, using standard data...`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| } | |
| } catch (e) { | |
| console.log(`❌ Failed to parse episode groups: ${e}`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| } | |
| }); | |
| } | |
| function processEpisodeGroups(tvShowId, groupsData, targetSeasonNumber) { | |
| let seasonsGroup = null; | |
| let groupSource = null; | |
| // Priority 1: Find group with name "Seasons" (case-insensitive) | |
| seasonsGroup = groupsData.results.find(group => | |
| group.name && group.name.toLowerCase() === "seasons" | |
| ); | |
| if (seasonsGroup) { | |
| groupSource = `name "Seasons"`; | |
| } | |
| // Priority 2: Find group with type 7 | |
| if (!seasonsGroup) { | |
| seasonsGroup = groupsData.results.find(group => group.type === 7); | |
| if (seasonsGroup) { | |
| groupSource = `type 7`; | |
| } | |
| } | |
| // Priority 3: Find group with type 6 | |
| if (!seasonsGroup) { | |
| seasonsGroup = groupsData.results.find(group => group.type === 6); | |
| if (seasonsGroup) { | |
| groupSource = `type 6`; | |
| } | |
| } | |
| // No suitable group found | |
| if (!seasonsGroup) { | |
| console.log(`⚠️ No suitable episode group found`); | |
| console.log(` Available groups: ${groupsData.results.map(g => `${g.name} (type ${g.type})`).join(", ")}`); | |
| console.log(` Falling back to standard season data...`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| return; | |
| } | |
| console.log(`✅ Found episode group by ${groupSource}: ${seasonsGroup.name}`); | |
| console.log(` ID: ${seasonsGroup.id}`); | |
| console.log(` Type: ${seasonsGroup.type}`); | |
| console.log(` Episodes: ${seasonsGroup.episode_count}`); | |
| console.log(` Groups: ${seasonsGroup.group_count}`); | |
| fetchEpisodeGroupDetails(tvShowId, seasonsGroup.id, targetSeasonNumber); | |
| } | |
| function fetchEpisodeGroupDetails(tvShowId, groupId, targetSeasonNumber) { | |
| let cacheKey = `episode_group_${groupId}`; | |
| let cachedDetails = getCachedData(cacheKey); | |
| if (cachedDetails) { | |
| console.log(`📦 Using cached episode group details`); | |
| processEpisodeGroupDetails(tvShowId, cachedDetails, targetSeasonNumber); | |
| return; | |
| } | |
| let url = `https://api.themoviedb.org/3/tv/episode_group/${groupId}`; | |
| console.log(`🔄 Fetching episode group details from TMDB...`); | |
| $httpClient.get({ | |
| url: url, | |
| headers: { | |
| "Authorization": `Bearer ${TMDB_ACCESS_TOKEN}`, | |
| "Content-Type": "application/json" | |
| }, | |
| timeout: 5000 | |
| }, function(error, response, data) { | |
| if (error) { | |
| console.log(`❌ Episode group details fetch failed: ${error}`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| return; | |
| } | |
| try { | |
| let detailsData = JSON.parse(data); | |
| if (detailsData && detailsData.groups) { | |
| console.log(`✅ Fetched episode group details`); | |
| console.log(` Name: ${detailsData.name}`); | |
| console.log(` Total groups: ${detailsData.groups.length}`); | |
| setCachedData(cacheKey, detailsData, CACHE_DURATION.EPISODE_GROUP_DETAILS); | |
| processEpisodeGroupDetails(tvShowId, detailsData, targetSeasonNumber); | |
| } else { | |
| console.log(`❌ Invalid episode group details`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| } | |
| } catch (e) { | |
| console.log(`❌ Failed to parse episode group details: ${e}`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| } | |
| }); | |
| } | |
| function processEpisodeGroupDetails(tvShowId, groupDetails, targetSeasonNumber) { | |
| console.log(`\n🔧 Processing episode group for season ${targetSeasonNumber}...`); | |
| // Find the target season - match by order field | |
| let targetGroup = groupDetails.groups.find(group => group.order === targetSeasonNumber); | |
| if (!targetGroup) { | |
| console.log(`⚠️ Season ${targetSeasonNumber} not found in episode group`); | |
| console.log(` Available seasons: ${groupDetails.groups.filter(g => g.order > 0).map(g => `${g.order}: ${g.name}`).join(", ")}`); | |
| fetchStandardSeasonData(tvShowId, targetSeasonNumber); | |
| return; | |
| } | |
| console.log(`✅ Found season ${targetSeasonNumber} in episode group`); | |
| console.log(` Name: ${targetGroup.name}`); | |
| console.log(` Episodes: ${targetGroup.episodes.length}`); | |
| buildSeasonResponse(tvShowId, targetGroup, targetSeasonNumber); | |
| } | |
| function buildSeasonResponse(tmdbShowId, episodeGroup, targetSeasonNumber) { | |
| console.log(`\n🔨 Building season ${targetSeasonNumber} response...`); | |
| let episodes = episodeGroup.episodes; | |
| console.log(` Processing ${episodes.length} episodes from episode group`); | |
| // Build episodes array - renumber episodes sequentially | |
| let newEpisodes = []; | |
| episodes.forEach((ep, index) => { | |
| let episodeNumber = index + 1; | |
| let newEpisode = { | |
| air_date: ep.air_date || null, | |
| episode_number: episodeNumber, | |
| episode_type: ep.episode_type || "standard", | |
| id: ep.id, | |
| name: ep.name || `Episode ${episodeNumber}`, | |
| overview: ep.overview || "", | |
| production_code: ep.production_code || "", | |
| runtime: ep.runtime || 24, | |
| season_number: targetSeasonNumber, | |
| show_id: parseInt(tmdbShowId), | |
| still_path: ep.still_path || null, | |
| vote_average: ep.vote_average || 0, | |
| vote_count: ep.vote_count || 0, | |
| crew: [], | |
| guest_stars: [] | |
| }; | |
| newEpisodes.push(newEpisode); | |
| let stillInfo = newEpisode.still_path ? "[has still]" : "[no still]"; | |
| console.log(` ✓ S${targetSeasonNumber}E${episodeNumber}: ${newEpisode.name} ${stillInfo} [ID: ${newEpisode.id}]`); | |
| }); | |
| // Create season response | |
| let seasonResponse = { | |
| air_date: newEpisodes.length > 0 ? newEpisodes[0].air_date : null, | |
| season_number: targetSeasonNumber, | |
| name: episodeGroup.name || `Season ${targetSeasonNumber}`, | |
| vote_average: 0, | |
| id: episodeGroupIdToNumber(episodeGroup.id), | |
| episodes: newEpisodes, | |
| poster_path: null, | |
| overview: "" | |
| }; | |
| console.log(`\n✅ Season ${targetSeasonNumber} response created with ${newEpisodes.length} episodes`); | |
| console.log(` Air date: ${seasonResponse.air_date}`); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(seasonResponse) | |
| }); | |
| } | |
| function fetchStandardSeasonData(tvShowId, targetSeasonNumber) { | |
| console.log(`\n🔄 Fetching standard season data as fallback...`); | |
| let url = `https://api.themoviedb.org/3/tv/${tvShowId}/season/${targetSeasonNumber}?language=en-US`; | |
| $httpClient.get({ | |
| url: url, | |
| headers: { | |
| "Authorization": `Bearer ${TMDB_ACCESS_TOKEN}`, | |
| "Content-Type": "application/json" | |
| }, | |
| timeout: 5000 | |
| }, function(error, response, data) { | |
| if (error) { | |
| console.log(`❌ Standard season fetch failed: ${error}`); | |
| $done({}); | |
| return; | |
| } | |
| try { | |
| let seasonData = JSON.parse(data); | |
| if (seasonData && seasonData.episodes) { | |
| console.log(`✅ Using standard season data (${seasonData.episodes.length} episodes)`); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(seasonData) | |
| }); | |
| } else { | |
| console.log(`❌ Invalid season data`); | |
| $done({}); | |
| } | |
| } catch (e) { | |
| console.log(`❌ Failed to parse season data: ${e}`); | |
| $done({}); | |
| } | |
| }); | |
| } | |
| // ==================== MAIN TV ENDPOINT ==================== | |
| function handleMainTVEndpoint(tvShowId) { | |
| if (!$response.body || $response.body.length === 0) { | |
| console.log(`⚠️ Response body is empty, fetching TMDB data for show ${tvShowId}...`); | |
| fetchTMDBShowData(tvShowId); | |
| return; | |
| } | |
| let obj; | |
| try { | |
| obj = JSON.parse($response.body); | |
| } catch (e) { | |
| console.log(`❌ Failed to parse response body: ${e}`); | |
| console.log(` Fetching TMDB data for show ${tvShowId}...`); | |
| fetchTMDBShowData(tvShowId); | |
| return; | |
| } | |
| processTVShowData(obj); | |
| } | |
| function fetchTMDBShowData(tvShowId) { | |
| let url = `https://api.themoviedb.org/3/tv/${tvShowId}?language=en-US`; | |
| console.log(`🔄 Fetching TMDB data for show ${tvShowId}...`); | |
| $httpClient.get({ | |
| url: url, | |
| headers: { | |
| "Authorization": `Bearer ${TMDB_ACCESS_TOKEN}`, | |
| "Content-Type": "application/json" | |
| }, | |
| timeout: 5000 | |
| }, function(error, response, data) { | |
| if (error) { | |
| console.log(`❌ TMDB fetch error: ${error}`); | |
| $done({}); | |
| return; | |
| } | |
| try { | |
| let showData = JSON.parse(data); | |
| console.log(`✅ Fetched TMDB data for: ${showData.name || showData.original_name}`); | |
| processTVShowData(showData); | |
| } catch (e) { | |
| console.log(`❌ Failed to parse TMDB response: ${e}`); | |
| $done({}); | |
| } | |
| }); | |
| } | |
| function processTVShowData(obj) { | |
| let isJapanese = obj.origin_country && obj.origin_country.includes("JP"); | |
| let isAnimation = obj.genres && obj.genres.some(genre => genre.id === 16); | |
| if (isJapanese && isAnimation) { | |
| console.log(`✅ Match found: ${obj.name} (TMDB ID: ${obj.id})`); | |
| console.log(` Origin: ${obj.origin_country.join(", ")}`); | |
| console.log(` Genres: ${obj.genres.map(g => g.name).join(", ")}`); | |
| fetchEpisodeGroupsForMain(obj.id, obj); | |
| } else { | |
| console.log(`❌ No match: ${obj.name || "Unknown"}`); | |
| console.log(` Japanese: ${isJapanese}, Animation: ${isAnimation}`); | |
| // Still remove Season 0 even for non-anime shows | |
| obj = removeSpecialsFromResponse(obj); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(obj) | |
| }); | |
| } | |
| } | |
| function fetchEpisodeGroupsForMain(tvShowId, originalResponse) { | |
| let cacheKey = `episode_groups_${tvShowId}`; | |
| let cachedGroups = getCachedData(cacheKey); | |
| if (cachedGroups) { | |
| console.log(`📦 Using cached episode groups`); | |
| processEpisodeGroupsForMain(tvShowId, cachedGroups, originalResponse); | |
| return; | |
| } | |
| let url = `https://api.themoviedb.org/3/tv/${tvShowId}/episode_groups`; | |
| console.log(`🔄 Fetching episode groups...`); | |
| $httpClient.get({ | |
| url: url, | |
| headers: { | |
| "Authorization": `Bearer ${TMDB_ACCESS_TOKEN}`, | |
| "Content-Type": "application/json" | |
| }, | |
| timeout: 5000 | |
| }, function(error, response, data) { | |
| if (error) { | |
| console.log(`⚠️ Episode groups fetch failed: ${error}`); | |
| console.log(` Using original response with cleanup...`); | |
| // Remove Season 0 from original response before returning | |
| originalResponse = removeSpecialsFromResponse(originalResponse); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(originalResponse) | |
| }); | |
| return; | |
| } | |
| try { | |
| let groupsData = JSON.parse(data); | |
| if (groupsData && groupsData.results && groupsData.results.length > 0) { | |
| console.log(`✅ Found ${groupsData.results.length} episode groups`); | |
| setCachedData(cacheKey, groupsData, CACHE_DURATION.EPISODE_GROUPS); | |
| processEpisodeGroupsForMain(tvShowId, groupsData, originalResponse); | |
| } else { | |
| console.log(`⚠️ No episode groups found`); | |
| // Remove Season 0 from original response before returning | |
| originalResponse = removeSpecialsFromResponse(originalResponse); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(originalResponse) | |
| }); | |
| } | |
| } catch (e) { | |
| console.log(`❌ Failed to parse episode groups: ${e}`); | |
| // Remove Season 0 from original response before returning | |
| originalResponse = removeSpecialsFromResponse(originalResponse); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(originalResponse) | |
| }); | |
| } | |
| }); | |
| } | |
| function processEpisodeGroupsForMain(tvShowId, groupsData, originalResponse) { | |
| let seasonsGroup = null; | |
| let groupSource = null; | |
| // Priority 1: Find group with name "Seasons" (case-insensitive) | |
| seasonsGroup = groupsData.results.find(group => | |
| group.name && group.name.toLowerCase() === "seasons" | |
| ); | |
| if (seasonsGroup) { | |
| groupSource = `name "Seasons"`; | |
| } | |
| // Priority 2: Find group with type 7 | |
| if (!seasonsGroup) { | |
| seasonsGroup = groupsData.results.find(group => group.type === 7); | |
| if (seasonsGroup) { | |
| groupSource = `type 7`; | |
| } | |
| } | |
| // Priority 3: Find group with type 6 | |
| if (!seasonsGroup) { | |
| seasonsGroup = groupsData.results.find(group => group.type === 6); | |
| if (seasonsGroup) { | |
| groupSource = `type 6`; | |
| } | |
| } | |
| // No suitable group found | |
| if (!seasonsGroup) { | |
| console.log(`⚠️ No suitable episode group found`); | |
| console.log(` Available groups: ${groupsData.results.map(g => `${g.name} (type ${g.type})`).join(", ")}`); | |
| console.log(` Using original response with cleanup...`); | |
| // Remove Season 0 from original response before returning | |
| originalResponse = removeSpecialsFromResponse(originalResponse); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(originalResponse) | |
| }); | |
| return; | |
| } | |
| console.log(`✅ Found episode group by ${groupSource}: ${seasonsGroup.name}`); | |
| console.log(` ID: ${seasonsGroup.id}`); | |
| console.log(` Type: ${seasonsGroup.type}`); | |
| console.log(` Episodes: ${seasonsGroup.episode_count}`); | |
| console.log(` Seasons: ${seasonsGroup.group_count}`); | |
| fetchEpisodeGroupDetailsForMain(tvShowId, seasonsGroup.id, seasonsGroup, originalResponse); | |
| } | |
| function fetchEpisodeGroupDetailsForMain(tvShowId, groupId, seasonsGroup, originalResponse) { | |
| let cacheKey = `episode_group_${groupId}`; | |
| let cachedDetails = getCachedData(cacheKey); | |
| if (cachedDetails) { | |
| console.log(`📦 Using cached episode group details`); | |
| updateMainTVResponse(originalResponse, cachedDetails, seasonsGroup); | |
| return; | |
| } | |
| let url = `https://api.themoviedb.org/3/tv/episode_group/${groupId}`; | |
| console.log(`🔄 Fetching episode group details...`); | |
| $httpClient.get({ | |
| url: url, | |
| headers: { | |
| "Authorization": `Bearer ${TMDB_ACCESS_TOKEN}`, | |
| "Content-Type": "application/json" | |
| }, | |
| timeout: 5000 | |
| }, function(error, response, data) { | |
| if (error) { | |
| console.log(`❌ Episode group details fetch failed: ${error}`); | |
| // Remove Season 0 from original response before returning | |
| originalResponse = removeSpecialsFromResponse(originalResponse); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(originalResponse) | |
| }); | |
| return; | |
| } | |
| try { | |
| let detailsData = JSON.parse(data); | |
| if (detailsData && detailsData.groups) { | |
| console.log(`✅ Fetched episode group details`); | |
| setCachedData(cacheKey, detailsData, CACHE_DURATION.EPISODE_GROUP_DETAILS); | |
| updateMainTVResponse(originalResponse, detailsData, seasonsGroup); | |
| } else { | |
| console.log(`❌ Invalid episode group details`); | |
| // Remove Season 0 from original response before returning | |
| originalResponse = removeSpecialsFromResponse(originalResponse); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(originalResponse) | |
| }); | |
| } | |
| } catch (e) { | |
| console.log(`❌ Failed to parse episode group details: ${e}`); | |
| // Remove Season 0 from original response before returning | |
| originalResponse = removeSpecialsFromResponse(originalResponse); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(originalResponse) | |
| }); | |
| } | |
| }); | |
| } | |
| function updateMainTVResponse(tmdbResponse, episodeGroupDetails, seasonsGroupSummary) { | |
| console.log(`\n🔄 Updating TMDB response with episode group data...`); | |
| // Filter out Season 0 and seasons with "Special" or "OVA" in the name | |
| let regularSeasons = episodeGroupDetails.groups.filter(g => { | |
| // Exclude Season 0 | |
| if (g.order <= 0) return false; | |
| // Exclude if name contains "Special", "Specials", or "OVA" (case-insensitive) | |
| if (g.name) { | |
| let nameLower = g.name.toLowerCase(); | |
| if (nameLower.includes("special") || nameLower.includes("ova")) { | |
| console.log(` 🗑️ Excluding season: "${g.name}" (order: ${g.order})`); | |
| return false; | |
| } | |
| } | |
| return true; | |
| }); | |
| console.log(` Filtered: ${episodeGroupDetails.groups.length} total groups → ${regularSeasons.length} regular seasons`); | |
| // Calculate total episodes excluding Specials/OVAs | |
| let totalEpisodes = 0; | |
| regularSeasons.forEach(group => { | |
| totalEpisodes += group.episodes.length; | |
| }); | |
| // Update counts | |
| tmdbResponse.number_of_episodes = totalEpisodes; | |
| tmdbResponse.number_of_seasons = regularSeasons.length; | |
| console.log(` Total Episodes: ${tmdbResponse.number_of_episodes} (excluding Specials/OVAs)`); | |
| console.log(` Total Seasons: ${tmdbResponse.number_of_seasons}`); | |
| // Store old seasons data for metadata | |
| let oldSeasons = tmdbResponse.seasons || []; | |
| let oldSeasonData = {}; | |
| oldSeasons.forEach(season => { | |
| oldSeasonData[season.season_number] = { | |
| poster_path: season.poster_path, | |
| overview: season.overview, | |
| vote_average: season.vote_average, | |
| id: season.id | |
| }; | |
| }); | |
| // Build new seasons array (excluding Season 0 and Specials/OVAs) | |
| tmdbResponse.seasons = []; | |
| let lastKnownPosterPath = null; | |
| regularSeasons.forEach(group => { | |
| let seasonNumber = group.order; | |
| let episodes = group.episodes; | |
| let oldData = oldSeasonData[seasonNumber] || {}; | |
| // Determine poster_path | |
| let posterPath = null; | |
| if (oldData.poster_path) { | |
| posterPath = oldData.poster_path; | |
| lastKnownPosterPath = posterPath; | |
| } else if (lastKnownPosterPath) { | |
| posterPath = lastKnownPosterPath; | |
| console.log(` 🖼️ Season ${seasonNumber} using last known poster`); | |
| } | |
| let seasonObj = { | |
| air_date: episodes.length > 0 ? episodes[0].air_date : null, | |
| episode_count: episodes.length, | |
| id: oldData.id || episodeGroupIdToNumber(group.id) || null, | |
| name: group.name || `Season ${seasonNumber}`, | |
| overview: oldData.overview || "", | |
| poster_path: posterPath, | |
| season_number: seasonNumber, | |
| vote_average: oldData.vote_average || 0 | |
| }; | |
| tmdbResponse.seasons.push(seasonObj); | |
| console.log(` ✓ Season ${seasonNumber}: ${episodes.length} episodes - ${group.name}`); | |
| }); | |
| // Sort seasons by season_number | |
| tmdbResponse.seasons.sort((a, b) => a.season_number - b.season_number); | |
| // Update last_episode_to_air and next_episode_to_air (excluding Specials/OVAs) | |
| let today = new Date(); | |
| let allEpisodes = []; | |
| regularSeasons.forEach(group => { | |
| group.episodes.forEach((ep, index) => { | |
| allEpisodes.push({ | |
| ...ep, | |
| seasonNumber: group.order, | |
| episodeNumber: index + 1 // Sequential numbering within group | |
| }); | |
| }); | |
| }); | |
| // Find last aired episode | |
| let airedEpisodes = allEpisodes.filter(ep => { | |
| if (!ep.air_date) return false; | |
| let airDate = new Date(ep.air_date); | |
| return airDate <= today; | |
| }); | |
| airedEpisodes.sort((a, b) => new Date(b.air_date) - new Date(a.air_date)); | |
| if (airedEpisodes.length > 0) { | |
| let lastEp = airedEpisodes[0]; | |
| tmdbResponse.last_episode_to_air = { | |
| id: lastEp.id, | |
| name: lastEp.name || "Untitled", | |
| overview: lastEp.overview || "", | |
| vote_average: lastEp.vote_average || 0, | |
| vote_count: lastEp.vote_count || 0, | |
| air_date: lastEp.air_date, | |
| episode_number: lastEp.episodeNumber, | |
| episode_type: lastEp.episode_type || "standard", | |
| production_code: lastEp.production_code || "", | |
| runtime: lastEp.runtime || 24, | |
| season_number: lastEp.seasonNumber, | |
| show_id: tmdbResponse.id, | |
| still_path: lastEp.still_path || null | |
| }; | |
| console.log(` Last Episode: S${lastEp.seasonNumber}E${lastEp.episodeNumber} - ${lastEp.name}`); | |
| console.log(` Aired: ${lastEp.air_date}`); | |
| } | |
| // Find next episode to air | |
| let unairedEpisodes = allEpisodes.filter(ep => { | |
| if (!ep.air_date) return false; | |
| let airDate = new Date(ep.air_date); | |
| return airDate > today; | |
| }); | |
| unairedEpisodes.sort((a, b) => new Date(a.air_date) - new Date(b.air_date)); | |
| if (unairedEpisodes.length > 0) { | |
| let nextEp = unairedEpisodes[0]; | |
| tmdbResponse.next_episode_to_air = { | |
| id: nextEp.id, | |
| name: nextEp.name || "Untitled", | |
| overview: nextEp.overview || "", | |
| vote_average: nextEp.vote_average || 0, | |
| vote_count: nextEp.vote_count || 0, | |
| air_date: nextEp.air_date, | |
| episode_number: nextEp.episodeNumber, | |
| episode_type: nextEp.episode_type || "standard", | |
| production_code: nextEp.production_code || "", | |
| runtime: nextEp.runtime || 24, | |
| season_number: nextEp.seasonNumber, | |
| show_id: tmdbResponse.id, | |
| still_path: nextEp.still_path || null | |
| }; | |
| console.log(` Next Episode: S${nextEp.seasonNumber}E${nextEp.episodeNumber} - ${nextEp.name}`); | |
| console.log(` Airs: ${nextEp.air_date}`); | |
| } else { | |
| tmdbResponse.next_episode_to_air = null; | |
| console.log(` Next Episode: None scheduled`); | |
| } | |
| // Update last_air_date | |
| if (airedEpisodes.length > 0) { | |
| tmdbResponse.last_air_date = airedEpisodes[0].air_date; | |
| } | |
| console.log(`\n✅ TMDB response updated with episode group data (Specials/OVAs excluded)`); | |
| $done({ | |
| status: 200, | |
| body: JSON.stringify(tmdbResponse) | |
| }); | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment