feat(ageRating): add age rating in imdb link and genres based on config

This commit is contained in:
Bimal Timilsina
2025-10-12 13:25:28 +05:45
parent 526ab579f7
commit b7438492d5
8 changed files with 195 additions and 56 deletions
+29 -17
View File
@@ -78,16 +78,16 @@ addon.get('/:catalogChoices?/configure', function (req, res) {
});
addon.get("/:catalogChoices?/manifest.json", async function (req, res) {
const { catalogChoices } = req.params;
const config = parseConfig(catalogChoices) || {};
const manifest = await getManifest(config);
const cacheOpts = {
cacheMaxAge: 12 * 60 * 60,
staleRevalidate: 14 * 24 * 60 * 60,
staleError: 30 * 24 * 60 * 60,
};
respond(res, manifest, cacheOpts);
const { catalogChoices } = req.params;
const config = parseConfig(catalogChoices) || {};
const manifest = await getManifest(config);
const cacheOpts = {
cacheMaxAge: 12 * 60 * 60,
staleRevalidate: 14 * 24 * 60 * 60,
staleError: 30 * 24 * 60 * 60,
};
respond(res, manifest, cacheOpts);
});
addon.get("/:catalogChoices?/catalog/:type/:id/:extra?.json", async function (req, res) {
@@ -129,7 +129,7 @@ addon.get("/:catalogChoices?/catalog/:type/:id/:extra?.json", async function (re
return;
}
const cacheOpts = {
cacheMaxAge: 1 * 24 * 60 * 60,
cacheMaxAge: 1 * 24 * 60 * 60,
staleRevalidate: 7 * 24 * 60 * 60,
staleError: 14 * 24 * 60 * 60,
};
@@ -137,7 +137,7 @@ addon.get("/:catalogChoices?/catalog/:type/:id/:extra?.json", async function (re
try {
metas = JSON.parse(JSON.stringify(metas));
metas.metas = await Promise.all(metas.metas.map(async (el) => {
const rpdbImage = getRpdbPoster(type, el.id.replace('tmdb:', ''), language, rpdbkey)
const rpdbImage = getRpdbPoster(type, el.id.replace('tmdb:', ''), language, rpdbkey)
el.poster = await checkIfExists(rpdbImage) ? rpdbImage : el.poster;
return el;
}))
@@ -156,7 +156,13 @@ addon.get("/:catalogChoices?/meta/:type/:id.json", async function (req, res) {
if (req.params.id.includes("tmdb:")) {
const resp = await cacheWrapMeta(`${language}:${type}:${tmdbId}`, async () => {
return await getMeta(type, language, tmdbId, rpdbkey, config);
return await getMeta(type, language, tmdbId, rpdbkey, {
...config,
hideEpisodeThumbnails: config.hideEpisodeThumbnails === "true",
enableAgeRating: config.enableAgeRating === "true",
showAgeRatingInGenres: config.showAgeRatingInGenres !== "false",
showAgeRatingWithImdbRating: config.showAgeRatingWithImdbRating === "true"
});
});
const cacheOpts = {
staleRevalidate: 20 * 24 * 60 * 60,
@@ -174,7 +180,13 @@ addon.get("/:catalogChoices?/meta/:type/:id.json", async function (req, res) {
const tmdbId = await getTmdb(type, imdbId);
if (tmdbId) {
const resp = await cacheWrapMeta(`${language}:${type}:${tmdbId}`, async () => {
return await getMeta(type, language, tmdbId, rpdbkey, config);
return await getMeta(type, language, tmdbId, rpdbkey, {
...config,
hideEpisodeThumbnails: config.hideEpisodeThumbnails === "true",
enableAgeRating: config.enableAgeRating === "true",
showAgeRatingInGenres: config.showAgeRatingInGenres !== "false",
showAgeRatingWithImdbRating: config.showAgeRatingWithImdbRating === "true"
});
});
const cacheOpts = {
staleRevalidate: 20 * 24 * 60 * 60,
@@ -216,21 +228,21 @@ addon.get("/api/proxy/status", async function (req, res) {
addon.get("/api/image/blur", async function (req, res) {
const imageUrl = req.query.url;
if (!imageUrl) {
return res.status(400).json({ error: 'Image URL not provided' });
}
try {
const blurredImageBuffer = await blurImage(imageUrl);
if (!blurredImageBuffer) {
return res.status(500).json({ error: 'Error processing image' });
}
res.setHeader('Content-Type', 'image/jpeg');
res.setHeader('Cache-Control', 'public, max-age=31536000');
res.send(blurredImageBuffer);
} catch (error) {
console.error('Error in blur route:', error);
+98 -26
View File
@@ -29,20 +29,40 @@ async function getCachedImdbRating(imdbId, type) {
}
// Helper functions
const getCacheKey = (type, language, tmdbId, rpdbkey, showAgeRatingInGenres = true) =>
`${type}-${language}-${tmdbId}-${rpdbkey}-ageRating:${showAgeRatingInGenres}`;
const getCacheKey = (
type,
language,
tmdbId,
rpdbkey,
enableAgeRating = false,
showAgeRatingInGenres = true,
showAgeRatingWithImdbRating = false
) =>
`${type}-${language}-${tmdbId}-${rpdbkey}-ageRating:${enableAgeRating}-${showAgeRatingInGenres}-${showAgeRatingWithImdbRating}`;
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, ageRating = null, showAgeRatingInGenres = true) => [
Utils.parseImdbLink(imdbRating, imdbId),
Utils.parseShareLink(title, imdbId, type),
...Utils.parseGenreLink(genres, type, language, imdbId, ageRating, showAgeRatingInGenres),
...Utils.parseCreditsLink(credits, castCount)
];
const buildLinks = (
imdbRating,
imdbId,
title,
type,
genres,
credits,
language,
castCount,
ageRating = null,
showAgeRatingInGenres = true,
showAgeRatingWithImdbRating = false
) => [
Utils.parseImdbLink(imdbRating, imdbId, ageRating, showAgeRatingWithImdbRating),
Utils.parseShareLink(title, imdbId, type),
...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) => {
@@ -60,6 +80,10 @@ const fetchMovieData = async (tmdbId, language) => {
};
const buildMovieResponse = async (res, type, language, tmdbId, rpdbkey, config = {}) => {
const enableAgeRating = config.enableAgeRating === true || config.enableAgeRating === "true";
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false && config.showAgeRatingInGenres !== "false";
const showAgeRatingWithImdbRating = config.showAgeRatingWithImdbRating === true || config.showAgeRatingWithImdbRating === "true";
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 => {
@@ -67,10 +91,10 @@ const buildMovieResponse = async (res, type, language, tmdbId, rpdbkey, config =
return null;
}),
getCachedImdbRating(res.external_ids?.imdb_id, type),
getCachedAgeRating(tmdbId, type, language).catch(e => {
enableAgeRating ? getCachedAgeRating(tmdbId, type, language).catch(e => {
console.warn(`Error fetching age rating for movie ${tmdbId}:`, e.message);
return null;
}),
}) : Promise.resolve(null),
]);
const imdbRating = imdbRatingRaw || res.vote_average?.toFixed(1) || "N/A";
@@ -79,14 +103,14 @@ const buildMovieResponse = async (res, type, language, tmdbId, rpdbkey, config =
const hideInCinemaTag = config.hideInCinemaTag === true || config.hideInCinemaTag === "true";
const parsedGenres = Utils.parseGenres(res.genres);
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false; // Default to true
const resolvedAgeRating = enableAgeRating ? ageRating : null;
const response = {
imdb_id: res.imdb_id,
country: Utils.parseCoutry(res.production_countries),
description: res.overview,
director: Utils.parseDirector(res.credits),
genre: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
genre: addAgeRatingToGenres(resolvedAgeRating, parsedGenres, showAgeRatingInGenres),
imdbRating,
name: res.title,
released: new Date(res.release_date),
@@ -99,11 +123,23 @@ const buildMovieResponse = async (res, type, language, tmdbId, rpdbkey, config =
poster,
runtime: Utils.parseRunTime(res.runtime),
id: returnImdbId ? res.imdb_id : `tmdb:${tmdbId}`,
genres: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
ageRating,
genres: addAgeRatingToGenres(resolvedAgeRating, parsedGenres, showAgeRatingInGenres),
ageRating: resolvedAgeRating,
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, ageRating, showAgeRatingInGenres),
links: buildLinks(
imdbRating,
res.imdb_id,
res.title,
type,
res.genres,
res.credits,
language,
castCount,
resolvedAgeRating,
showAgeRatingInGenres,
showAgeRatingWithImdbRating
),
behaviorHints: {
defaultVideoId: res.imdb_id ? res.imdb_id : `tmdb:${res.id}`,
hasScheduledVideos: false
@@ -128,6 +164,9 @@ 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 enableAgeRating = config.enableAgeRating === true || config.enableAgeRating === "true";
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false && config.showAgeRatingInGenres !== "false";
const showAgeRatingWithImdbRating = config.showAgeRatingWithImdbRating === true || config.showAgeRatingWithImdbRating === "true";
const [poster, logo, imdbRatingRaw, episodes, ageRating] = await Promise.all([
Utils.parsePoster(type, tmdbId, res.poster_path, language, rpdbkey),
@@ -142,10 +181,10 @@ const buildTvResponse = async (res, type, language, tmdbId, rpdbkey, config = {}
console.warn(`Error fetching episodes for series ${tmdbId}:`, e.message);
return [];
}),
getCachedAgeRating(tmdbId, type, language).catch(e => {
enableAgeRating ? getCachedAgeRating(tmdbId, type, language).catch(e => {
console.warn(`Error fetching age rating for series ${tmdbId}:`, e.message);
return null;
})
}) : Promise.resolve(null)
]);
const imdbRating = imdbRatingRaw || res.vote_average?.toFixed(1) || "N/A";
@@ -153,12 +192,12 @@ const buildTvResponse = 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 resolvedAgeRating = enableAgeRating ? ageRating : null;
const response = {
country: Utils.parseCoutry(res.production_countries),
description: res.overview,
genre: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
genre: addAgeRatingToGenres(resolvedAgeRating, parsedGenres, showAgeRatingInGenres),
imdbRating,
imdb_id: res.external_ids.imdb_id,
name: res.name,
@@ -172,11 +211,23 @@ 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: addAgeRatingToGenres(ageRating, parsedGenres, showAgeRatingInGenres),
ageRating,
genres: addAgeRatingToGenres(resolvedAgeRating, parsedGenres, showAgeRatingInGenres),
ageRating: resolvedAgeRating,
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, ageRating, showAgeRatingInGenres),
links: buildLinks(
imdbRating,
res.external_ids.imdb_id,
res.name,
type,
res.genres,
res.credits,
language,
castCount,
resolvedAgeRating,
showAgeRatingInGenres,
showAgeRatingWithImdbRating
),
trailers: Utils.parseTrailers(res.videos),
trailerStreams: Utils.parseTrailerStream(res.videos),
behaviorHints: {
@@ -206,8 +257,19 @@ const buildTvResponse = async (res, type, language, tmdbId, rpdbkey, config = {}
// Main function
async function getMeta(type, language, tmdbId, rpdbkey, config = {}) {
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false; // Default to true
const cacheKey = getCacheKey(type, language, tmdbId, rpdbkey, showAgeRatingInGenres);
const enableAgeRating = config.enableAgeRating === true || config.enableAgeRating === "true";
const showAgeRatingInGenres = config.showAgeRatingInGenres !== false && config.showAgeRatingInGenres !== "false";
const showAgeRatingWithImdbRating = config.showAgeRatingWithImdbRating === true || config.showAgeRatingWithImdbRating === "true";
const cacheKey = getCacheKey(
type,
language,
tmdbId,
rpdbkey,
enableAgeRating,
showAgeRatingInGenres,
showAgeRatingWithImdbRating
);
const cachedData = cache.get(cacheKey);
if (cachedData && (Date.now() - cachedData.timestamp) < CACHE_TTL) {
@@ -216,8 +278,18 @@ async function getMeta(type, language, tmdbId, rpdbkey, config = {}) {
try {
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))
fetchMovieData(tmdbId, language).then(res => buildMovieResponse(res, type, language, tmdbId, rpdbkey, {
...config,
enableAgeRating,
showAgeRatingInGenres,
showAgeRatingWithImdbRating
})) :
fetchTvData(tmdbId, language).then(res => buildTvResponse(res, type, language, tmdbId, rpdbkey, {
...config,
enableAgeRating,
showAgeRatingInGenres,
showAgeRatingWithImdbRating
}))
);
cache.set(cacheKey, { data: meta, timestamp: Date.now() });
+2 -2
View File
@@ -71,9 +71,9 @@ function parseTrailerStream(videos) {
});
}
function parseImdbLink(vote_average, imdb_id) {
function parseImdbLink(vote_average, imdb_id, ageRating = null, showAgeRatingWithImdbRating = false) {
return {
name: vote_average,
name: showAgeRatingWithImdbRating && ageRating ? `${ageRating}\u2003\u2003${vote_average}` : vote_average,
category: "imdb",
url: `https://imdb.com/title/${imdb_id}`,
};
@@ -2,17 +2,56 @@ import { Switch } from "@/components/ui/switch";
import { useConfig } from "@/contexts/ConfigContext";
export function AgeRatingDisplayToggle() {
const { showAgeRatingInGenres, setShowAgeRatingInGenres } = useConfig();
const {
enableAgeRating,
showAgeRatingInGenres,
showAgeRatingWithImdbRating,
setEnableAgeRating,
setShowAgeRatingInGenres,
setShowAgeRatingWithImdbRating
} = useConfig();
return (
<>
<div className="space-y-0.5">
<h1 className="text-sm font-semibold mb-1">Display Age Rating</h1>
<p className="text-gray-500 text-sm">
Display age rating as first genre
</p>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<h1 className="text-sm font-semibold mb-1">Enable Age Rating</h1>
<p className="text-gray-500 text-sm">
Fetch and attach the content rating to metadata
</p>
</div>
<Switch checked={enableAgeRating} onCheckedChange={setEnableAgeRating} />
</div>
<Switch checked={showAgeRatingInGenres} onCheckedChange={setShowAgeRatingInGenres} />
</>
<div className={`grid gap-4 transition-opacity ${enableAgeRating ? "opacity-100" : "opacity-40"}`}>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<h2 className="text-sm font-semibold mb-1">Show in Genres</h2>
<p className="text-gray-500 text-sm">
Insert the age rating as the first genre entry
</p>
</div>
<Switch
checked={showAgeRatingInGenres}
onCheckedChange={setShowAgeRatingInGenres}
disabled={!enableAgeRating}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<h2 className="text-sm font-semibold mb-1">Show with IMDb Rating</h2>
<p className="text-gray-500 text-sm">
Append the age rating next to the IMDb score
</p>
</div>
<Switch
checked={showAgeRatingWithImdbRating}
onCheckedChange={setShowAgeRatingWithImdbRating}
disabled={!enableAgeRating}
/>
</div>
</div>
</div>
);
}
+8
View File
@@ -31,6 +31,8 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
const [hideInCinemaTag, setHideInCinemaTag] = useState(false);
const [castCount, setCastCount] = useState<number | undefined>(5);
const [showAgeRatingInGenres, setShowAgeRatingInGenres] = useState(true);
const [enableAgeRating, setEnableAgeRating] = useState(false);
const [showAgeRatingWithImdbRating, setShowAgeRatingWithImdbRating] = useState(false);
const loadDefaultCatalogs = () => {
const defaultCatalogs = baseCatalogs.map(catalog => ({
@@ -60,7 +62,9 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
if (config.language) setLanguage(config.language);
if (config.hideInCinemaTag) setHideInCinemaTag(config.hideInCinemaTag === "true" || config.hideInCinemaTag === true);
if (config.castCount !== undefined) setCastCount(config.castCount === "Unlimited" ? undefined : Number(config.castCount));
if (config.enableAgeRating !== undefined) setEnableAgeRating(config.enableAgeRating === "true" || config.enableAgeRating === true);
if (config.showAgeRatingInGenres !== undefined) setShowAgeRatingInGenres(config.showAgeRatingInGenres === "true" || config.showAgeRatingInGenres === true);
if (config.showAgeRatingWithImdbRating !== undefined) setShowAgeRatingWithImdbRating(config.showAgeRatingWithImdbRating === "true" || config.showAgeRatingWithImdbRating === true);
if (config.catalogs) {
const catalogsWithNames = config.catalogs.map(catalog => {
@@ -122,6 +126,8 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
hideInCinemaTag,
castCount,
showAgeRatingInGenres,
enableAgeRating,
showAgeRatingWithImdbRating,
setRpdbkey,
setGeminiKey,
setMdblistkey,
@@ -139,6 +145,8 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) {
setHideInCinemaTag,
setCastCount,
setShowAgeRatingInGenres,
setEnableAgeRating,
setShowAgeRatingWithImdbRating,
loadConfigFromUrl
};
+4
View File
@@ -26,6 +26,8 @@ export type ConfigContextType = {
hideInCinemaTag: boolean;
castCount: number | undefined;
showAgeRatingInGenres: boolean;
enableAgeRating: boolean;
showAgeRatingWithImdbRating: boolean;
setRpdbkey: (rpdbkey: string) => void;
setGeminiKey: (geminikey: string) => void;
setMdblistkey: (mdblistkey: string) => void;
@@ -43,6 +45,8 @@ export type ConfigContextType = {
setHideInCinemaTag: (hide: boolean) => void;
setCastCount: (count: number | undefined) => void;
setShowAgeRatingInGenres: (show: boolean) => void;
setEnableAgeRating: (enable: boolean) => void;
setShowAgeRatingWithImdbRating: (show: boolean) => void;
loadConfigFromUrl: () => void;
};
+5 -1
View File
@@ -22,6 +22,8 @@ interface AddonConfig {
hideInCinemaTag?: boolean;
castCount?: number;
showAgeRatingInGenres?: boolean;
enableAgeRating?: boolean;
showAgeRatingWithImdbRating?: boolean;
}
export function generateAddonUrl(config: AddonConfig): string {
@@ -47,7 +49,9 @@ export function generateAddonUrl(config: AddonConfig): string {
searchEnabled: config.searchEnabled === false ? "false" : undefined,
hideInCinemaTag: config.hideInCinemaTag === true ? "true" : undefined,
castCount: typeof config.castCount === "number" ? config.castCount : undefined,
showAgeRatingInGenres: config.showAgeRatingInGenres === true ? "true" : undefined,
enableAgeRating: typeof config.enableAgeRating === "boolean" ? String(config.enableAgeRating) : undefined,
showAgeRatingInGenres: typeof config.showAgeRatingInGenres === "boolean" ? String(config.showAgeRatingInGenres) : undefined,
showAgeRatingWithImdbRating: typeof config.showAgeRatingWithImdbRating === "boolean" ? String(config.showAgeRatingWithImdbRating) : undefined,
};
const cleanConfig = Object.fromEntries(
+1 -1
View File
@@ -107,7 +107,7 @@ const Others = () => {
<Card className="p-6">
<AgeRatingSelect />
</Card>
<Card className="flex flex-row items-center justify-between p-6">
<Card className="p-6">
<AgeRatingDisplayToggle />
</Card>
</div>