mirror of
https://github.com/Viren070/tmdb-addon.git
synced 2025-12-01 23:18:11 +01:00
feat: Add age rating as first genre with IMDb parental guide link
- Extract age ratings from TMDB API with regional fallback (US) - Add age rating as first genre in metadata response - Include IMDb parental guide link as first genre link - Add configurable toggle for age rating display - Implement caching for age rating data (1 hour TTL) - Support both movies and TV shows with proper error handling
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
require("dotenv").config();
|
||||
const { TMDBClient } = require("../utils/tmdbClient");
|
||||
const Utils = require("../utils/parseProps");
|
||||
const moviedb = new TMDBClient(process.env.TMDB_API);
|
||||
|
||||
// Cache for age ratings
|
||||
const ageRatingCache = new Map();
|
||||
const CACHE_TTL = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
/**
|
||||
* Get age rating for a movie from TMDB
|
||||
*/
|
||||
async function getMovieAgeRating(tmdbId, language) {
|
||||
try {
|
||||
const releaseDates = await moviedb.movieReleaseDates({ id: tmdbId });
|
||||
const userRegion = language.split("-")[1] || "US";
|
||||
|
||||
// Try user's region first
|
||||
let ageRating = Utils.parseCertification(releaseDates, language);
|
||||
|
||||
// If no rating found for user's region, fallback to US
|
||||
if (!ageRating && userRegion !== "US") {
|
||||
ageRating = Utils.parseCertification(releaseDates, "en-US");
|
||||
}
|
||||
|
||||
return ageRating || null;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching age rating for movie ${tmdbId}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get age rating for a TV show from TMDB
|
||||
*/
|
||||
async function getTvAgeRating(tmdbId, language) {
|
||||
try {
|
||||
const contentRatings = await moviedb.tvContentRatings({ id: tmdbId });
|
||||
const userRegion = language.split("-")[1] || "US";
|
||||
|
||||
// Find rating for user's region
|
||||
let ageRating = null;
|
||||
const userRegionRating = contentRatings.results.find(
|
||||
rating => rating.iso_3166_1 === userRegion
|
||||
);
|
||||
|
||||
if (userRegionRating && userRegionRating.rating) {
|
||||
ageRating = userRegionRating.rating;
|
||||
} else {
|
||||
// Fallback to US rating
|
||||
const usRating = contentRatings.results.find(
|
||||
rating => rating.iso_3166_1 === "US"
|
||||
);
|
||||
if (usRating && usRating.rating) {
|
||||
ageRating = usRating.rating;
|
||||
}
|
||||
}
|
||||
|
||||
return ageRating || null;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching age rating for TV show ${tmdbId}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached age rating
|
||||
*/
|
||||
async function getCachedAgeRating(tmdbId, type, language) {
|
||||
if (!tmdbId) return null;
|
||||
|
||||
const cacheKey = `${type}-${tmdbId}-${language}`;
|
||||
const cached = ageRatingCache.get(cacheKey);
|
||||
|
||||
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
|
||||
return cached.rating;
|
||||
}
|
||||
|
||||
try {
|
||||
const rating = type === "movie"
|
||||
? await getMovieAgeRating(tmdbId, language)
|
||||
: await getTvAgeRating(tmdbId, language);
|
||||
|
||||
ageRatingCache.set(cacheKey, { rating, timestamp: Date.now() });
|
||||
return rating;
|
||||
} catch (err) {
|
||||
console.error(`Error fetching age rating for ${type} ${tmdbId}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getCachedAgeRating };
|
||||
+39
-16
@@ -5,11 +5,12 @@ const moviedb = new TMDBClient(process.env.TMDB_API);
|
||||
const { getEpisodes } = require("./getEpisodes");
|
||||
const { getLogo, getTvLogo } = require("./getLogo");
|
||||
const { getImdbRating } = require("./getImdbRating");
|
||||
const { getCachedAgeRating } = require("./getAgeRating");
|
||||
const { checkSeasonsAndReport } = require("../utils/checkSeasons");
|
||||
|
||||
// Configuration
|
||||
const CACHE_TTL = 1000 * 60 * 60; // 1 hour
|
||||
const blacklistLogoUrls = [ "https://assets.fanart.tv/fanart/tv/0/hdtvlogo/-60a02798b7eea.png" ];
|
||||
const blacklistLogoUrls = ["https://assets.fanart.tv/fanart/tv/0/hdtvlogo/-60a02798b7eea.png"];
|
||||
|
||||
// Cache
|
||||
const cache = new Map();
|
||||
@@ -28,21 +29,27 @@ async function getCachedImdbRating(imdbId, type) {
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
const getCacheKey = (type, language, tmdbId, rpdbkey) =>
|
||||
`${type}-${language}-${tmdbId}-${rpdbkey}`;
|
||||
const getCacheKey = (type, language, tmdbId, rpdbkey, showAgeRatingInGenres = true) =>
|
||||
`${type}-${language}-${tmdbId}-${rpdbkey}-ageRating:${showAgeRatingInGenres}`;
|
||||
|
||||
const processLogo = (logo) => {
|
||||
if (!logo || blacklistLogoUrls.includes(logo)) return null;
|
||||
return logo.replace("http://", "https://");
|
||||
};
|
||||
|
||||
const buildLinks = (imdbRating, imdbId, title, type, genres, credits, language, castCount) => [
|
||||
const buildLinks = (imdbRating, imdbId, title, type, genres, credits, language, castCount, ageRating = null, showAgeRatingInGenres = true) => [
|
||||
Utils.parseImdbLink(imdbRating, imdbId),
|
||||
Utils.parseShareLink(title, imdbId, type),
|
||||
...Utils.parseGenreLink(genres, type, language),
|
||||
...Utils.parseGenreLink(genres, type, language, imdbId, ageRating, showAgeRatingInGenres),
|
||||
...Utils.parseCreditsLink(credits, castCount)
|
||||
];
|
||||
|
||||
// Helper function to add age rating to genres
|
||||
const addAgeRatingToGenres = (ageRating, genres, showAgeRatingInGenres = true) => {
|
||||
if (!ageRating || !showAgeRatingInGenres) return genres;
|
||||
return [ageRating, ...genres];
|
||||
};
|
||||
|
||||
// Movie specific functions
|
||||
const fetchMovieData = async (tmdbId, language) => {
|
||||
return await moviedb.movieInfo({
|
||||
@@ -53,13 +60,17 @@ const fetchMovieData = async (tmdbId, language) => {
|
||||
};
|
||||
|
||||
const buildMovieResponse = async (res, type, language, tmdbId, rpdbkey, config = {}) => {
|
||||
const [poster, logo, imdbRatingRaw] = await Promise.all([
|
||||
const [poster, logo, imdbRatingRaw, ageRating] = await Promise.all([
|
||||
Utils.parsePoster(type, tmdbId, res.poster_path, language, rpdbkey),
|
||||
getLogo(tmdbId, language, res.original_language).catch(e => {
|
||||
console.warn(`Error fetching logo for movie ${tmdbId}:`, e.message);
|
||||
return null;
|
||||
}),
|
||||
getCachedImdbRating(res.external_ids?.imdb_id, type),
|
||||
getCachedAgeRating(tmdbId, type, language).catch(e => {
|
||||
console.warn(`Error fetching age rating for movie ${tmdbId}:`, e.message);
|
||||
return null;
|
||||
}),
|
||||
]);
|
||||
|
||||
const imdbRating = imdbRatingRaw || res.vote_average?.toFixed(1) || "N/A";
|
||||
@@ -67,12 +78,15 @@ const buildMovieResponse = async (res, type, language, tmdbId, rpdbkey, config =
|
||||
const returnImdbId = config.returnImdbId === true || config.returnImdbId === "true";
|
||||
const hideInCinemaTag = config.hideInCinemaTag === true || config.hideInCinemaTag === "true";
|
||||
|
||||
const parsedGenres = Utils.parseGenres(res.genres);
|
||||
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false; // Default to true
|
||||
|
||||
const response = {
|
||||
imdb_id: res.imdb_id,
|
||||
country: Utils.parseCoutry(res.production_countries),
|
||||
description: res.overview,
|
||||
director: Utils.parseDirector(res.credits),
|
||||
genre: Utils.parseGenres(res.genres),
|
||||
genre: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
|
||||
imdbRating,
|
||||
name: res.title,
|
||||
released: new Date(res.release_date),
|
||||
@@ -85,10 +99,11 @@ const buildMovieResponse = async (res, type, language, tmdbId, rpdbkey, config =
|
||||
poster,
|
||||
runtime: Utils.parseRunTime(res.runtime),
|
||||
id: returnImdbId ? res.imdb_id : `tmdb:${tmdbId}`,
|
||||
genres: Utils.parseGenres(res.genres),
|
||||
genres: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
|
||||
ageRating,
|
||||
releaseInfo: res.release_date ? res.release_date.substr(0, 4) : "",
|
||||
trailerStreams: Utils.parseTrailerStream(res.videos),
|
||||
links: buildLinks(imdbRating, res.imdb_id, res.title, type, res.genres, res.credits, language, castCount),
|
||||
links: buildLinks(imdbRating, res.imdb_id, res.title, type, res.genres, res.credits, language, castCount, ageRating, showAgeRatingInGenres),
|
||||
behaviorHints: {
|
||||
defaultVideoId: res.imdb_id ? res.imdb_id : `tmdb:${res.id}`,
|
||||
hasScheduledVideos: false
|
||||
@@ -114,7 +129,7 @@ const fetchTvData = async (tmdbId, language) => {
|
||||
const buildTvResponse = async (res, type, language, tmdbId, rpdbkey, config = {}) => {
|
||||
const runtime = res.episode_run_time?.[0] ?? res.last_episode_to_air?.runtime ?? res.next_episode_to_air?.runtime ?? null;
|
||||
|
||||
const [poster, logo, imdbRatingRaw, episodes] = await Promise.all([
|
||||
const [poster, logo, imdbRatingRaw, episodes, ageRating] = await Promise.all([
|
||||
Utils.parsePoster(type, tmdbId, res.poster_path, language, rpdbkey),
|
||||
getTvLogo(res.external_ids?.tvdb_id, res.id, language, res.original_language).catch(e => {
|
||||
console.warn(`Error fetching logo for series ${tmdbId}:`, e.message);
|
||||
@@ -126,6 +141,10 @@ const buildTvResponse = async (res, type, language, tmdbId, rpdbkey, config = {}
|
||||
}).catch(e => {
|
||||
console.warn(`Error fetching episodes for series ${tmdbId}:`, e.message);
|
||||
return [];
|
||||
}),
|
||||
getCachedAgeRating(tmdbId, type, language).catch(e => {
|
||||
console.warn(`Error fetching age rating for series ${tmdbId}:`, e.message);
|
||||
return null;
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -133,11 +152,13 @@ const buildTvResponse = async (res, type, language, tmdbId, rpdbkey, config = {}
|
||||
const castCount = config.castCount
|
||||
const returnImdbId = config.returnImdbId === true || config.returnImdbId === "true";
|
||||
const hideInCinemaTag = config.hideInCinemaTag === true || config.hideInCinemaTag === "true";
|
||||
const parsedGenres = Utils.parseGenres(res.genres);
|
||||
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false; // Default to true
|
||||
|
||||
const response = {
|
||||
country: Utils.parseCoutry(res.production_countries),
|
||||
description: res.overview,
|
||||
genre: Utils.parseGenres(res.genres),
|
||||
genre: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
|
||||
imdbRating,
|
||||
imdb_id: res.external_ids.imdb_id,
|
||||
name: res.name,
|
||||
@@ -151,10 +172,11 @@ const buildTvResponse = async (res, type, language, tmdbId, rpdbkey, config = {}
|
||||
background: `https://image.tmdb.org/t/p/original${res.backdrop_path}`,
|
||||
slug: Utils.parseSlug(type, res.name, res.external_ids.imdb_id),
|
||||
id: returnImdbId ? res.imdb_id : `tmdb:${tmdbId}`,
|
||||
genres: Utils.parseGenres(res.genres),
|
||||
genres: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
|
||||
ageRating,
|
||||
releaseInfo: Utils.parseYear(res.status, res.first_air_date, res.last_air_date),
|
||||
videos: episodes || [],
|
||||
links: buildLinks(imdbRating, res.external_ids.imdb_id, res.name, type, res.genres, res.credits, language, castCount),
|
||||
links: buildLinks(imdbRating, res.external_ids.imdb_id, res.name, type, res.genres, res.credits, language, castCount, ageRating, showAgeRatingInGenres),
|
||||
trailers: Utils.parseTrailers(res.videos),
|
||||
trailerStreams: Utils.parseTrailerStream(res.videos),
|
||||
behaviorHints: {
|
||||
@@ -184,15 +206,16 @@ const buildTvResponse = async (res, type, language, tmdbId, rpdbkey, config = {}
|
||||
|
||||
// Main function
|
||||
async function getMeta(type, language, tmdbId, rpdbkey, config = {}) {
|
||||
const cacheKey = getCacheKey(type, language, tmdbId, rpdbkey);
|
||||
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false; // Default to true
|
||||
const cacheKey = getCacheKey(type, language, tmdbId, rpdbkey, showAgeRatingInGenres);
|
||||
const cachedData = cache.get(cacheKey);
|
||||
|
||||
|
||||
if (cachedData && (Date.now() - cachedData.timestamp) < CACHE_TTL) {
|
||||
return Promise.resolve({ meta: cachedData.data });
|
||||
}
|
||||
|
||||
try {
|
||||
const meta = await (type === "movie" ?
|
||||
const meta = await (type === "movie" ?
|
||||
fetchMovieData(tmdbId, language).then(res => buildMovieResponse(res, type, language, tmdbId, rpdbkey, config)) :
|
||||
fetchTvData(tmdbId, language).then(res => buildTvResponse(res, type, language, tmdbId, rpdbkey, config))
|
||||
);
|
||||
|
||||
@@ -87,8 +87,18 @@ function parseShareLink(title, imdb_id, type) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseGenreLink(genres, type, language) {
|
||||
return genres.map((genre) => {
|
||||
function parseImdbParentalGuideLink(imdbId, ageRating) {
|
||||
if (!imdbId || !ageRating) return null;
|
||||
|
||||
return {
|
||||
name: ageRating,
|
||||
category: "Genres",
|
||||
url: `https://www.imdb.com/title/${imdbId}/parentalguide`
|
||||
};
|
||||
}
|
||||
|
||||
function parseGenreLink(genres, type, language, imdbId = null, ageRating = null, showAgeRatingInGenres = true) {
|
||||
const genreLinks = genres.map((genre) => {
|
||||
return {
|
||||
name: genre.name,
|
||||
category: "Genres",
|
||||
@@ -99,6 +109,16 @@ function parseGenreLink(genres, type, language) {
|
||||
)}`,
|
||||
};
|
||||
});
|
||||
|
||||
// Add IMDb parental guide link as first genre if available and enabled
|
||||
if (showAgeRatingInGenres) {
|
||||
const parentalGuideLink = parseImdbParentalGuideLink(imdbId, ageRating);
|
||||
if (parentalGuideLink) {
|
||||
return [parentalGuideLink, ...genreLinks];
|
||||
}
|
||||
}
|
||||
|
||||
return genreLinks;
|
||||
}
|
||||
|
||||
function parseCreditsLink(credits, castCount) {
|
||||
@@ -151,7 +171,7 @@ function parseRunTime(runtime) {
|
||||
if (runtime === 0 || !runtime) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
const hours = Math.floor(runtime / 60);
|
||||
const minutes = runtime % 60;
|
||||
|
||||
@@ -168,12 +188,12 @@ function parseCreatedBy(created_by) {
|
||||
|
||||
function parseConfig(catalogChoices) {
|
||||
let config = {};
|
||||
|
||||
|
||||
// If catalogChoices is null, undefined or empty, return empty object
|
||||
if (!catalogChoices) {
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Try to decompress with lz-string
|
||||
const decoded = decompressFromEncodedURIComponent(catalogChoices);
|
||||
@@ -200,7 +220,7 @@ async function parsePoster(type, id, poster, language, rpdbkey) {
|
||||
}
|
||||
|
||||
function parseMedia(el, type, genreList = []) {
|
||||
const genres = Array.isArray(el.genre_ids)
|
||||
const genres = Array.isArray(el.genre_ids)
|
||||
? el.genre_ids.map(genre => genreList.find((x) => x.id === genre)?.name || 'Unknown')
|
||||
: [];
|
||||
|
||||
@@ -249,6 +269,7 @@ module.exports = {
|
||||
parseTrailerStream,
|
||||
parseImdbLink,
|
||||
parseShareLink,
|
||||
parseImdbParentalGuideLink,
|
||||
parseGenreLink,
|
||||
parseCreditsLink,
|
||||
parseCoutry,
|
||||
|
||||
Reference in New Issue
Block a user