diff --git a/.all-contributorsrc b/.all-contributorsrc
index b285bb485..2dfb170b0 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -296,7 +296,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/13354331?v=4",
"profile": "https://github.com/xeruf",
"contributions": [
- "doc"
+ "doc",
+ "code"
]
},
{
@@ -381,33 +382,6 @@
"code"
]
},
- {
- "login": "j0srisk",
- "name": "Joseph Risk",
- "avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
- "profile": "http://josephrisk.com",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Loetwiek",
- "name": "Loetwiek",
- "avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
- "profile": "https://github.com/Loetwiek",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Fuochi",
- "name": "Fuochi",
- "avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
- "profile": "https://github.com/Fuochi",
- "contributions": [
- "doc"
- ]
- },
{
"login": "mobihen",
"name": "Nir Israel Hen",
@@ -453,69 +427,6 @@
"security"
]
},
- {
- "login": "j0srisk",
- "name": "Joseph Risk",
- "avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
- "profile": "http://josephrisk.com",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Loetwiek",
- "name": "Loetwiek",
- "avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
- "profile": "https://github.com/Loetwiek",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Fuochi",
- "name": "Fuochi",
- "avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
- "profile": "https://github.com/Fuochi",
- "contributions": [
- "doc"
- ]
- },
- {
- "login": "demrich",
- "name": "David Emrich",
- "avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
- "profile": "https://github.com/demrich",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "maxnatamo",
- "name": "Max T. Kristiansen",
- "avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
- "profile": "https://maxtrier.dk",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "DamsDev1",
- "name": "Damien Fajole",
- "avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
- "profile": "https://damsdev.me",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "AhmedNSidd",
- "name": "Ahmed Siddiqui",
- "avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
- "profile": "https://github.com/AhmedNSidd",
- "contributions": [
- "code"
- ]
- },
{
"login": "Zariel",
"name": "Chris Bannister",
@@ -624,87 +535,6 @@
"code"
]
},
- {
- "login": "j0srisk",
- "name": "Joseph Risk",
- "avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
- "profile": "http://josephrisk.com",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Loetwiek",
- "name": "Loetwiek",
- "avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
- "profile": "https://github.com/Loetwiek",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Fuochi",
- "name": "Fuochi",
- "avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
- "profile": "https://github.com/Fuochi",
- "contributions": [
- "doc"
- ]
- },
- {
- "login": "demrich",
- "name": "David Emrich",
- "avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
- "profile": "https://github.com/demrich",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "maxnatamo",
- "name": "Max T. Kristiansen",
- "avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
- "profile": "https://maxtrier.dk",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "DamsDev1",
- "name": "Damien Fajole",
- "avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
- "profile": "https://damsdev.me",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "AhmedNSidd",
- "name": "Ahmed Siddiqui",
- "avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
- "profile": "https://github.com/AhmedNSidd",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "JackW6809",
- "name": "JackOXI",
- "avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
- "profile": "https://github.com/JackW6809",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "StancuFlorin",
- "name": "Stancu Florin",
- "avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
- "profile": "http://indicus.ro",
- "contributions": [
- "code"
- ]
- },
{
"login": "RankWeis",
"name": "RankWeis",
@@ -714,105 +544,6 @@
"code"
]
},
- {
- "login": "j0srisk",
- "name": "Joseph Risk",
- "avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
- "profile": "http://josephrisk.com",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Loetwiek",
- "name": "Loetwiek",
- "avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
- "profile": "https://github.com/Loetwiek",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Fuochi",
- "name": "Fuochi",
- "avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
- "profile": "https://github.com/Fuochi",
- "contributions": [
- "doc"
- ]
- },
- {
- "login": "demrich",
- "name": "David Emrich",
- "avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
- "profile": "https://github.com/demrich",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "maxnatamo",
- "name": "Max T. Kristiansen",
- "avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
- "profile": "https://maxtrier.dk",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "DamsDev1",
- "name": "Damien Fajole",
- "avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
- "profile": "https://damsdev.me",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "AhmedNSidd",
- "name": "Ahmed Siddiqui",
- "avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
- "profile": "https://github.com/AhmedNSidd",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "JackW6809",
- "name": "JackOXI",
- "avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
- "profile": "https://github.com/JackW6809",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "StancuFlorin",
- "name": "Stancu Florin",
- "avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
- "profile": "http://indicus.ro",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "lmiklosko",
- "name": "Lukas Miklosko",
- "avatar_url": "https://avatars.githubusercontent.com/u/44380311?v=4",
- "profile": "https://github.com/lmiklosko",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "gauthier-th",
- "name": "Gauthier",
- "avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
- "profile": "https://gauthierth.fr/",
- "contributions": [
- "code"
- ]
- },
{
"login": "jessielw",
"name": "Jessie Wilson",
@@ -849,105 +580,6 @@
"code"
]
},
- {
- "login": "j0srisk",
- "name": "Joseph Risk",
- "avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
- "profile": "http://josephrisk.com",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Loetwiek",
- "name": "Loetwiek",
- "avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
- "profile": "https://github.com/Loetwiek",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "Fuochi",
- "name": "Fuochi",
- "avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
- "profile": "https://github.com/Fuochi",
- "contributions": [
- "doc"
- ]
- },
- {
- "login": "demrich",
- "name": "David Emrich",
- "avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
- "profile": "https://github.com/demrich",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "maxnatamo",
- "name": "Max T. Kristiansen",
- "avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
- "profile": "https://maxtrier.dk",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "DamsDev1",
- "name": "Damien Fajole",
- "avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
- "profile": "https://damsdev.me",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "AhmedNSidd",
- "name": "Ahmed Siddiqui",
- "avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
- "profile": "https://github.com/AhmedNSidd",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "JackW6809",
- "name": "JackOXI",
- "avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
- "profile": "https://github.com/JackW6809",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "StancuFlorin",
- "name": "Stancu Florin",
- "avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
- "profile": "http://indicus.ro",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "lmiklosko",
- "name": "Lukas Miklosko",
- "avatar_url": "https://avatars.githubusercontent.com/u/44380311?v=4",
- "profile": "https://github.com/lmiklosko",
- "contributions": [
- "code"
- ]
- },
- {
- "login": "gauthier-th",
- "name": "Gauthier",
- "avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
- "profile": "https://gauthierth.fr/",
- "contributions": [
- "code"
- ]
- },
{
"login": "vfaergestad",
"name": "vfaergestad",
diff --git a/README.md b/README.md
index 957d84fba..e20d9bdfa 100644
--- a/README.md
+++ b/README.md
@@ -136,73 +136,33 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
 Joaquin Olivero 💻 |
 Julian Behr 🌍 |
 ThowZzy 💻 |
-  Joseph Risk 💻 |
-  Loetwiek 💻 |
-
-
-  Fuochi 📖 |
 Nir Israel Hen 🌍 |
 Baraa 💻 |
+
+
 Francisco Sales 💻 |
 Oliver Laing 💻 |
 Ludovic Ortega 🛡️ |
-  Joseph Risk 💻 |
-
-
-  Loetwiek 💻 |
-  Fuochi 📖 |
-  David Emrich 💻 |
-  Max T. Kristiansen 💻 |
-  Damien Fajole 💻 |
-  Ahmed Siddiqui 💻 |
 Chris Bannister 💻 |
-
-
 Joe 📖 |
 Guillaume ARNOUX 💻 |
 dr-carrot 💻 |
+
+
 Gage Orsburn 💻 |
 GkhnGRBZ 💻 |
 Ben Haney 💻 |
 Wunderharke 📖 |
-
-
 Metin Bektas 🚇 |
 andrewkolda 🎨 |
 Ishan Jain 💻 |
+
+
 Michael Thomas 💻 |
-  Joseph Risk 💻 |
-  Loetwiek 💻 |
-  Fuochi 📖 |
-
-
-  David Emrich 💻 |
-  Max T. Kristiansen 💻 |
-  Damien Fajole 💻 |
-  Ahmed Siddiqui 💻 |
-  JackOXI 💻 |
-  Stancu Florin 💻 |
 RankWeis 💻 |
-
-
-  Joseph Risk 💻 |
-  Loetwiek 💻 |
-  Fuochi 📖 |
-  David Emrich 💻 |
-  Max T. Kristiansen 💻 |
-  Damien Fajole 💻 |
-  Ahmed Siddiqui 💻 |
-
-
-  JackOXI 💻 |
-  Stancu Florin 💻 |
-  Lukas Miklosko 💻 |
-  Gauthier 💻 |
 Jessie Wilson 💻 |
 DominicKo 💻 |
 Corentin Normand 💻 |
-
-
 Ben Beauchamp 💻 |
 Joseph Risk 💻 |
 Loetwiek 💻 |
@@ -353,7 +313,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
 Izaac Brånn 💻 |
 Salman Tariq 💻 |
 Andrew Kennedy 💻 |
-  Fallenbagel 🪼⌨️ 💻 |
+  Fallenbagel 💻 |
 Anton K. (ai Doge) 💻 |
 Marco Faggian 💻 |
 Eric Nemchik 💻 |
diff --git a/server/entity/Media.ts b/server/entity/Media.ts
index 1941162a6..1eafdc120 100644
--- a/server/entity/Media.ts
+++ b/server/entity/Media.ts
@@ -108,7 +108,9 @@ class Media {
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status4k: MediaStatus;
- @OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
+ @OneToMany(() => MediaRequest, (request) => request.media, {
+ cascade: ['insert', 'update'],
+ })
public requests: MediaRequest[];
@OneToMany(() => Watchlist, (watchlist) => watchlist.media)
diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts
index f531bcb30..cd103ef4f 100644
--- a/server/entity/MediaRequest.ts
+++ b/server/entity/MediaRequest.ts
@@ -1,10 +1,3 @@
-import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
-import RadarrAPI from '@server/api/servarr/radarr';
-import type {
- AddSeriesOptions,
- SonarrSeries,
-} 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 type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
@@ -20,10 +13,9 @@ import notificationManager, { Notification } from '@server/lib/notifications';
import { Permission } from '@server/lib/permissions';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
-import { isEqual, truncate } from 'lodash';
+import { truncate } from 'lodash';
import {
AfterInsert,
- AfterRemove,
AfterUpdate,
Column,
CreateDateColumn,
@@ -614,12 +606,6 @@ export class MediaRequest {
Object.assign(this, init);
}
- @AfterUpdate()
- @AfterInsert()
- public async sendMedia(): Promise {
- await Promise.all([this.sendToRadarr(), this.sendToSonarr()]);
- }
-
@AfterInsert()
public async notifyNewRequest(): Promise {
if (this.status === MediaRequestStatus.PENDING) {
@@ -636,10 +622,14 @@ export class MediaRequest {
return;
}
- this.sendNotification(media, Notification.MEDIA_PENDING);
+ MediaRequest.sendNotification(this, media, Notification.MEDIA_PENDING);
if (this.isAutoRequest) {
- this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
+ MediaRequest.sendNotification(
+ this,
+ media,
+ Notification.MEDIA_AUTO_REQUESTED
+ );
}
}
}
@@ -677,7 +667,8 @@ export class MediaRequest {
return;
}
- this.sendNotification(
+ MediaRequest.sendNotification(
+ this,
media,
this.status === MediaRequestStatus.APPROVED
? autoApproved
@@ -691,7 +682,11 @@ export class MediaRequest {
autoApproved &&
this.isAutoRequest
) {
- this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
+ MediaRequest.sendNotification(
+ this,
+ media,
+ Notification.MEDIA_AUTO_REQUESTED
+ );
}
}
}
@@ -703,701 +698,56 @@ export class MediaRequest {
}
}
- @AfterUpdate()
- @AfterInsert()
- public async updateParentStatus(): Promise {
- const mediaRepository = getRepository(Media);
- const media = await mediaRepository.findOne({
- where: { id: this.media.id },
- relations: { requests: true },
- });
- if (!media) {
- logger.error('Media data not found', {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
- return;
- }
- const seasonRequestRepository = getRepository(SeasonRequest);
- if (
- this.status === MediaRequestStatus.APPROVED &&
- // Do not update the status if the item is already partially available or available
- media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
- media[this.is4k ? 'status4k' : 'status'] !==
- MediaStatus.PARTIALLY_AVAILABLE &&
- media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
- ) {
- const statusField = this.is4k ? 'status4k' : 'status';
-
- await mediaRepository.update(
- { id: this.media.id },
- { [statusField]: MediaStatus.PROCESSING }
- );
- }
-
- if (
- media.mediaType === MediaType.MOVIE &&
- this.status === MediaRequestStatus.DECLINED &&
- media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
- ) {
- const statusField = this.is4k ? 'status4k' : 'status';
- await mediaRepository.update(
- { id: this.media.id },
- { [statusField]: MediaStatus.UNKNOWN }
- );
- }
-
- /**
- * If the media type is TV, and we are declining a request,
- * we must check if its the only pending request and that
- * there the current media status is just pending (meaning no
- * other requests have yet to be approved)
- */
- if (
- media.mediaType === MediaType.TV &&
- this.status === MediaRequestStatus.DECLINED &&
- media.requests.filter(
- (request) => request.status === MediaRequestStatus.PENDING
- ).length === 0 &&
- media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
- media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
- ) {
- const statusField = this.is4k ? 'status4k' : 'status';
- mediaRepository.update(
- { id: this.media.id },
- { [statusField]: MediaStatus.UNKNOWN }
- );
- }
-
- // Approve child seasons if parent is approved
- if (
- media.mediaType === MediaType.TV &&
- this.status === MediaRequestStatus.APPROVED
- ) {
- this.seasons.forEach((season) => {
- season.status = MediaRequestStatus.APPROVED;
- seasonRequestRepository.save(season);
- });
- }
- }
-
- @AfterRemove()
- public async handleRemoveParentUpdate(): Promise {
- const mediaRepository = getRepository(Media);
- const fullMedia = await mediaRepository.findOneOrFail({
- where: { id: this.media.id },
- relations: { requests: true },
- });
-
- if (
- !fullMedia.requests.some((request) => !request.is4k) &&
- fullMedia.status !== MediaStatus.AVAILABLE
- ) {
- fullMedia.status = MediaStatus.UNKNOWN;
- }
-
- if (
- !fullMedia.requests.some((request) => request.is4k) &&
- fullMedia.status4k !== MediaStatus.AVAILABLE
- ) {
- fullMedia.status4k = MediaStatus.UNKNOWN;
- }
-
- mediaRepository.save(fullMedia);
- }
-
- public async sendToRadarr(): Promise {
- if (
- this.status === MediaRequestStatus.APPROVED &&
- this.type === MediaType.MOVIE
- ) {
- try {
- const mediaRepository = getRepository(Media);
- const settings = getSettings();
- if (settings.radarr.length === 0 && !settings.radarr[0]) {
- logger.info(
- 'No Radarr server configured, skipping request processing',
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- return;
- }
-
- let radarrSettings = settings.radarr.find(
- (radarr) => radarr.isDefault && radarr.is4k === this.is4k
- );
-
- if (
- this.serverId !== null &&
- this.serverId >= 0 &&
- radarrSettings?.id !== this.serverId
- ) {
- radarrSettings = settings.radarr.find(
- (radarr) => radarr.id === this.serverId
- );
- logger.info(
- `Request has an override server: ${radarrSettings?.name}`,
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- }
-
- if (!radarrSettings) {
- logger.warn(
- `There is no default ${
- this.is4k ? '4K ' : ''
- }Radarr server configured. Did you set any of your ${
- this.is4k ? '4K ' : ''
- }Radarr servers as default?`,
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- return;
- }
-
- let rootFolder = radarrSettings.activeDirectory;
- let qualityProfile = radarrSettings.activeProfileId;
- let tags = radarrSettings.tags ? [...radarrSettings.tags] : [];
-
- if (
- this.rootFolder &&
- this.rootFolder !== '' &&
- this.rootFolder !== radarrSettings.activeDirectory
- ) {
- rootFolder = this.rootFolder;
- logger.info(`Request has an override root folder: ${rootFolder}`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
- }
-
- if (
- this.profileId &&
- this.profileId !== radarrSettings.activeProfileId
- ) {
- qualityProfile = this.profileId;
- logger.info(
- `Request has an override quality profile ID: ${qualityProfile}`,
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- }
-
- if (this.tags && !isEqual(this.tags, radarrSettings.tags)) {
- tags = this.tags;
- logger.info(`Request has override tags`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- tagIds: tags,
- });
- }
-
- const tmdb = new TheMovieDb();
- const radarr = new RadarrAPI({
- apiKey: radarrSettings.apiKey,
- url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
- });
- const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
-
- const media = await mediaRepository.findOne({
- where: { id: this.media.id },
- });
-
- if (!media) {
- logger.error('Media data not found', {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
- return;
- }
-
- if (radarrSettings.tagRequests) {
- let userTag = (await radarr.getTags()).find((v) =>
- v.label.startsWith(this.requestedBy.id + ' - ')
- );
- if (!userTag) {
- logger.info(`Requester has no active tag. Creating new`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- userId: this.requestedBy.id,
- newTag:
- this.requestedBy.id + ' - ' + this.requestedBy.displayName,
- });
- userTag = await radarr.createTag({
- label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
- });
- }
- if (userTag.id) {
- if (!tags?.find((v) => v === userTag?.id)) {
- tags?.push(userTag.id);
- }
- } else {
- logger.warn(`Requester has no tag and failed to add one`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- userId: this.requestedBy.id,
- radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
- });
- }
- }
-
- if (
- media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
- ) {
- logger.warn('Media already exists, marking request as APPROVED', {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
-
- const requestRepository = getRepository(MediaRequest);
-
- await requestRepository.update(this.id, {
- status: MediaRequestStatus.APPROVED,
- });
- return;
- }
-
- const radarrMovieOptions: RadarrMovieOptions = {
- profileId: qualityProfile,
- qualityProfileId: qualityProfile,
- rootFolderPath: rootFolder,
- minimumAvailability: radarrSettings.minimumAvailability,
- title: movie.title,
- tmdbId: movie.id,
- year: Number(movie.release_date.slice(0, 4)),
- monitored: true,
- tags,
- searchNow: !radarrSettings.preventSearch,
- };
-
- // Run this asynchronously so we don't wait for it on the UI side
- radarr
- .addMovie(radarrMovieOptions)
- .then(async (radarrMovie) => {
- // We grab media again here to make sure we have the latest version of it
- const media = await mediaRepository.findOne({
- where: { id: this.media.id },
- });
-
- if (!media) {
- throw new Error('Media data not found');
- }
-
- const updateFields = {
- [this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
- radarrMovie.id,
- [this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
- radarrMovie.titleSlug,
- [this.is4k ? 'serviceId4k' : 'serviceId']: radarrSettings?.id,
- };
-
- await mediaRepository.update({ id: this.media.id }, updateFields);
- })
- .catch(async () => {
- const requestRepository = getRepository(MediaRequest);
-
- await requestRepository.update(this.id, {
- status: MediaRequestStatus.FAILED,
- });
-
- logger.warn(
- 'Something went wrong sending movie request to Radarr, marking status as FAILED',
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- radarrMovieOptions,
- }
- );
-
- this.sendNotification(media, Notification.MEDIA_FAILED);
- })
- .finally(() => {
- radarr.clearCache({
- tmdbId: movie.id,
- externalId: this.is4k
- ? media.externalServiceId4k
- : media.externalServiceId,
- });
- });
- logger.info('Sent request to Radarr', {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
- } catch (e) {
- logger.error('Something went wrong sending request to Radarr', {
- label: 'Media Request',
- errorMessage: e.message,
- requestId: this.id,
- mediaId: this.media.id,
- });
- throw new Error(e.message);
- }
- }
- }
-
- public async sendToSonarr(): Promise {
- if (
- this.status === MediaRequestStatus.APPROVED &&
- this.type === MediaType.TV
- ) {
- try {
- const mediaRepository = getRepository(Media);
- const settings = getSettings();
- if (settings.sonarr.length === 0 && !settings.sonarr[0]) {
- logger.warn(
- 'No Sonarr server configured, skipping request processing',
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- return;
- }
-
- let sonarrSettings = settings.sonarr.find(
- (sonarr) => sonarr.isDefault && sonarr.is4k === this.is4k
- );
-
- if (
- this.serverId !== null &&
- this.serverId >= 0 &&
- sonarrSettings?.id !== this.serverId
- ) {
- sonarrSettings = settings.sonarr.find(
- (sonarr) => sonarr.id === this.serverId
- );
- logger.info(
- `Request has an override server: ${sonarrSettings?.name}`,
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- }
-
- if (!sonarrSettings) {
- logger.warn(
- `There is no default ${
- this.is4k ? '4K ' : ''
- }Sonarr server configured. Did you set any of your ${
- this.is4k ? '4K ' : ''
- }Sonarr servers as default?`,
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- return;
- }
-
- const media = await mediaRepository.findOne({
- where: { id: this.media.id },
- relations: { requests: true },
- });
-
- if (!media) {
- throw new Error('Media data not found');
- }
-
- if (
- media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
- ) {
- logger.warn('Media already exists, marking request as APPROVED', {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
-
- const requestRepository = getRepository(MediaRequest);
- await requestRepository.update(this.id, {
- status: MediaRequestStatus.APPROVED,
- });
- return;
- }
-
- const tmdb = new TheMovieDb();
- const sonarr = new SonarrAPI({
- apiKey: sonarrSettings.apiKey,
- url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
- });
- const series = await tmdb.getTvShow({ tvId: media.tmdbId });
- const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
-
- if (!tvdbId) {
- const requestRepository = getRepository(MediaRequest);
- await mediaRepository.remove(media);
- await requestRepository.remove(this);
- throw new Error('TVDB ID not found');
- }
-
- 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
- )
- ) {
- seriesType = sonarrSettings.animeSeriesType ?? 'anime';
- }
-
- let rootFolder =
- seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
- ? sonarrSettings.activeAnimeDirectory
- : sonarrSettings.activeDirectory;
- let qualityProfile =
- seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
- ? sonarrSettings.activeAnimeProfileId
- : sonarrSettings.activeProfileId;
- let languageProfile =
- seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
- ? sonarrSettings.activeAnimeLanguageProfileId
- : sonarrSettings.activeLanguageProfileId;
- let tags =
- seriesType === 'anime'
- ? sonarrSettings.animeTags
- ? [...sonarrSettings.animeTags]
- : []
- : sonarrSettings.tags
- ? [...sonarrSettings.tags]
- : [];
-
- if (
- this.rootFolder &&
- this.rootFolder !== '' &&
- this.rootFolder !== rootFolder
- ) {
- rootFolder = this.rootFolder;
- logger.info(`Request has an override root folder: ${rootFolder}`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
- }
-
- if (this.profileId && this.profileId !== qualityProfile) {
- qualityProfile = this.profileId;
- logger.info(
- `Request has an override quality profile ID: ${qualityProfile}`,
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- }
-
- if (
- this.languageProfileId &&
- this.languageProfileId !== languageProfile
- ) {
- languageProfile = this.languageProfileId;
- logger.info(
- `Request has an override language profile ID: ${languageProfile}`,
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- }
- );
- }
-
- if (this.tags && !isEqual(this.tags, tags)) {
- tags = this.tags;
- logger.info(`Request has override tags`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- tagIds: tags,
- });
- }
-
- if (sonarrSettings.tagRequests) {
- let userTag = (await sonarr.getTags()).find((v) =>
- v.label.startsWith(this.requestedBy.id + ' - ')
- );
- if (!userTag) {
- logger.info(`Requester has no active tag. Creating new`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- userId: this.requestedBy.id,
- newTag:
- this.requestedBy.id + ' - ' + this.requestedBy.displayName,
- });
- userTag = await sonarr.createTag({
- label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
- });
- }
- if (userTag.id) {
- if (!tags?.find((v) => v === userTag?.id)) {
- tags?.push(userTag.id);
- }
- } else {
- logger.warn(`Requester has no tag and failed to add one`, {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- userId: this.requestedBy.id,
- sonarrServer: sonarrSettings.hostname + ':' + sonarrSettings.port,
- });
- }
- }
-
- const sonarrSeriesOptions: AddSeriesOptions = {
- profileId: qualityProfile,
- languageProfileId: languageProfile,
- rootFolderPath: rootFolder,
- title: series.name,
- tvdbid: tvdbId,
- seasons: this.seasons.map((season) => season.seasonNumber),
- seasonFolder: sonarrSettings.enableSeasonFolders,
- seriesType,
- tags,
- monitored: true,
- searchNow: !sonarrSettings.preventSearch,
- };
-
- // Run this asynchronously so we don't wait for it on the UI side
- sonarr
- .addSeries(sonarrSeriesOptions)
- .then(async (sonarrSeries) => {
- // We grab media again here to make sure we have the latest version of it
- const media = await mediaRepository.findOne({
- where: { id: this.media.id },
- relations: { requests: true },
- });
-
- if (!media) {
- throw new Error('Media data not found');
- }
-
- const updateFields = {
- [this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
- sonarrSeries.id,
- [this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
- sonarrSeries.titleSlug,
- [this.is4k ? 'serviceId4k' : 'serviceId']: sonarrSettings?.id,
- };
-
- await mediaRepository.update({ id: this.media.id }, updateFields);
- })
- .catch(async () => {
- const requestRepository = getRepository(MediaRequest);
-
- await requestRepository.update(
- { id: this.id },
- { status: MediaRequestStatus.FAILED }
- );
-
- logger.warn(
- 'Something went wrong sending series request to Sonarr, marking status as FAILED',
- {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- sonarrSeriesOptions,
- }
- );
-
- this.sendNotification(media, Notification.MEDIA_FAILED);
- })
- .finally(() => {
- sonarr.clearCache({
- tvdbId,
- externalId: this.is4k
- ? media.externalServiceId4k
- : media.externalServiceId,
- title: series.name,
- });
- });
- logger.info('Sent request to Sonarr', {
- label: 'Media Request',
- requestId: this.id,
- mediaId: this.media.id,
- });
- } catch (e) {
- logger.error('Something went wrong sending request to Sonarr', {
- label: 'Media Request',
- errorMessage: e.message,
- requestId: this.id,
- mediaId: this.media.id,
- });
- throw new Error(e.message);
- }
- }
- }
-
- private async sendNotification(media: Media, type: Notification) {
+ static async sendNotification(
+ entity: MediaRequest,
+ media: Media,
+ type: Notification
+ ) {
const tmdb = new TheMovieDb();
try {
- const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
+ const mediaType = entity.type === MediaType.MOVIE ? 'Movie' : 'Series';
let event: string | undefined;
let notifyAdmin = true;
let notifySystem = true;
switch (type) {
case Notification.MEDIA_APPROVED:
- event = `${this.is4k ? '4K ' : ''}${mediaType} Request Approved`;
+ event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Approved`;
notifyAdmin = false;
break;
case Notification.MEDIA_DECLINED:
- event = `${this.is4k ? '4K ' : ''}${mediaType} Request Declined`;
+ event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Declined`;
notifyAdmin = false;
break;
case Notification.MEDIA_PENDING:
- event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
+ event = `New ${entity.is4k ? '4K ' : ''}${mediaType} Request`;
break;
case Notification.MEDIA_AUTO_REQUESTED:
event = `${
- this.is4k ? '4K ' : ''
+ entity.is4k ? '4K ' : ''
}${mediaType} Request Automatically Submitted`;
notifyAdmin = false;
notifySystem = false;
break;
case Notification.MEDIA_AUTO_APPROVED:
event = `${
- this.is4k ? '4K ' : ''
+ entity.is4k ? '4K ' : ''
}${mediaType} Request Automatically Approved`;
break;
case Notification.MEDIA_FAILED:
- event = `${this.is4k ? '4K ' : ''}${mediaType} Request Failed`;
+ event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Failed`;
break;
}
- if (this.type === MediaType.MOVIE) {
+ if (entity.type === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
notificationManager.sendNotification(type, {
media,
- request: this,
+ request: entity,
notifyAdmin,
notifySystem,
- notifyUser: notifyAdmin ? undefined : this.requestedBy,
+ notifyUser: notifyAdmin ? undefined : entity.requestedBy,
event,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
@@ -1409,14 +759,14 @@ export class MediaRequest {
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
});
- } else if (this.type === MediaType.TV) {
+ } else if (entity.type === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(type, {
media,
- request: this,
+ request: entity,
notifyAdmin,
notifySystem,
- notifyUser: notifyAdmin ? undefined : this.requestedBy,
+ notifyUser: notifyAdmin ? undefined : entity.requestedBy,
event,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
@@ -1430,7 +780,7 @@ export class MediaRequest {
extra: [
{
name: 'Requested Seasons',
- value: this.seasons
+ value: entity.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
@@ -1441,8 +791,8 @@ export class MediaRequest {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
- requestId: this.id,
- mediaId: this.media.id,
+ requestId: entity.id,
+ mediaId: entity.media.id,
});
}
}
diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts
index 2b293d97b..3cb6cacb7 100644
--- a/server/subscriber/MediaRequestSubscriber.ts
+++ b/server/subscriber/MediaRequestSubscriber.ts
@@ -1,14 +1,32 @@
+import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
+import RadarrAPI from '@server/api/servarr/radarr';
+import type {
+ AddSeriesOptions,
+ SonarrSeries,
+} 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,
MediaType,
} from '@server/constants/media';
+import { getRepository } from '@server/datasource';
+import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
+import SeasonRequest from '@server/entity/SeasonRequest';
import notificationManager, { Notification } from '@server/lib/notifications';
+import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
-import { truncate } from 'lodash';
-import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
+import { isEqual, truncate } from 'lodash';
+import type {
+ EntityManager,
+ EntitySubscriberInterface,
+ InsertEvent,
+ RemoveEvent,
+ UpdateEvent,
+} from 'typeorm';
import { EventSubscriber } from 'typeorm';
@EventSubscriber()
@@ -110,21 +128,690 @@ export class MediaRequestSubscriber
}
}
- public afterUpdate(event: UpdateEvent): void {
+ public async sendToRadarr(entity: MediaRequest): Promise {
+ if (
+ entity.status === MediaRequestStatus.APPROVED &&
+ entity.type === MediaType.MOVIE
+ ) {
+ try {
+ const mediaRepository = getRepository(Media);
+ const settings = getSettings();
+ if (settings.radarr.length === 0 && !settings.radarr[0]) {
+ logger.info(
+ 'No Radarr server configured, skipping request processing',
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ return;
+ }
+
+ let radarrSettings = settings.radarr.find(
+ (radarr) => radarr.isDefault && radarr.is4k === entity.is4k
+ );
+
+ if (
+ entity.serverId !== null &&
+ entity.serverId >= 0 &&
+ radarrSettings?.id !== entity.serverId
+ ) {
+ radarrSettings = settings.radarr.find(
+ (radarr) => radarr.id === entity.serverId
+ );
+ logger.info(
+ `Request has an override server: ${radarrSettings?.name}`,
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ }
+
+ if (!radarrSettings) {
+ logger.warn(
+ `There is no default ${
+ entity.is4k ? '4K ' : ''
+ }Radarr server configured. Did you set any of your ${
+ entity.is4k ? '4K ' : ''
+ }Radarr servers as default?`,
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ return;
+ }
+
+ let rootFolder = radarrSettings.activeDirectory;
+ let qualityProfile = radarrSettings.activeProfileId;
+ let tags = radarrSettings.tags ? [...radarrSettings.tags] : [];
+
+ if (
+ entity.rootFolder &&
+ entity.rootFolder !== '' &&
+ entity.rootFolder !== radarrSettings.activeDirectory
+ ) {
+ rootFolder = entity.rootFolder;
+ logger.info(`Request has an override root folder: ${rootFolder}`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ }
+
+ if (
+ entity.profileId &&
+ entity.profileId !== radarrSettings.activeProfileId
+ ) {
+ qualityProfile = entity.profileId;
+ logger.info(
+ `Request has an override quality profile ID: ${qualityProfile}`,
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ }
+
+ if (entity.tags && !isEqual(entity.tags, radarrSettings.tags)) {
+ tags = entity.tags;
+ logger.info(`Request has override tags`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ tagIds: tags,
+ });
+ }
+
+ const tmdb = new TheMovieDb();
+ const radarr = new RadarrAPI({
+ apiKey: radarrSettings.apiKey,
+ url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
+ });
+ const movie = await tmdb.getMovie({ movieId: entity.media.tmdbId });
+
+ const media = await mediaRepository.findOne({
+ where: { id: entity.media.id },
+ });
+
+ if (!media) {
+ logger.error('Media data not found', {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ return;
+ }
+
+ if (radarrSettings.tagRequests) {
+ let userTag = (await radarr.getTags()).find((v) =>
+ v.label.startsWith(entity.requestedBy.id + ' - ')
+ );
+ if (!userTag) {
+ logger.info(`Requester has no active tag. Creating new`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ userId: entity.requestedBy.id,
+ newTag:
+ entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
+ });
+ userTag = await radarr.createTag({
+ label:
+ entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
+ });
+ }
+ if (userTag.id) {
+ if (!tags?.find((v) => v === userTag?.id)) {
+ tags?.push(userTag.id);
+ }
+ } else {
+ logger.warn(`Requester has no tag and failed to add one`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ userId: entity.requestedBy.id,
+ radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
+ });
+ }
+ }
+
+ if (
+ media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
+ ) {
+ logger.warn('Media already exists, marking request as APPROVED', {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+
+ const requestRepository = getRepository(MediaRequest);
+ entity.status = MediaRequestStatus.APPROVED;
+ await requestRepository.save(entity);
+ return;
+ }
+
+ const radarrMovieOptions: RadarrMovieOptions = {
+ profileId: qualityProfile,
+ qualityProfileId: qualityProfile,
+ rootFolderPath: rootFolder,
+ minimumAvailability: radarrSettings.minimumAvailability,
+ title: movie.title,
+ tmdbId: movie.id,
+ year: Number(movie.release_date.slice(0, 4)),
+ monitored: true,
+ tags,
+ searchNow: !radarrSettings.preventSearch,
+ };
+
+ // Run entity asynchronously so we don't wait for it on the UI side
+ radarr
+ .addMovie(radarrMovieOptions)
+ .then(async (radarrMovie) => {
+ // We grab media again here to make sure we have the latest version of it
+ const media = await mediaRepository.findOne({
+ where: { id: entity.media.id },
+ });
+
+ if (!media) {
+ throw new Error('Media data not found');
+ }
+
+ media[entity.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
+ radarrMovie.id;
+ media[
+ entity.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
+ ] = radarrMovie.titleSlug;
+ media[entity.is4k ? 'serviceId4k' : 'serviceId'] =
+ radarrSettings?.id;
+ await mediaRepository.save(media);
+ })
+ .catch(async () => {
+ const requestRepository = getRepository(MediaRequest);
+
+ entity.status = MediaRequestStatus.FAILED;
+ requestRepository.save(entity);
+
+ logger.warn(
+ 'Something went wrong sending movie request to Radarr, marking status as FAILED',
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ radarrMovieOptions,
+ }
+ );
+
+ MediaRequest.sendNotification(
+ entity,
+ media,
+ Notification.MEDIA_FAILED
+ );
+ })
+ .finally(() => {
+ radarr.clearCache({
+ tmdbId: movie.id,
+ externalId: entity.is4k
+ ? media.externalServiceId4k
+ : media.externalServiceId,
+ });
+ });
+ logger.info('Sent request to Radarr', {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ } catch (e) {
+ logger.error('Something went wrong sending request to Radarr', {
+ label: 'Media Request',
+ errorMessage: e.message,
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ throw new Error(e.message);
+ }
+ }
+ }
+
+ public async sendToSonarr(entity: MediaRequest): Promise {
+ if (
+ entity.status === MediaRequestStatus.APPROVED &&
+ entity.type === MediaType.TV
+ ) {
+ try {
+ const mediaRepository = getRepository(Media);
+ const settings = getSettings();
+ if (settings.sonarr.length === 0 && !settings.sonarr[0]) {
+ logger.warn(
+ 'No Sonarr server configured, skipping request processing',
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ return;
+ }
+
+ let sonarrSettings = settings.sonarr.find(
+ (sonarr) => sonarr.isDefault && sonarr.is4k === entity.is4k
+ );
+
+ if (
+ entity.serverId !== null &&
+ entity.serverId >= 0 &&
+ sonarrSettings?.id !== entity.serverId
+ ) {
+ sonarrSettings = settings.sonarr.find(
+ (sonarr) => sonarr.id === entity.serverId
+ );
+ logger.info(
+ `Request has an override server: ${sonarrSettings?.name}`,
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ }
+
+ if (!sonarrSettings) {
+ logger.warn(
+ `There is no default ${
+ entity.is4k ? '4K ' : ''
+ }Sonarr server configured. Did you set any of your ${
+ entity.is4k ? '4K ' : ''
+ }Sonarr servers as default?`,
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ return;
+ }
+
+ const media = await mediaRepository.findOne({
+ where: { id: entity.media.id },
+ relations: { requests: true },
+ });
+
+ if (!media) {
+ throw new Error('Media data not found');
+ }
+
+ if (
+ media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
+ ) {
+ logger.warn('Media already exists, marking request as APPROVED', {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+
+ const requestRepository = getRepository(MediaRequest);
+ entity.status = MediaRequestStatus.APPROVED;
+ await requestRepository.save(entity);
+ return;
+ }
+
+ const tmdb = new TheMovieDb();
+ const sonarr = new SonarrAPI({
+ apiKey: sonarrSettings.apiKey,
+ url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
+ });
+ const series = await tmdb.getTvShow({ tvId: media.tmdbId });
+ const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
+
+ if (!tvdbId) {
+ const requestRepository = getRepository(MediaRequest);
+ await mediaRepository.remove(media);
+ await requestRepository.remove(entity);
+ throw new Error('TVDB ID not found');
+ }
+
+ 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
+ )
+ ) {
+ seriesType = sonarrSettings.animeSeriesType ?? 'anime';
+ }
+
+ let rootFolder =
+ seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
+ ? sonarrSettings.activeAnimeDirectory
+ : sonarrSettings.activeDirectory;
+ let qualityProfile =
+ seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
+ ? sonarrSettings.activeAnimeProfileId
+ : sonarrSettings.activeProfileId;
+ let languageProfile =
+ seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
+ ? sonarrSettings.activeAnimeLanguageProfileId
+ : sonarrSettings.activeLanguageProfileId;
+ let tags =
+ seriesType === 'anime'
+ ? sonarrSettings.animeTags
+ ? [...sonarrSettings.animeTags]
+ : []
+ : sonarrSettings.tags
+ ? [...sonarrSettings.tags]
+ : [];
+
+ if (
+ entity.rootFolder &&
+ entity.rootFolder !== '' &&
+ entity.rootFolder !== rootFolder
+ ) {
+ rootFolder = entity.rootFolder;
+ logger.info(`Request has an override root folder: ${rootFolder}`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ }
+
+ if (entity.profileId && entity.profileId !== qualityProfile) {
+ qualityProfile = entity.profileId;
+ logger.info(
+ `Request has an override quality profile ID: ${qualityProfile}`,
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ }
+
+ if (
+ entity.languageProfileId &&
+ entity.languageProfileId !== languageProfile
+ ) {
+ languageProfile = entity.languageProfileId;
+ logger.info(
+ `Request has an override language profile ID: ${languageProfile}`,
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ }
+ );
+ }
+
+ if (entity.tags && !isEqual(entity.tags, tags)) {
+ tags = entity.tags;
+ logger.info(`Request has override tags`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ tagIds: tags,
+ });
+ }
+
+ if (sonarrSettings.tagRequests) {
+ let userTag = (await sonarr.getTags()).find((v) =>
+ v.label.startsWith(entity.requestedBy.id + ' - ')
+ );
+ if (!userTag) {
+ logger.info(`Requester has no active tag. Creating new`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ userId: entity.requestedBy.id,
+ newTag:
+ entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
+ });
+ userTag = await sonarr.createTag({
+ label:
+ entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
+ });
+ }
+ if (userTag.id) {
+ if (!tags?.find((v) => v === userTag?.id)) {
+ tags?.push(userTag.id);
+ }
+ } else {
+ logger.warn(`Requester has no tag and failed to add one`, {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ userId: entity.requestedBy.id,
+ sonarrServer: sonarrSettings.hostname + ':' + sonarrSettings.port,
+ });
+ }
+ }
+
+ const sonarrSeriesOptions: AddSeriesOptions = {
+ profileId: qualityProfile,
+ languageProfileId: languageProfile,
+ rootFolderPath: rootFolder,
+ title: series.name,
+ tvdbid: tvdbId,
+ seasons: entity.seasons.map((season) => season.seasonNumber),
+ seasonFolder: sonarrSettings.enableSeasonFolders,
+ seriesType,
+ tags,
+ monitored: true,
+ searchNow: !sonarrSettings.preventSearch,
+ };
+
+ // Run entity asynchronously so we don't wait for it on the UI side
+ sonarr
+ .addSeries(sonarrSeriesOptions)
+ .then(async (sonarrSeries) => {
+ // We grab media again here to make sure we have the latest version of it
+ const media = await mediaRepository.findOne({
+ where: { id: entity.media.id },
+ relations: { requests: true },
+ });
+
+ if (!media) {
+ throw new Error('Media data not found');
+ }
+
+ media[entity.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
+ sonarrSeries.id;
+ media[
+ entity.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
+ ] = sonarrSeries.titleSlug;
+ media[entity.is4k ? 'serviceId4k' : 'serviceId'] =
+ sonarrSettings?.id;
+ await mediaRepository.save(media);
+ })
+ .catch(async () => {
+ const requestRepository = getRepository(MediaRequest);
+
+ entity.status = MediaRequestStatus.FAILED;
+ requestRepository.save(entity);
+
+ logger.warn(
+ 'Something went wrong sending series request to Sonarr, marking status as FAILED',
+ {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ sonarrSeriesOptions,
+ }
+ );
+
+ MediaRequest.sendNotification(
+ entity,
+ media,
+ Notification.MEDIA_FAILED
+ );
+ })
+ .finally(() => {
+ sonarr.clearCache({
+ tvdbId,
+ externalId: entity.is4k
+ ? media.externalServiceId4k
+ : media.externalServiceId,
+ title: series.name,
+ });
+ });
+ logger.info('Sent request to Sonarr', {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ } catch (e) {
+ logger.error('Something went wrong sending request to Sonarr', {
+ label: 'Media Request',
+ errorMessage: e.message,
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ throw new Error(e.message);
+ }
+ }
+ }
+
+ public async updateParentStatus(entity: MediaRequest): Promise {
+ const mediaRepository = getRepository(Media);
+ const media = await mediaRepository.findOne({
+ where: { id: entity.media.id },
+ relations: { requests: true },
+ });
+ if (!media) {
+ logger.error('Media data not found', {
+ label: 'Media Request',
+ requestId: entity.id,
+ mediaId: entity.media.id,
+ });
+ return;
+ }
+ const seasonRequestRepository = getRepository(SeasonRequest);
+ if (
+ entity.status === MediaRequestStatus.APPROVED &&
+ // Do not update the status if the item is already partially available or available
+ media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
+ media[entity.is4k ? 'status4k' : 'status'] !==
+ MediaStatus.PARTIALLY_AVAILABLE &&
+ media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
+ ) {
+ media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
+ mediaRepository.save(media);
+ }
+
+ if (
+ media.mediaType === MediaType.MOVIE &&
+ entity.status === MediaRequestStatus.DECLINED &&
+ media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
+ ) {
+ media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
+ mediaRepository.save(media);
+ }
+
+ /**
+ * If the media type is TV, and we are declining a request,
+ * we must check if its the only pending request and that
+ * there the current media status is just pending (meaning no
+ * other requests have yet to be approved)
+ */
+ if (
+ media.mediaType === MediaType.TV &&
+ entity.status === MediaRequestStatus.DECLINED &&
+ media.requests.filter(
+ (request) => request.status === MediaRequestStatus.PENDING
+ ).length === 0 &&
+ media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
+ media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
+ ) {
+ media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
+ mediaRepository.save(media);
+ }
+
+ // Approve child seasons if parent is approved
+ if (
+ media.mediaType === MediaType.TV &&
+ entity.status === MediaRequestStatus.APPROVED
+ ) {
+ entity.seasons.forEach((season) => {
+ season.status = MediaRequestStatus.APPROVED;
+ seasonRequestRepository.save(season);
+ });
+ }
+ }
+
+ public async handleRemoveParentUpdate(
+ manager: EntityManager,
+ entity: MediaRequest
+ ): Promise {
+ const fullMedia = await manager.findOneOrFail(Media, {
+ where: { id: entity.media.id },
+ relations: { requests: true },
+ });
+
+ if (!fullMedia) return;
+
+ if (
+ !fullMedia.requests.some((request) => !request.is4k) &&
+ fullMedia.status !== MediaStatus.AVAILABLE
+ ) {
+ fullMedia.status = MediaStatus.UNKNOWN;
+ }
+
+ if (
+ !fullMedia.requests.some((request) => request.is4k) &&
+ fullMedia.status4k !== MediaStatus.AVAILABLE
+ ) {
+ fullMedia.status4k = MediaStatus.UNKNOWN;
+ }
+
+ await manager.save(fullMedia);
+ }
+
+ public async afterUpdate(event: UpdateEvent): Promise {
if (!event.entity) {
return;
}
+ await this.sendToRadarr(event.entity as MediaRequest);
+ await this.sendToSonarr(event.entity as MediaRequest);
+
+ await this.updateParentStatus(event.entity as MediaRequest);
+
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
- this.notifyAvailableMovie(event.entity as MediaRequest);
+ await this.notifyAvailableMovie(event.entity as MediaRequest);
}
if (event.entity.media.mediaType === MediaType.TV) {
- this.notifyAvailableSeries(event.entity as MediaRequest);
+ await this.notifyAvailableSeries(event.entity as MediaRequest);
}
}
}
+ public async afterInsert(event: InsertEvent): Promise {
+ if (!event.entity) {
+ return;
+ }
+
+ await this.sendToRadarr(event.entity as MediaRequest);
+ await this.sendToSonarr(event.entity as MediaRequest);
+
+ await this.updateParentStatus(event.entity as MediaRequest);
+ }
+
+ public async afterRemove(event: RemoveEvent): Promise {
+ if (!event.entity) {
+ return;
+ }
+
+ await this.handleRemoveParentUpdate(
+ event.manager as EntityManager,
+ event.entity as MediaRequest
+ );
+ }
+
public listenTo(): typeof MediaRequest {
return MediaRequest;
}
diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx
index 4124e3e4a..0202fbfaa 100644
--- a/src/components/RequestModal/TvRequestModal.tsx
+++ b/src/components/RequestModal/TvRequestModal.tsx
@@ -202,7 +202,8 @@ const TvRequestModal = ({
seasons: settings.currentSettings.partialRequestsEnabled
? selectedSeasons.sort((a, b) => a - b)
: getAllSeasons().filter(
- (season) => !getAllRequestedSeasons().includes(season)
+ (season) =>
+ !getAllRequestedSeasons().includes(season) && season !== 0
),
...overrideParams,
});
@@ -302,8 +303,10 @@ const TvRequestModal = ({
}
};
- const unrequestedSeasons = getAllSeasons().filter(
- (season) => !getAllRequestedSeasons().includes(season)
+ const unrequestedSeasons = getAllSeasons().filter((season) =>
+ !settings.currentSettings.partialRequestsEnabled
+ ? !getAllRequestedSeasons().includes(season) && season !== 0
+ : !getAllRequestedSeasons().includes(season)
);
const toggleAllSeasons = (): void => {
@@ -575,7 +578,11 @@ const TvRequestModal = ({
(season) =>
(!settings.currentSettings.enableSpecialEpisodes
? season.seasonNumber !== 0
- : true) && season.episodeCount !== 0
+ : true) &&
+ (!settings.currentSettings.partialRequestsEnabled
+ ? season.episodeCount !== 0 &&
+ season.seasonNumber !== 0
+ : season.episodeCount !== 0)
)
.map((season) => {
const seasonRequest = getSeasonRequest(
diff --git a/src/i18n/locale/zh_Hant.json b/src/i18n/locale/zh_Hant.json
index 5f4af08ad..836e2ce36 100644
--- a/src/i18n/locale/zh_Hant.json
+++ b/src/i18n/locale/zh_Hant.json
@@ -721,8 +721,6 @@
"components.Settings.Notifications.NotificationsLunaSea.validationWebhookUrl": "請輸入有效的網址",
"components.Settings.Notifications.NotificationsLunaSea.agentenabled": "啟用通知",
"components.Settings.is4k": "4K",
- "components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingsfailed": "網路推送通知設定儲存失敗。",
- "components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingssaved": "網路推送通知設定儲存成功!",
"components.Settings.Notifications.toastEmailTestSuccess": "電子郵件測試通知已發送!",
"components.Settings.Notifications.NotificationsWebPush.toastWebPushTestSuccess": "網路推送測試通知已發送!",
"components.Settings.Notifications.toastTelegramTestSuccess": "Telegram 測試通知已發送!",
@@ -1205,5 +1203,26 @@
"components.Settings.SettingsJobsCache.imagecachesize": "總快取大小",
"components.Settings.SettingsMain.applicationurl": "應用程式網址",
"components.Settings.SettingsMain.cacheImages": "啟用影像快取",
- "components.Settings.SettingsMain.cacheImagesTip": "快取外部來源影像(需要大量磁碟空間)"
+ "components.Settings.SettingsMain.cacheImagesTip": "快取外部來源影像(需要大量磁碟空間)",
+ "components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# 個使用中的篩選項目} other {# 個使用中的篩選項目}}",
+ "components.Discover.DiscoverTv.activefilters": "{count, plural, one {# 個使用中的篩選項目} other {# 個使用中的篩選項目}}",
+ "components.Discover.FilterSlideover.activefilters": "{count, plural, one {# 個使用中的篩選項目} other {# 個使用中的篩選項目}}",
+ "components.Discover.FilterSlideover.tmdbuservotecount": "TMDB 用戶評分數",
+ "components.Discover.FilterSlideover.voteCount": "在 {minValue} 和 {maxValue} 之間的評分數",
+ "components.Discover.tmdbmoviestreamingservices": "TMDB 電影串流服務",
+ "components.Settings.Notifications.NotificationsPushover.deviceDefault": "預設裝置",
+ "components.Settings.Notifications.NotificationsPushover.sound": "通知提示聲",
+ "components.Discover.tmdbtvstreamingservices": "TMDB 電視串流服務",
+ "components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "每 {jobScheduleSeconds} 秒",
+ "components.Settings.SettingsJobsCache.availability-sync": "同步媒體可用性",
+ "components.Settings.SonarrModal.seriesType": "劇集類型",
+ "components.Settings.SonarrModal.tagRequests": "標記請求",
+ "components.Settings.SonarrModal.tagRequestsInfo": "自動新增帶有請求者的用户 ID 和顯示名稱的附加標籤",
+ "components.Settings.RadarrModal.tagRequests": "標籤請求",
+ "components.Settings.RadarrModal.tagRequestsInfo": "自動新增帶有請求者的用户 ID 和顯示名稱的附加標籤",
+ "components.UserProfile.UserSettings.UserNotificationSettings.sound": "通知提示聲",
+ "i18n.collection": "合集",
+ "components.MovieDetails.imdbuserscore": "IMDB 用戶評分",
+ "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "預設裝置",
+ "components.Settings.SonarrModal.animeSeriesType": "動漫劇集類型"
}