mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-23 18:29:19 -05:00
fix(blacklist): handle invalid keywords gracefully (#1815)
* fix(blacklist): handle invalid keywords gracefully * fix(blacklist): only remove keywords on 404 errors * fix(blacklist): remove non-null assertion and add proper type annotation * refactor(blacklist): return null instead of 404 for missing keywords * fix(blacklist): add type annotation for validKeywords * fix(selector): update type annotation for validKeywords
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1054,7 +1054,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
keywordId,
|
||||
}: {
|
||||
keywordId: number;
|
||||
}): Promise<TmdbKeyword> {
|
||||
}): Promise<TmdbKeyword | null> {
|
||||
try {
|
||||
const data = await this.get<TmdbKeyword>(
|
||||
`/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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
const blacklistedTagsArr = blacklistedTags.split(',');
|
||||
|
||||
const pageLimit = settings.main.blacklistedTagsLimit;
|
||||
const invalidKeywords = new Set<string>();
|
||||
|
||||
if (blacklistedTags.length === 0) {
|
||||
return;
|
||||
@@ -87,6 +88,19 @@ class BlacklistedTagProcessor implements RunnableScanner<StatusBase> {
|
||||
|
||||
// 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<StatusBase> {
|
||||
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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<Keyword>(
|
||||
`/api/v1/keyword/${keywordId}`
|
||||
);
|
||||
return data.name;
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
const { data } = await axios.get<Keyword | null>(
|
||||
`/api/v1/keyword/${keywordId}`
|
||||
);
|
||||
return data?.name || `[Invalid: ${keywordId}]`;
|
||||
})
|
||||
).then((keywords) => {
|
||||
setTagNamesBlacklistedFor(keywords.join(', '));
|
||||
|
||||
@@ -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<Keyword>(
|
||||
const { data } = await axios.get<Keyword | null>(
|
||||
`/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,
|
||||
}))
|
||||
|
||||
@@ -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<Keyword>(
|
||||
const keyword = await axios.get<Keyword | null>(
|
||||
`/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,
|
||||
}))
|
||||
|
||||
@@ -309,16 +309,19 @@ export const KeywordSelector = ({
|
||||
|
||||
const keywords = await Promise.all(
|
||||
defaultValue.split(',').map(async (keywordId) => {
|
||||
const keyword = await axios.get<Keyword>(
|
||||
const keyword = await axios.get<Keyword | null>(
|
||||
`/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,
|
||||
}))
|
||||
|
||||
@@ -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<Keyword | null>(
|
||||
`/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)
|
||||
|
||||
Reference in New Issue
Block a user