From b7438492d5b5545e24f31d2b2a32412c08ac653a Mon Sep 17 00:00:00 2001 From: Bimal Timilsina Date: Sun, 12 Oct 2025 13:25:28 +0545 Subject: [PATCH] feat(ageRating): add age rating in imdb link and genres based on config --- addon/index.js | 46 ++++--- addon/lib/getMeta.js | 124 ++++++++++++++---- addon/utils/parseProps.js | 4 +- .../src/components/AgeRatingDisplayToggle.tsx | 57 ++++++-- configure/src/contexts/ConfigContext.tsx | 8 ++ configure/src/contexts/config.ts | 4 + configure/src/lib/config.ts | 6 +- configure/src/pages/Others.tsx | 2 +- 8 files changed, 195 insertions(+), 56 deletions(-) diff --git a/addon/index.js b/addon/index.js index 88ab23e..5fa19e5 100644 --- a/addon/index.js +++ b/addon/index.js @@ -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); diff --git a/addon/lib/getMeta.js b/addon/lib/getMeta.js index 7e37ccf..bfd86ee 100644 --- a/addon/lib/getMeta.js +++ b/addon/lib/getMeta.js @@ -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() }); diff --git a/addon/utils/parseProps.js b/addon/utils/parseProps.js index 98b3dc8..1d8a9c3 100644 --- a/addon/utils/parseProps.js +++ b/addon/utils/parseProps.js @@ -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}`, }; diff --git a/configure/src/components/AgeRatingDisplayToggle.tsx b/configure/src/components/AgeRatingDisplayToggle.tsx index fca1c83..7634581 100644 --- a/configure/src/components/AgeRatingDisplayToggle.tsx +++ b/configure/src/components/AgeRatingDisplayToggle.tsx @@ -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 ( - <> -
-

Display Age Rating

-

- Display age rating as first genre -

+
+
+
+

Enable Age Rating

+

+ Fetch and attach the content rating to metadata +

+
+
- - + +
+
+
+

Show in Genres

+

+ Insert the age rating as the first genre entry +

+
+ +
+ +
+
+

Show with IMDb Rating

+

+ Append the age rating next to the IMDb score +

+
+ +
+
+
); } diff --git a/configure/src/contexts/ConfigContext.tsx b/configure/src/contexts/ConfigContext.tsx index 4f3f51c..ce66999 100644 --- a/configure/src/contexts/ConfigContext.tsx +++ b/configure/src/contexts/ConfigContext.tsx @@ -31,6 +31,8 @@ export function ConfigProvider({ children }: { children: React.ReactNode }) { const [hideInCinemaTag, setHideInCinemaTag] = useState(false); const [castCount, setCastCount] = useState(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 }; diff --git a/configure/src/contexts/config.ts b/configure/src/contexts/config.ts index 9779d4f..f8fb9e1 100644 --- a/configure/src/contexts/config.ts +++ b/configure/src/contexts/config.ts @@ -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; }; diff --git a/configure/src/lib/config.ts b/configure/src/lib/config.ts index a62d62c..03db3aa 100644 --- a/configure/src/lib/config.ts +++ b/configure/src/lib/config.ts @@ -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( diff --git a/configure/src/pages/Others.tsx b/configure/src/pages/Others.tsx index de2194c..062fe06 100644 --- a/configure/src/pages/Others.tsx +++ b/configure/src/pages/Others.tsx @@ -107,7 +107,7 @@ const Others = () => { - +