diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index ac5aaa26..86644b44 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -7273,6 +7273,26 @@ paths: application/json: schema: $ref: '#/components/schemas/Keyword' + '404': + description: Keyword not found + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: 'Keyword not found' + '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 d55e3278..dcb94aec 100644 --- a/server/api/themoviedb/index.ts +++ b/server/api/themoviedb/index.ts @@ -1064,6 +1064,9 @@ class TheMovieDb extends ExternalAPI { return data; } catch (e) { + if (e.response?.status === 404) { + throw e; + } throw new Error(`[TMDB] Failed to fetch keyword: ${e.message}`); } } diff --git a/server/job/blacklistedTagsProcessor.ts b/server/job/blacklistedTagsProcessor.ts index eab46a1e..775cecce 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,17 @@ class BlacklistedTagProcessor implements RunnableScanner { // Iterate for each tag for (const tag of blacklistedTagsArr) { + try { + await tmdb.getKeywordDetails({ keywordId: Number(tag) }); + } catch (error) { + 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 +114,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/index.ts b/server/routes/index.ts index b2842139..dedc7b68 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -330,6 +330,10 @@ router.get('/keyword/:keywordId', async (req, res, next) => { return res.status(200).json(result); } catch (e) { + if (e.response?.status === 404) { + return res.status(404).json({ message: 'Keyword not found' }); + } + logger.debug('Something went wrong retrieving keyword data', { label: 'API', errorMessage: e.message, diff --git a/src/components/BlacklistedTagsBadge/index.tsx b/src/components/BlacklistedTagsBadge/index.tsx index eb1c9a47..b57f5df5 100644 --- a/src/components/BlacklistedTagsBadge/index.tsx +++ b/src/components/BlacklistedTagsBadge/index.tsx @@ -35,11 +35,15 @@ const BlacklistedTagsBadge = ({ data }: BlacklistedTagsBadgeProps) => { ); return data.name; } catch (err) { + if (err.response?.status === 404) { + return `[Invalid: ${keywordId}]`; + } return ''; } }) ).then((keywords) => { - setTagNamesBlacklistedFor(keywords.join(', ')); + const validKeywords = keywords.filter((name) => name !== ''); + setTagNamesBlacklistedFor(validKeywords.join(', ')); }); }, [data.blacklistedTags]); diff --git a/src/components/BlacklistedTagsSelector/index.tsx b/src/components/BlacklistedTagsSelector/index.tsx index b83691ef..74890a33 100644 --- a/src/components/BlacklistedTagsSelector/index.tsx +++ b/src/components/BlacklistedTagsSelector/index.tsx @@ -124,17 +124,23 @@ const ControlledKeywordSelector = ({ const keywords = await Promise.all( defaultValue.split(',').map(async (keywordId) => { - const { data } = await axios.get( - `/api/v1/keyword/${keywordId}` - ); - return data; + try { + const { data } = await axios.get( + `/api/v1/keyword/${keywordId}` + ); + return data; + } catch (error) { + return null; + } }) ); + const validKeywords = keywords.filter((keyword) => keyword !== null); + onChange( - keywords.map((keyword) => ({ - label: keyword.name, - value: keyword.id, + validKeywords.map((keyword) => ({ + label: keyword!.name, + value: keyword!.id, })) ); };