diff --git a/overseerr-api.yml b/overseerr-api.yml index 6e8f58964d..bfbb12d4fd 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -360,6 +360,9 @@ components: is4k: type: boolean example: false + isAnime: + type: boolean + example: false minimumAvailability: type: string example: 'In Cinema' @@ -385,6 +388,7 @@ components: - activeProfileName - activeDirectory - is4k + - isAnime - minimumAvailability - isDefault SonarrSettings: @@ -439,6 +443,9 @@ components: is4k: type: boolean example: false + isAnime: + type: boolean + example: false enableSeasonFolders: type: boolean example: false @@ -464,6 +471,7 @@ components: - activeProfileName - activeDirectory - is4k + - isAnime - enableSeasonFolders - isDefault ServarrTag: @@ -1028,6 +1036,9 @@ components: is4k: type: boolean example: false + isAnime: + type: boolean + example: false serverId: type: number profileId: @@ -5018,6 +5029,9 @@ paths: is4k: type: boolean example: false + isAnime: + type: boolean + example: false serverId: type: number profileId: @@ -5122,6 +5136,9 @@ paths: is4k: type: boolean example: false + isAnime: + type: boolean + example: false serverId: type: number profileId: @@ -5763,6 +5780,9 @@ paths: is4k: type: boolean example: false + isAnime: + type: boolean + example: false responses: '200': description: Returned media diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index ba67ab7bef..beb5d10a29 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -6,7 +6,6 @@ import type { } from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr'; import TheMovieDb from '@server/api/themoviedb'; -import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; import { MediaRequestStatus, MediaStatus, @@ -157,6 +156,7 @@ export class MediaRequest { .leftJoin('request.media', 'media') .leftJoinAndSelect('request.requestedBy', 'user') .where('request.is4k = :is4k', { is4k: requestBody.is4k }) + .andWhere('request.isAnime = :isAnime', { isAnime: requestBody.isAnime }) .andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id }) .andWhere('media.mediaType = :mediaType', { mediaType: requestBody.mediaType, @@ -173,6 +173,7 @@ export class MediaRequest { tmdbId: tmdbMedia.id, mediaType: requestBody.mediaType, is4k: requestBody.is4k, + isAnime: requestBody.isAnime, label: 'Media Request', }); @@ -231,6 +232,7 @@ export class MediaRequest { ? user : undefined, is4k: requestBody.is4k, + isAnime: requestBody.isAnime, serverId: requestBody.serverId, profileId: requestBody.profileId, rootFolder: requestBody.rootFolder, @@ -260,6 +262,7 @@ export class MediaRequest { .filter( (request) => request.is4k === requestBody.is4k && + request.isAnime === requestBody.isAnime && request.status !== MediaRequestStatus.DECLINED ) .reduce((seasons, request) => { @@ -334,6 +337,7 @@ export class MediaRequest { ? user : undefined, is4k: requestBody.is4k, + isAnime: requestBody.isAnime, serverId: requestBody.serverId, profileId: requestBody.profileId, rootFolder: requestBody.rootFolder, @@ -414,6 +418,9 @@ export class MediaRequest { @Column({ default: false }) public is4k: boolean; + @Column({ default: false }) + public isAnime: boolean; + @Column({ nullable: true }) public serverId: number; @@ -665,9 +672,20 @@ export class MediaRequest { } let radarrSettings = settings.radarr.find( - (radarr) => radarr.isDefault && radarr.is4k === this.is4k + (radarr) => + radarr.isDefault && + radarr.is4k === this.is4k && + radarr.isAnime === this.isAnime ); + // Fallback for requesting anime if there is no default anime server + // This will sent the anime request to the regular default Radarr instance for single-instance setups + if (!radarrSettings && this.isAnime) { + radarrSettings = settings.radarr.find( + (radarr) => radarr.isDefault && radarr.is4k === this.is4k + ); + } + if ( this.serverId !== null && this.serverId >= 0 && @@ -689,9 +707,9 @@ export class MediaRequest { if (!radarrSettings) { logger.warn( `There is no default ${ - this.is4k ? '4K ' : '' + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' }Radarr server configured. Did you set any of your ${ - this.is4k ? '4K ' : '' + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' }Radarr servers as default?`, { label: 'Media Request', @@ -900,9 +918,20 @@ export class MediaRequest { } let sonarrSettings = settings.sonarr.find( - (sonarr) => sonarr.isDefault && sonarr.is4k === this.is4k + (sonarr) => + sonarr.isDefault && + sonarr.is4k === this.is4k && + sonarr.isAnime == this.isAnime ); + // Fallback for requesting anime if there is no default anime server + // This will sent the anime request to the regular default Sonarr instance for single-instance setups + if (!sonarrSettings && this.isAnime) { + sonarrSettings = settings.sonarr.find( + (sonarr) => sonarr.isDefault && sonarr.is4k === this.is4k + ); + } + if ( this.serverId !== null && this.serverId >= 0 && @@ -924,9 +953,9 @@ export class MediaRequest { if (!sonarrSettings) { logger.warn( `There is no default ${ - this.is4k ? '4K ' : '' + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' }Sonarr server configured. Did you set any of your ${ - this.is4k ? '4K ' : '' + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' }Sonarr servers as default?`, { label: 'Media Request', @@ -979,11 +1008,7 @@ export class MediaRequest { let seriesType: SonarrSeries['seriesType'] = 'standard'; // Change series type to anime if the anime keyword is present on tmdb - if ( - series.keywords.results.some( - (keyword) => keyword.id === ANIME_KEYWORD_ID - ) - ) { + if (this.isAnime) { seriesType = sonarrSettings.animeSeriesType ?? 'anime'; } @@ -1171,30 +1196,38 @@ export class MediaRequest { switch (type) { case Notification.MEDIA_APPROVED: - event = `${this.is4k ? '4K ' : ''}${mediaType} Request Approved`; + event = `${ + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' + }${mediaType} Request Approved`; notifyAdmin = false; break; case Notification.MEDIA_DECLINED: - event = `${this.is4k ? '4K ' : ''}${mediaType} Request Declined`; + event = `${ + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' + }${mediaType} Request Declined`; notifyAdmin = false; break; case Notification.MEDIA_PENDING: - event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`; + event = `New ${ + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' + }${mediaType} Request`; break; case Notification.MEDIA_AUTO_REQUESTED: event = `${ - this.is4k ? '4K ' : '' + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' }${mediaType} Request Automatically Submitted`; notifyAdmin = false; notifySystem = false; break; case Notification.MEDIA_AUTO_APPROVED: event = `${ - this.is4k ? '4K ' : '' + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' }${mediaType} Request Automatically Approved`; break; case Notification.MEDIA_FAILED: - event = `${this.is4k ? '4K ' : ''}${mediaType} Request Failed`; + event = `${ + this.isAnime ? 'Anime ' : this.is4k ? '4K ' : '' + }${mediaType} Request Failed`; break; } diff --git a/server/interfaces/api/requestInterfaces.ts b/server/interfaces/api/requestInterfaces.ts index 89863cb042..d860c9667e 100644 --- a/server/interfaces/api/requestInterfaces.ts +++ b/server/interfaces/api/requestInterfaces.ts @@ -12,6 +12,7 @@ export type MediaRequestBody = { tvdbId?: number; seasons?: number[] | 'all'; is4k?: boolean; + isAnime?: boolean; serverId?: number; profileId?: number; rootFolder?: string; diff --git a/server/interfaces/api/serviceInterfaces.ts b/server/interfaces/api/serviceInterfaces.ts index 3b430b0b59..6c7de584c6 100644 --- a/server/interfaces/api/serviceInterfaces.ts +++ b/server/interfaces/api/serviceInterfaces.ts @@ -5,6 +5,7 @@ export interface ServiceCommonServer { id: number; name: string; is4k: boolean; + isAnime: boolean; isDefault: boolean; activeProfileId: number; activeDirectory: string; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 10213a0403..51f615a726 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -57,6 +57,7 @@ export interface DVRSettings { activeDirectory: string; tags: number[]; is4k: boolean; + isAnime: boolean; isDefault: boolean; externalUrl?: string; syncEnabled: boolean; diff --git a/server/migration/1698786580184-AddMediaRequestIsAnimeField.ts b/server/migration/1698786580184-AddMediaRequestIsAnimeField.ts new file mode 100644 index 0000000000..3e53e66585 --- /dev/null +++ b/server/migration/1698786580184-AddMediaRequestIsAnimeField.ts @@ -0,0 +1,33 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMediaRequestIsAnimeField1698786580184 + implements MigrationInterface +{ + name = 'AddMediaRequestIsAnimeField1698786580184'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "isAnime" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "media_request"` + ); + await queryRunner.query(`DROP TABLE "media_request"`); + await queryRunner.query( + `ALTER TABLE "temporary_media_request" RENAME TO "media_request"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "media_request" RENAME TO "temporary_media_request"` + ); + await queryRunner.query( + `CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "temporary_media_request"` + ); + await queryRunner.query(`DROP TABLE "temporary_media_request"`); + } +} diff --git a/server/routes/service.ts b/server/routes/service.ts index 083e1eb57d..cd23bcfa97 100644 --- a/server/routes/service.ts +++ b/server/routes/service.ts @@ -19,6 +19,7 @@ serviceRoutes.get('/radarr', async (req, res) => { id: radarr.id, name: radarr.name, is4k: radarr.is4k, + isAnime: radarr.isAnime, isDefault: radarr.isDefault, activeDirectory: radarr.activeDirectory, activeProfileId: radarr.activeProfileId, @@ -59,6 +60,7 @@ serviceRoutes.get<{ radarrId: string }>( id: radarrSettings.id, name: radarrSettings.name, is4k: radarrSettings.is4k, + isAnime: radarrSettings.isAnime, isDefault: radarrSettings.isDefault, activeDirectory: radarrSettings.activeDirectory, activeProfileId: radarrSettings.activeProfileId, @@ -87,6 +89,7 @@ serviceRoutes.get('/sonarr', async (req, res) => { id: sonarr.id, name: sonarr.name, is4k: sonarr.is4k, + isAnime: sonarr.isAnime, isDefault: sonarr.isDefault, activeDirectory: sonarr.activeDirectory, activeProfileId: sonarr.activeProfileId, @@ -133,6 +136,7 @@ serviceRoutes.get<{ sonarrId: string }>( id: sonarrSettings.id, name: sonarrSettings.name, is4k: sonarrSettings.is4k, + isAnime: sonarrSettings.isAnime, isDefault: sonarrSettings.isDefault, activeDirectory: sonarrSettings.activeDirectory, activeProfileId: sonarrSettings.activeProfileId, diff --git a/server/routes/settings/radarr.ts b/server/routes/settings/radarr.ts index c2b0a6f523..b36c6162f0 100644 --- a/server/routes/settings/radarr.ts +++ b/server/routes/settings/radarr.ts @@ -24,7 +24,11 @@ radarrRoutes.post('/', (req, res) => { // and are the default if (req.body.isDefault) { settings.radarr - .filter((radarrInstance) => radarrInstance.is4k === req.body.is4k) + .filter( + (radarrInstance) => + radarrInstance.is4k === req.body.is4k && + radarrInstance.isAnime === req.body.isAnime + ) .forEach((radarrInstance) => { radarrInstance.isDefault = false; }); @@ -92,7 +96,11 @@ radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>( // and are the default if (req.body.isDefault) { settings.radarr - .filter((radarrInstance) => radarrInstance.is4k === req.body.is4k) + .filter( + (radarrInstance) => + radarrInstance.is4k === req.body.is4k && + radarrInstance.isAnime === req.body.isAnime + ) .forEach((radarrInstance) => { radarrInstance.isDefault = false; }); diff --git a/server/routes/settings/sonarr.ts b/server/routes/settings/sonarr.ts index 358d070023..718269bf46 100644 --- a/server/routes/settings/sonarr.ts +++ b/server/routes/settings/sonarr.ts @@ -24,7 +24,11 @@ sonarrRoutes.post('/', (req, res) => { // and are the default if (req.body.isDefault) { settings.sonarr - .filter((sonarrInstance) => sonarrInstance.is4k === req.body.is4k) + .filter( + (sonarrInstance) => + sonarrInstance.is4k === req.body.is4k && + sonarrInstance.isAnime === req.body.isAnime + ) .forEach((sonarrInstance) => { sonarrInstance.isDefault = false; }); @@ -90,7 +94,11 @@ sonarrRoutes.put<{ id: string }>('/:id', (req, res) => { // and are the default if (req.body.isDefault) { settings.sonarr - .filter((sonarrInstance) => sonarrInstance.is4k === req.body.is4k) + .filter( + (sonarrInstance) => + sonarrInstance.is4k === req.body.is4k && + sonarrInstance.isAnime === req.body.isAnime + ) .forEach((sonarrInstance) => { sonarrInstance.isDefault = false; }); diff --git a/src/components/RequestBlock/index.tsx b/src/components/RequestBlock/index.tsx index ed4c3ec358..c1d0003ab0 100644 --- a/src/components/RequestBlock/index.tsx +++ b/src/components/RequestBlock/index.tsx @@ -78,6 +78,7 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => { tmdbId={request.media.tmdbId} type={request.type} is4k={request.is4k} + isAnime={request.isAnime} editRequest={request} onCancel={() => setShowEditModal(false)} onComplete={() => { diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 44abd555a8..10c5eb32b7 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -308,6 +308,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { tmdbId={request.media.tmdbId} type={request.type} is4k={request.is4k} + isAnime={request.isAnime} editRequest={request} onCancel={() => setShowEditModal(false)} onComplete={() => { diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index a42483abe4..91c82cd4c9 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -368,6 +368,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { tmdbId={request.media.tmdbId} type={request.type} is4k={request.is4k} + isAnime={request.isAnime} editRequest={request} onCancel={() => setShowEditModal(false)} onComplete={() => { diff --git a/src/components/RequestModal/AdvancedRequester/index.tsx b/src/components/RequestModal/AdvancedRequester/index.tsx index 4f5bb9ac6f..f17c470f17 100644 --- a/src/components/RequestModal/AdvancedRequester/index.tsx +++ b/src/components/RequestModal/AdvancedRequester/index.tsx @@ -152,7 +152,8 @@ const AdvancedRequester = ({ useEffect(() => { let defaultServer = data?.find( - (server) => server.isDefault && is4k === server.is4k + (server) => + server.isDefault && is4k === server.is4k && isAnime === server.isAnime ); if (!defaultServer && (data ?? []).length > 0) { @@ -293,7 +294,9 @@ const AdvancedRequester = ({ if ( (!data || selectedServer === null || - (data.filter((server) => server.is4k === is4k).length < 2 && + (data.filter( + (server) => server.is4k === is4k && server.isAnime === isAnime + ).length < 2 && (!serverData || (serverData.profiles.length < 2 && serverData.rootFolders.length < 2 && @@ -312,7 +315,9 @@ const AdvancedRequester = ({
{!!data && selectedServer !== null && (
- {data.filter((server) => server.is4k === is4k).length > 1 && ( + {data.filter( + (server) => server.is4k === is4k && server.isAnime === isAnime + ).length > 1 && (
+
+ +
+ +
+
+
+ +
+ +
+