diff --git a/overseerr-api.yml b/overseerr-api.yml index c4c1e97b..8f1bba9d 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: @@ -5024,6 +5035,9 @@ paths: is4k: type: boolean example: false + isAnime: + type: boolean + example: false serverId: type: number profileId: @@ -5128,6 +5142,9 @@ paths: is4k: type: boolean example: false + isAnime: + type: boolean + example: false serverId: type: number profileId: @@ -5769,6 +5786,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 ba67ab7b..beb5d10a 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 89863cb0..d860c966 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 3b430b0b..6c7de584 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 10213a04..51f615a7 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 00000000..3e53e665 --- /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 083e1eb5..cd23bcfa 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 c2b0a6f5..b36c6162 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 358d0700..718269bf 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 ed4c3ec3..c1d0003a 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 44abd555..10c5eb32 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 a42483ab..91c82cd4 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 4f5bb9ac..f17c470f 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 && (
+
+ +
+ +
+
+
+ +
+ +
+