Compare commits
3 Commits
renovate/e
...
advanced-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
503c099cd1 | ||
|
|
4afcfbb598 | ||
|
|
7ec5123cd0 |
@@ -7755,6 +7755,32 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/OverrideRule'
|
$ref: '#/components/schemas/OverrideRule'
|
||||||
|
/overrideRule/advancedRequest:
|
||||||
|
post:
|
||||||
|
summary: Advanced override rule request
|
||||||
|
description: Processes an advanced override rule request.
|
||||||
|
tags:
|
||||||
|
- overriderule
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Advanced override rule request processed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
rootFolder:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
profileId:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
- apiKey: []
|
- apiKey: []
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
|
||||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
MediaType,
|
MediaType,
|
||||||
} from '@server/constants/media';
|
} from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import OverrideRule from '@server/entity/OverrideRule';
|
|
||||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||||
|
import overrideRules from '@server/lib/overrideRules';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
@@ -211,121 +209,20 @@ export class MediaRequest {
|
|||||||
let tags = requestBody.tags;
|
let tags = requestBody.tags;
|
||||||
|
|
||||||
if (useOverrides) {
|
if (useOverrides) {
|
||||||
const defaultRadarrId = requestBody.is4k
|
const overrideRulesResult = await overrideRules({
|
||||||
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
mediaType: requestBody.mediaType,
|
||||||
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
is4k: requestBody.is4k || false,
|
||||||
const defaultSonarrId = requestBody.is4k
|
tmdbMedia,
|
||||||
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
|
requestUser,
|
||||||
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
|
|
||||||
|
|
||||||
const overrideRuleRepository = getRepository(OverrideRule);
|
|
||||||
const overrideRules = await overrideRuleRepository.find({
|
|
||||||
where:
|
|
||||||
requestBody.mediaType === MediaType.MOVIE
|
|
||||||
? { radarrServiceId: defaultRadarrId }
|
|
||||||
: { sonarrServiceId: defaultSonarrId },
|
|
||||||
});
|
});
|
||||||
|
if (overrideRulesResult.rootFolder) {
|
||||||
const appliedOverrideRules = overrideRules.filter((rule) => {
|
rootFolder = overrideRulesResult.rootFolder;
|
||||||
const hasAnimeKeyword =
|
}
|
||||||
'results' in tmdbMedia.keywords &&
|
if (overrideRulesResult.profileId) {
|
||||||
tmdbMedia.keywords.results.some(
|
profileId = overrideRulesResult.profileId;
|
||||||
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
}
|
||||||
);
|
if (overrideRulesResult.tags) {
|
||||||
|
tags = overrideRulesResult.tags;
|
||||||
// Skip override rules if the media is an anime TV show as anime TV
|
|
||||||
// is handled by default and override rules do not explicitly include
|
|
||||||
// the anime keyword
|
|
||||||
if (
|
|
||||||
requestBody.mediaType === MediaType.TV &&
|
|
||||||
hasAnimeKeyword &&
|
|
||||||
(!rule.keywords ||
|
|
||||||
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
rule.users &&
|
|
||||||
!rule.users
|
|
||||||
.split(',')
|
|
||||||
.some((userId) => Number(userId) === requestUser.id)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
rule.genre &&
|
|
||||||
!rule.genre
|
|
||||||
.split(',')
|
|
||||||
.some((genreId) =>
|
|
||||||
tmdbMedia.genres
|
|
||||||
.map((genre) => genre.id)
|
|
||||||
.includes(Number(genreId))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
rule.language &&
|
|
||||||
!rule.language
|
|
||||||
.split('|')
|
|
||||||
.some((languageId) => languageId === tmdbMedia.original_language)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
rule.keywords &&
|
|
||||||
!rule.keywords.split(',').some((keywordId) => {
|
|
||||||
let keywordList: TmdbKeyword[] = [];
|
|
||||||
|
|
||||||
if ('keywords' in tmdbMedia.keywords) {
|
|
||||||
keywordList = tmdbMedia.keywords.keywords;
|
|
||||||
} else if ('results' in tmdbMedia.keywords) {
|
|
||||||
keywordList = tmdbMedia.keywords.results;
|
|
||||||
}
|
|
||||||
|
|
||||||
return keywordList
|
|
||||||
.map((keyword: TmdbKeyword) => keyword.id)
|
|
||||||
.includes(Number(keywordId));
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// hacky way to prioritize rules
|
|
||||||
// TODO: make this better
|
|
||||||
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
|
|
||||||
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
|
|
||||||
|
|
||||||
const aSpecificity = keys.filter((key) => a[key] !== null).length;
|
|
||||||
const bSpecificity = keys.filter((key) => b[key] !== null).length;
|
|
||||||
|
|
||||||
// Take the rule with the most specific condition first
|
|
||||||
return bSpecificity - aSpecificity;
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
if (prioritizedRule) {
|
|
||||||
if (prioritizedRule.rootFolder) {
|
|
||||||
rootFolder = prioritizedRule.rootFolder;
|
|
||||||
}
|
|
||||||
if (prioritizedRule.profileId) {
|
|
||||||
profileId = prioritizedRule.profileId;
|
|
||||||
}
|
|
||||||
if (prioritizedRule.tags) {
|
|
||||||
tags = [
|
|
||||||
...new Set([
|
|
||||||
...(tags || []),
|
|
||||||
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('Override rule applied.', {
|
|
||||||
label: 'Media Request',
|
|
||||||
overrides: prioritizedRule,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
152
server/lib/overrideRules.ts
Normal file
152
server/lib/overrideRules.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
|
import type {
|
||||||
|
TmdbKeyword,
|
||||||
|
TmdbMovieDetails,
|
||||||
|
TmdbTvDetails,
|
||||||
|
} from '@server/api/themoviedb/interfaces';
|
||||||
|
import { MediaType } from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import OverrideRule from '@server/entity/OverrideRule';
|
||||||
|
import type { User } from '@server/entity/User';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
|
||||||
|
export type OverrideRulesResult = {
|
||||||
|
rootFolder: string | null;
|
||||||
|
profileId: number | null;
|
||||||
|
tags: number[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function overrideRules({
|
||||||
|
mediaType,
|
||||||
|
is4k,
|
||||||
|
tmdbMedia,
|
||||||
|
requestUser,
|
||||||
|
}: {
|
||||||
|
mediaType: MediaType;
|
||||||
|
is4k: boolean;
|
||||||
|
tmdbMedia: TmdbMovieDetails | TmdbTvDetails;
|
||||||
|
requestUser: User;
|
||||||
|
}): Promise<OverrideRulesResult> {
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
let rootFolder: string | null = null;
|
||||||
|
let profileId: number | null = null;
|
||||||
|
let tags: number[] | null = null;
|
||||||
|
|
||||||
|
const defaultRadarrId = is4k
|
||||||
|
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||||
|
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||||
|
const defaultSonarrId = is4k
|
||||||
|
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
|
||||||
|
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
|
||||||
|
|
||||||
|
const overrideRuleRepository = getRepository(OverrideRule);
|
||||||
|
const overrideRules = await overrideRuleRepository.find({
|
||||||
|
where:
|
||||||
|
mediaType === MediaType.MOVIE
|
||||||
|
? { radarrServiceId: defaultRadarrId }
|
||||||
|
: { sonarrServiceId: defaultSonarrId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const appliedOverrideRules = overrideRules.filter((rule) => {
|
||||||
|
const hasAnimeKeyword =
|
||||||
|
'results' in tmdbMedia.keywords &&
|
||||||
|
tmdbMedia.keywords.results.some(
|
||||||
|
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip override rules if the media is an anime TV show as anime TV
|
||||||
|
// is handled by default and override rules do not explicitly include
|
||||||
|
// the anime keyword
|
||||||
|
if (
|
||||||
|
mediaType === MediaType.TV &&
|
||||||
|
hasAnimeKeyword &&
|
||||||
|
(!rule.keywords ||
|
||||||
|
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
rule.users &&
|
||||||
|
!rule.users.split(',').some((userId) => Number(userId) === requestUser.id)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rule.genre &&
|
||||||
|
!rule.genre
|
||||||
|
.split(',')
|
||||||
|
.some((genreId) =>
|
||||||
|
tmdbMedia.genres.map((genre) => genre.id).includes(Number(genreId))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rule.language &&
|
||||||
|
!rule.language
|
||||||
|
.split('|')
|
||||||
|
.some((languageId) => languageId === tmdbMedia.original_language)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rule.keywords &&
|
||||||
|
!rule.keywords.split(',').some((keywordId) => {
|
||||||
|
let keywordList: TmdbKeyword[] = [];
|
||||||
|
|
||||||
|
if ('keywords' in tmdbMedia.keywords) {
|
||||||
|
keywordList = tmdbMedia.keywords.keywords;
|
||||||
|
} else if ('results' in tmdbMedia.keywords) {
|
||||||
|
keywordList = tmdbMedia.keywords.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keywordList
|
||||||
|
.map((keyword: TmdbKeyword) => keyword.id)
|
||||||
|
.includes(Number(keywordId));
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// hacky way to prioritize rules
|
||||||
|
// TODO: make this better
|
||||||
|
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
|
||||||
|
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
|
||||||
|
|
||||||
|
const aSpecificity = keys.filter((key) => a[key] !== null).length;
|
||||||
|
const bSpecificity = keys.filter((key) => b[key] !== null).length;
|
||||||
|
|
||||||
|
// Take the rule with the most specific condition first
|
||||||
|
return bSpecificity - aSpecificity;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (prioritizedRule) {
|
||||||
|
if (prioritizedRule.rootFolder) {
|
||||||
|
rootFolder = prioritizedRule.rootFolder;
|
||||||
|
}
|
||||||
|
if (prioritizedRule.profileId) {
|
||||||
|
profileId = prioritizedRule.profileId;
|
||||||
|
}
|
||||||
|
if (prioritizedRule.tags) {
|
||||||
|
tags = [
|
||||||
|
...new Set([
|
||||||
|
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Override rule applied.', {
|
||||||
|
label: 'Media Request',
|
||||||
|
overrides: prioritizedRule,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rootFolder, profileId, tags };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default overrideRules;
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
|
import { MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import OverrideRule from '@server/entity/OverrideRule';
|
import OverrideRule from '@server/entity/OverrideRule';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
||||||
|
import overrideRules, {
|
||||||
|
type OverrideRulesResult,
|
||||||
|
} from '@server/lib/overrideRules';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { isAuthenticated } from '@server/middleware/auth';
|
import { isAuthenticated } from '@server/middleware/auth';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
@@ -61,6 +67,40 @@ overrideRuleRoutes.post<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
overrideRuleRoutes.post(
|
||||||
|
'/advancedRequest',
|
||||||
|
isAuthenticated(Permission.REQUEST_ADVANCED),
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
const tmdbMedia =
|
||||||
|
req.body.mediaType === MediaType.MOVIE
|
||||||
|
? await tmdb.getMovie({ movieId: req.body.tmdbId })
|
||||||
|
: await tmdb.getTvShow({ tvId: req.body.tmdbId });
|
||||||
|
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const user = await userRepository.findOne({
|
||||||
|
where: { id: req.body.requestUser },
|
||||||
|
relations: { requests: true },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
return next({ status: 404, message: 'User not found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const overrideRulesResult: OverrideRulesResult = await overrideRules({
|
||||||
|
mediaType: req.body.mediaType,
|
||||||
|
is4k: req.body.is4k,
|
||||||
|
tmdbMedia,
|
||||||
|
requestUser: user,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json(overrideRulesResult);
|
||||||
|
} catch {
|
||||||
|
next({ status: 404, message: 'Media not found' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
overrideRuleRoutes.put<
|
overrideRuleRoutes.put<
|
||||||
{ ruleId: string },
|
{ ruleId: string },
|
||||||
OverrideRule,
|
OverrideRule,
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import type {
|
|||||||
ServiceCommonServerWithDetails,
|
ServiceCommonServerWithDetails,
|
||||||
} from '@server/interfaces/api/serviceInterfaces';
|
} from '@server/interfaces/api/serviceInterfaces';
|
||||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||||
|
import type { OverrideRulesResult } from '@server/lib/overrideRules';
|
||||||
import { hasPermission } from '@server/lib/permissions';
|
import { hasPermission } from '@server/lib/permissions';
|
||||||
|
import axios from 'axios';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -51,6 +53,7 @@ export type RequestOverrides = {
|
|||||||
|
|
||||||
interface AdvancedRequesterProps {
|
interface AdvancedRequesterProps {
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv';
|
||||||
|
tmdbId?: number;
|
||||||
is4k: boolean;
|
is4k: boolean;
|
||||||
isAnime?: boolean;
|
isAnime?: boolean;
|
||||||
defaultOverrides?: RequestOverrides;
|
defaultOverrides?: RequestOverrides;
|
||||||
@@ -60,6 +63,7 @@ interface AdvancedRequesterProps {
|
|||||||
|
|
||||||
const AdvancedRequester = ({
|
const AdvancedRequester = ({
|
||||||
type,
|
type,
|
||||||
|
tmdbId,
|
||||||
is4k = false,
|
is4k = false,
|
||||||
isAnime = false,
|
isAnime = false,
|
||||||
defaultOverrides,
|
defaultOverrides,
|
||||||
@@ -284,6 +288,35 @@ const AdvancedRequester = ({
|
|||||||
selectedTags,
|
selectedTags,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (tmdbId) {
|
||||||
|
try {
|
||||||
|
const { data: override } = await axios.post<OverrideRulesResult>(
|
||||||
|
'/api/v1/overrideRule/advancedRequest',
|
||||||
|
{
|
||||||
|
mediaType: type,
|
||||||
|
is4k,
|
||||||
|
requestUser: requestUser?.id ?? currentUser?.id,
|
||||||
|
tmdbId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (override.rootFolder) {
|
||||||
|
setSelectedFolder(override.rootFolder);
|
||||||
|
}
|
||||||
|
if (override.profileId) {
|
||||||
|
setSelectedProfile(override.profileId);
|
||||||
|
}
|
||||||
|
if (override.tags) {
|
||||||
|
setSelectedTags(override.tags);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [serverData, is4k, requestUser]);
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 w-full">
|
<div className="mb-2 w-full">
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const MovieRequestModal = ({
|
|||||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||||
<AdvancedRequester
|
<AdvancedRequester
|
||||||
type="movie"
|
type="movie"
|
||||||
|
tmdbId={tmdbId}
|
||||||
is4k={is4k}
|
is4k={is4k}
|
||||||
requestUser={editRequest.requestedBy}
|
requestUser={editRequest.requestedBy}
|
||||||
defaultOverrides={{
|
defaultOverrides={{
|
||||||
@@ -357,6 +358,7 @@ const MovieRequestModal = ({
|
|||||||
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
||||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||||
<AdvancedRequester
|
<AdvancedRequester
|
||||||
|
tmdbId={tmdbId}
|
||||||
type="movie"
|
type="movie"
|
||||||
is4k={is4k}
|
is4k={is4k}
|
||||||
onChange={(overrides) => {
|
onChange={(overrides) => {
|
||||||
|
|||||||
@@ -722,6 +722,7 @@ const TvRequestModal = ({
|
|||||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||||
<AdvancedRequester
|
<AdvancedRequester
|
||||||
type="tv"
|
type="tv"
|
||||||
|
tmdbId={tmdbId}
|
||||||
is4k={is4k}
|
is4k={is4k}
|
||||||
isAnime={data?.keywords.some(
|
isAnime={data?.keywords.some(
|
||||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
|
|||||||
Reference in New Issue
Block a user