diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index b3dc5c466..600bd81b0 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -7324,11 +7324,22 @@ paths: example: 1 responses: '200': - description: Keyword returned + description: Keyword returned (null if not found) content: application/json: schema: + nullable: true $ref: '#/components/schemas/Keyword' + '500': + description: Internal server error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Unable to retrieve keyword data.' /watchproviders/regions: get: summary: Get watch provider regions diff --git a/server/api/themoviedb/index.ts b/server/api/themoviedb/index.ts index d55e32788..9d6f5089b 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -1054,7 +1054,7 @@ class TheMovieDb extends ExternalAPI { keywordId, }: { keywordId: number; - }): Promise { + }): Promise { try { const data = await this.get( `/keyword/${keywordId}`, @@ -1064,6 +1064,9 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { + if (e.response?.status === 404) { + return null; + } throw new Error(`[TMDB] Failed to fetch keyword: ${e.message}`); } } diff --git a/server/job/blacklistedTagsProcessor.ts b/server/job/blacklistedTagsProcessor.ts index eab46a1ee..f7ca4f0f2 100644 --- a/server/job/blacklistedTagsProcessor.ts +++ b/server/job/blacklistedTagsProcessor.ts @@ -72,6 +72,7 @@ class BlacklistedTagProcessor implements RunnableScanner { const blacklistedTagsArr = blacklistedTags.split(','); const pageLimit = settings.main.blacklistedTagsLimit; + const invalidKeywords = new Set(); if (blacklistedTags.length === 0) { return; @@ -87,6 +88,19 @@ class BlacklistedTagProcessor implements RunnableScanner { // Iterate for each tag for (const tag of blacklistedTagsArr) { + const keywordDetails = await tmdb.getKeywordDetails({ + keywordId: Number(tag), + }); + + if (keywordDetails === null) { + logger.warn('Skipping invalid keyword in blacklisted tags', { + label: 'Blacklisted Tags Processor', + keywordId: tag, + }); + invalidKeywords.add(tag); + continue; + } + let queryMax = pageLimit * SortOptionsIterable.length; let fixedSortMode = false; // Set to true when the page limit allows for getting every page of tag @@ -102,24 +116,51 @@ class BlacklistedTagProcessor implements RunnableScanner { throw new AbortTransaction(); } - const response = await getDiscover({ - page, - sortBy, - keywords: tag, - }); - await this.processResults(response, tag, type, em); - await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS)); + try { + const response = await getDiscover({ + page, + sortBy, + keywords: tag, + }); - this.progress++; - if (page === 1 && response.total_pages <= queryMax) { - // We will finish the tag with less queries than expected, move progress accordingly - this.progress += queryMax - response.total_pages; - fixedSortMode = true; - queryMax = response.total_pages; + await this.processResults(response, tag, type, em); + await new Promise((res) => setTimeout(res, TMDB_API_DELAY_MS)); + + this.progress++; + if (page === 1 && response.total_pages <= queryMax) { + // We will finish the tag with less queries than expected, move progress accordingly + this.progress += queryMax - response.total_pages; + fixedSortMode = true; + queryMax = response.total_pages; + } + } catch (error) { + logger.error('Error processing keyword in blacklisted tags', { + label: 'Blacklisted Tags Processor', + keywordId: tag, + errorMessage: error.message, + }); } } } } + + if (invalidKeywords.size > 0) { + const currentTags = blacklistedTagsArr.filter( + (tag) => !invalidKeywords.has(tag) + ); + const cleanedTags = currentTags.join(','); + + if (cleanedTags !== blacklistedTags) { + settings.main.blacklistedTags = cleanedTags; + await settings.save(); + + logger.info('Cleaned up invalid keywords from settings', { + label: 'Blacklisted Tags Processor', + removedKeywords: Array.from(invalidKeywords), + newBlacklistedTags: cleanedTags, + }); + } + } } private async processResults( diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 72688b2f3..4fdd11678 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -128,11 +128,15 @@ discoverRoutes.get('/movies', async (req, res, next) => { if (keywords) { const splitKeywords = keywords.split(','); - keywordData = await Promise.all( + const keywordResults = await Promise.all( splitKeywords.map(async (keywordId) => { return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) }); }) ); + + keywordData = keywordResults.filter( + (keyword): keyword is TmdbKeyword => keyword !== null + ); } return res.status(200).json({ @@ -415,11 +419,15 @@ discoverRoutes.get('/tv', async (req, res, next) => { if (keywords) { const splitKeywords = keywords.split(','); - keywordData = await Promise.all( + const keywordResults = await Promise.all( splitKeywords.map(async (keywordId) => { return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) }); }) ); + + keywordData = keywordResults.filter( + (keyword): keyword is TmdbKeyword => keyword !== null + ); } return res.status(200).json({ diff --git a/src/components/BlacklistedTagsBadge/index.tsx b/src/components/BlacklistedTagsBadge/index.tsx index eb1c9a475..a96272a4b 100644 --- a/src/components/BlacklistedTagsBadge/index.tsx +++ b/src/components/BlacklistedTagsBadge/index.tsx @@ -29,14 +29,10 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => { const keywordIds = data.blacklistedTags.slice(1, -1).split(','); Promise.all( keywordIds.map(async (keywordId) => { - try { - const { data } = await axios.get( - `/api/v1/keyword/${keywordId}` - ); - return data.name; - } catch (err) { - return ''; - } + const { data } = await axios.get( + `/api/v1/keyword/${keywordId}` + ); + return data?.name || `[Invalid: ${keywordId}]`; }) ).then((keywords) => { setTagNamesBlacklistedFor(keywords.join(', ')); diff --git a/src/components/BlacklistedTagsSelector/index.tsx b/src/components/BlacklistedTagsSelector/index.tsx index b83691efc..42139b59a 100644 --- a/src/components/BlacklistedTagsSelector/index.tsx +++ b/src/components/BlacklistedTagsSelector/index.tsx @@ -5,7 +5,10 @@ import { encodeURIExtraParams } from '@app/hooks/useDiscover'; import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; import { ArrowDownIcon } from '@heroicons/react/24/solid'; -import type { TmdbKeywordSearchResponse } from '@server/api/themoviedb/interfaces'; +import type { + TmdbKeyword, + TmdbKeywordSearchResponse, +} from '@server/api/themoviedb/interfaces'; import type { Keyword } from '@server/models/common'; import axios from 'axios'; import { useFormikContext } from 'formik'; @@ -124,15 +127,19 @@ const ControlledKeywordSelector = ({ const keywords = await Promise.all( defaultValue.split(',').map(async (keywordId) => { - const { data } = await axios.get( + const { data } = await axios.get( `/api/v1/keyword/${keywordId}` ); return data; }) ); + const validKeywords: TmdbKeyword[] = keywords.filter( + (keyword): keyword is TmdbKeyword => keyword !== null + ); + onChange( - keywords.map((keyword) => ({ + validKeywords.map((keyword) => ({ label: keyword.name, value: keyword.id, })) diff --git a/src/components/Discover/CreateSlider/index.tsx b/src/components/Discover/CreateSlider/index.tsx index 32cca0794..9c7493d2a 100644 --- a/src/components/Discover/CreateSlider/index.tsx +++ b/src/components/Discover/CreateSlider/index.tsx @@ -77,16 +77,19 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => { const keywords = await Promise.all( slider.data.split(',').map(async (keywordId) => { - const keyword = await axios.get( + const keyword = await axios.get( `/api/v1/keyword/${keywordId}` ); - return keyword.data; }) ); + const validKeywords: Keyword[] = keywords.filter( + (keyword): keyword is Keyword => keyword !== null + ); + setDefaultDataValue( - keywords.map((keyword) => ({ + validKeywords.map((keyword) => ({ label: keyword.name, value: keyword.id, })) diff --git a/src/components/Selector/index.tsx b/src/components/Selector/index.tsx index e6eb15ff4..b8d078876 100644 --- a/src/components/Selector/index.tsx +++ b/src/components/Selector/index.tsx @@ -309,16 +309,19 @@ export const KeywordSelector = ({ const keywords = await Promise.all( defaultValue.split(',').map(async (keywordId) => { - const keyword = await axios.get( + const keyword = await axios.get( `/api/v1/keyword/${keywordId}` ); - return keyword.data; }) ); + const validKeywords: Keyword[] = keywords.filter( + (keyword): keyword is Keyword => keyword !== null + ); + setDefaultDataValue( - keywords.map((keyword) => ({ + validKeywords.map((keyword) => ({ label: keyword.name, value: keyword.id, })) diff --git a/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx b/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx index 4208836f8..3b08a1b27 100644 --- a/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx +++ b/src/components/Settings/OverrideRule/OverrideRuleTiles.tsx @@ -113,12 +113,16 @@ const OverrideRuleTiles = ({ .flat() .filter((keywordId) => keywordId) .map(async (keywordId) => { - const response = await axios.get(`/api/v1/keyword/${keywordId}`); - const keyword: Keyword = response.data; - return keyword; + const response = await axios.get( + `/api/v1/keyword/${keywordId}` + ); + return response.data; }) ); - setKeywords(keywords); + const validKeywords: Keyword[] = keywords.filter( + (keyword): keyword is Keyword => keyword !== null + ); + setKeywords(validKeywords); const allUsersFromRules = rules .map((rule) => rule.users) .filter((users) => users)