mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-30 21:49:11 -05:00
Compare commits
3 Commits
0xsysr3ll/
...
advanced-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
503c099cd1 | ||
|
|
4afcfbb598 | ||
|
|
7ec5123cd0 |
@@ -2,6 +2,7 @@
|
||||
.next/
|
||||
dist/
|
||||
config/
|
||||
CHANGELOG.md
|
||||
pnpm-lock.yaml
|
||||
cypress/config/settings.cypress.json
|
||||
|
||||
|
||||
1216
CHANGELOG.md
Normal file
1216
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -151,9 +151,9 @@ When adding new UI text, please try to adhere to the following guidelines:
|
||||
|
||||
## Translation
|
||||
|
||||
We use [Weblate](https://translate.seerr.dev/projects/seerr/seerr-frontend/) for our translations, and your help with localizing Seerr would be greatly appreciated! If your language is not listed below, please [open a feature request](/../../issues/new/choose).
|
||||
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Seerr would be greatly appreciated! If your language is not listed below, please [open a feature request](/../../issues/new/choose).
|
||||
|
||||
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/multi-auto.svg" alt="Translation status" /></a>
|
||||
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>
|
||||
|
||||
## Migrations
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
||||
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
|
||||
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="http://translate.seerr.dev/engage/seerr/"><img src="http://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
||||
|
||||
**Seerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||
|
||||
@@ -7755,6 +7755,32 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
- cookieAuth: []
|
||||
- apiKey: []
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||
import overrideRules from '@server/lib/overrideRules';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
@@ -211,121 +209,20 @@ export class MediaRequest {
|
||||
let tags = requestBody.tags;
|
||||
|
||||
if (useOverrides) {
|
||||
const defaultRadarrId = requestBody.is4k
|
||||
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||
const defaultSonarrId = requestBody.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:
|
||||
requestBody.mediaType === MediaType.MOVIE
|
||||
? { radarrServiceId: defaultRadarrId }
|
||||
: { sonarrServiceId: defaultSonarrId },
|
||||
const overrideRulesResult = await overrideRules({
|
||||
mediaType: requestBody.mediaType,
|
||||
is4k: requestBody.is4k || false,
|
||||
tmdbMedia,
|
||||
requestUser,
|
||||
});
|
||||
|
||||
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 (
|
||||
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,
|
||||
});
|
||||
if (overrideRulesResult.rootFolder) {
|
||||
rootFolder = overrideRulesResult.rootFolder;
|
||||
}
|
||||
if (overrideRulesResult.profileId) {
|
||||
profileId = overrideRulesResult.profileId;
|
||||
}
|
||||
if (overrideRulesResult.tags) {
|
||||
tags = overrideRulesResult.tags;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 OverrideRule from '@server/entity/OverrideRule';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
||||
import overrideRules, {
|
||||
type OverrideRulesResult,
|
||||
} from '@server/lib/overrideRules';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
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<
|
||||
{ ruleId: string },
|
||||
OverrideRule,
|
||||
|
||||
@@ -420,32 +420,13 @@ export class MediaRequestSubscriber
|
||||
mediaId: entity.media.id,
|
||||
});
|
||||
} catch (e) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const mediaRepository = getRepository(Media);
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: entity.media.id },
|
||||
logger.error('Something went wrong sending request to Radarr', {
|
||||
label: 'Media Request',
|
||||
errorMessage: e.message,
|
||||
requestId: entity.id,
|
||||
mediaId: entity.media.id,
|
||||
});
|
||||
|
||||
if (media) {
|
||||
entity.status = MediaRequestStatus.FAILED;
|
||||
await requestRepository.save(entity);
|
||||
|
||||
logger.warn(
|
||||
'Failed to send movie request to Radarr due to connection or configuration error, marking status as FAILED',
|
||||
{
|
||||
label: 'Media Request',
|
||||
requestId: entity.id,
|
||||
mediaId: entity.media.id,
|
||||
errorMessage: e.message,
|
||||
}
|
||||
);
|
||||
|
||||
MediaRequest.sendNotification(
|
||||
entity,
|
||||
media,
|
||||
Notification.MEDIA_FAILED
|
||||
);
|
||||
}
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -746,32 +727,13 @@ export class MediaRequestSubscriber
|
||||
mediaId: entity.media.id,
|
||||
});
|
||||
} catch (e) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const mediaRepository = getRepository(Media);
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: entity.media.id },
|
||||
logger.error('Something went wrong sending request to Sonarr', {
|
||||
label: 'Media Request',
|
||||
errorMessage: e.message,
|
||||
requestId: entity.id,
|
||||
mediaId: entity.media.id,
|
||||
});
|
||||
|
||||
if (media) {
|
||||
entity.status = MediaRequestStatus.FAILED;
|
||||
await requestRepository.save(entity);
|
||||
|
||||
logger.warn(
|
||||
'Failed to send series request to Sonarr due to connection or configuration error, marking status as FAILED',
|
||||
{
|
||||
label: 'Media Request',
|
||||
requestId: entity.id,
|
||||
mediaId: entity.media.id,
|
||||
errorMessage: e.message,
|
||||
}
|
||||
);
|
||||
|
||||
MediaRequest.sendNotification(
|
||||
entity,
|
||||
media,
|
||||
Notification.MEDIA_FAILED
|
||||
);
|
||||
}
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ import type {
|
||||
ServiceCommonServerWithDetails,
|
||||
} from '@server/interfaces/api/serviceInterfaces';
|
||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import type { OverrideRulesResult } from '@server/lib/overrideRules';
|
||||
import { hasPermission } from '@server/lib/permissions';
|
||||
import axios from 'axios';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -51,6 +53,7 @@ export type RequestOverrides = {
|
||||
|
||||
interface AdvancedRequesterProps {
|
||||
type: 'movie' | 'tv';
|
||||
tmdbId?: number;
|
||||
is4k: boolean;
|
||||
isAnime?: boolean;
|
||||
defaultOverrides?: RequestOverrides;
|
||||
@@ -60,6 +63,7 @@ interface AdvancedRequesterProps {
|
||||
|
||||
const AdvancedRequester = ({
|
||||
type,
|
||||
tmdbId,
|
||||
is4k = false,
|
||||
isAnime = false,
|
||||
defaultOverrides,
|
||||
@@ -284,6 +288,35 @@ const AdvancedRequester = ({
|
||||
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) {
|
||||
return (
|
||||
<div className="mb-2 w-full">
|
||||
|
||||
@@ -288,6 +288,7 @@ const MovieRequestModal = ({
|
||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||
<AdvancedRequester
|
||||
type="movie"
|
||||
tmdbId={tmdbId}
|
||||
is4k={is4k}
|
||||
requestUser={editRequest.requestedBy}
|
||||
defaultOverrides={{
|
||||
@@ -357,6 +358,7 @@ const MovieRequestModal = ({
|
||||
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||
<AdvancedRequester
|
||||
tmdbId={tmdbId}
|
||||
type="movie"
|
||||
is4k={is4k}
|
||||
onChange={(overrides) => {
|
||||
|
||||
@@ -722,6 +722,7 @@ const TvRequestModal = ({
|
||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||
<AdvancedRequester
|
||||
type="tv"
|
||||
tmdbId={tmdbId}
|
||||
is4k={is4k}
|
||||
isAnime={data?.keywords.some(
|
||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
TmdbGenre,
|
||||
TmdbKeywordSearchResponse,
|
||||
} from '@server/api/themoviedb/interfaces';
|
||||
import type { GenreSliderItem } from '@server/interfaces/api/discoverInterfaces';
|
||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import type {
|
||||
Keyword,
|
||||
@@ -184,7 +185,9 @@ export const GenreSelector = ({
|
||||
}, [defaultValue, type]);
|
||||
|
||||
const loadGenreOptions = async (inputValue: string) => {
|
||||
const results = await axios.get<TmdbGenre[]>(`/api/v1/genres/${type}`);
|
||||
const results = await axios.get<GenreSliderItem[]>(
|
||||
`/api/v1/discover/genreslider/${type}`
|
||||
);
|
||||
|
||||
return results.data
|
||||
.map((result) => ({
|
||||
@@ -198,7 +201,7 @@ export const GenreSelector = ({
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
key={`genre-select-${type}-${defaultDataValue}`}
|
||||
key={`genre-select-${defaultDataValue}`}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
defaultValue={isMulti ? defaultDataValue : defaultDataValue?.[0]}
|
||||
|
||||
@@ -337,13 +337,7 @@ const OverrideRuleModal = ({
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<GenreSelector
|
||||
type={
|
||||
values.radarrServiceId != null
|
||||
? 'movie'
|
||||
: values.sonarrServiceId != null
|
||||
? 'tv'
|
||||
: 'tv'
|
||||
}
|
||||
type={values.radarrServiceId ? 'movie' : 'tv'}
|
||||
defaultValue={values.genre}
|
||||
isMulti
|
||||
isDisabled={!isValidated || isTesting}
|
||||
|
||||
Reference in New Issue
Block a user