fix: address unhandled promise rejections & bump node to v16.13 (#2398)

* fix: unhandled promise rejections

* build(deps): bump node from 14.18 to 16.13

* fix: unhandled promise rejection in new Plex users endpoint

* fix: build error

Co-authored-by: Ryan Cohen <ryan@sct.dev>
pull/2463/head
TheCatLady 3 years ago committed by GitHub
parent ca184728e9
commit 8cba486249
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,7 +12,7 @@ jobs:
test: test:
name: Lint & Test Build name: Lint & Test Build
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: node:14.18-alpine container: node:16.13-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.4.0 uses: actions/checkout@v2.4.0

@ -9,7 +9,7 @@ jobs:
test: test:
name: Lint & Test Build name: Lint & Test Build
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: node:14.18-alpine container: node:16.13-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.4.0 uses: actions/checkout@v2.4.0

@ -20,7 +20,7 @@ jobs:
name: Lint & Test Build name: Lint & Test Build
needs: jobs needs: jobs
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: node:14.18-alpine container: node:16.13-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.4.0 uses: actions/checkout@v2.4.0

@ -1,4 +1,4 @@
FROM node:14.18-alpine AS BUILD_IMAGE FROM node:16.13-alpine AS BUILD_IMAGE
WORKDIR /app WORKDIR /app
@ -33,7 +33,7 @@ RUN touch config/DOCKER
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:14.18-alpine FROM node:16.13-alpine
WORKDIR /app WORKDIR /app

@ -1,4 +1,4 @@
FROM node:14.18-alpine FROM node:16.13-alpine
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app

@ -1,7 +1,7 @@
import logger from '../../logger'; import logger from '../../logger';
import ServarrBase from './base'; import ServarrBase from './base';
interface RadarrMovieOptions { export interface RadarrMovieOptions {
title: string; title: string;
qualityProfileId: number; qualityProfileId: number;
minimumAvailability: string; minimumAvailability: string;

@ -63,7 +63,7 @@ export interface SonarrSeries {
}; };
} }
interface AddSeriesOptions { export interface AddSeriesOptions {
tvdbid: number; tvdbid: number;
title: string; title: string;
profileId: number; profileId: number;

@ -13,8 +13,11 @@ import {
RelationCount, RelationCount,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import RadarrAPI from '../api/servarr/radarr'; import RadarrAPI, { RadarrMovieOptions } from '../api/servarr/radarr';
import SonarrAPI, { SonarrSeries } from '../api/servarr/sonarr'; import SonarrAPI, {
AddSeriesOptions,
SonarrSeries,
} from '../api/servarr/sonarr';
import TheMovieDb from '../api/themoviedb'; import TheMovieDb from '../api/themoviedb';
import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants'; import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media'; import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
@ -135,55 +138,15 @@ export class MediaRequest {
where: { id: this.media.id }, where: { id: this.media.id },
}); });
if (!media) { if (!media) {
logger.error('No parent media!', { label: 'Media Request' }); logger.error('Media data not found', {
return; label: 'Media Request',
} requestId: this.id,
const tmdb = new TheMovieDb(); mediaId: this.media.id,
if (this.type === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, {
event: `New ${this.is4k ? '4K ' : ''}Movie Request`,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
media,
request: this,
notifyAdmin: true,
}); });
return;
} }
if (this.type === MediaType.TV) { this.sendNotification(media, Notification.MEDIA_PENDING);
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_PENDING, {
event: `New ${this.is4k ? '4K ' : ''}Series Request`,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media,
extra: [
{
name: 'Requested Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: this,
notifyAdmin: true,
});
}
} }
} }
@ -204,90 +167,30 @@ export class MediaRequest {
where: { id: this.media.id }, where: { id: this.media.id },
}); });
if (!media) { if (!media) {
logger.error('No parent media!', { label: 'Media Request' }); logger.error('Media data not found', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
return; return;
} }
if (media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE) { if (media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE) {
logger.warn( logger.warn(
'Media became available before request was approved. Approval notification will be skipped.', 'Media became available before request was approved. Skipping approval notification',
{ label: 'Media Request' } { label: 'Media Request', requestId: this.id, mediaId: this.media.id }
); );
return; return;
} }
const tmdb = new TheMovieDb(); this.sendNotification(
if (this.media.mediaType === MediaType.MOVIE) { media,
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId }); this.status === MediaRequestStatus.APPROVED
notificationManager.sendNotification( ? autoApproved
this.status === MediaRequestStatus.APPROVED ? Notification.MEDIA_AUTO_APPROVED
? autoApproved : Notification.MEDIA_APPROVED
? Notification.MEDIA_AUTO_APPROVED : Notification.MEDIA_DECLINED
: Notification.MEDIA_APPROVED );
: Notification.MEDIA_DECLINED,
{
event: `${this.is4k ? '4K ' : ''}Movie Request ${
this.status === MediaRequestStatus.APPROVED
? autoApproved
? 'Automatically Approved'
: 'Approved'
: 'Declined'
}`,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyAdmin: autoApproved,
notifyUser: autoApproved ? undefined : this.requestedBy,
media,
request: this,
}
);
} else if (this.media.mediaType === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: this.media.tmdbId });
notificationManager.sendNotification(
this.status === MediaRequestStatus.APPROVED
? autoApproved
? Notification.MEDIA_AUTO_APPROVED
: Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED,
{
event: `${this.is4k ? '4K ' : ''}Series Request ${
this.status === MediaRequestStatus.APPROVED
? autoApproved
? 'Automatically Approved'
: 'Approved'
: 'Declined'
}`,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyAdmin: autoApproved,
notifyUser: autoApproved ? undefined : this.requestedBy,
media,
extra: [
{
name: 'Requested Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: this,
}
);
}
} }
} }
@ -307,7 +210,11 @@ export class MediaRequest {
relations: ['requests'], relations: ['requests'],
}); });
if (!media) { if (!media) {
logger.error('No parent media!', { label: 'Media Request' }); logger.error('Media data not found', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
return; return;
} }
const seasonRequestRepository = getRepository(SeasonRequest); const seasonRequestRepository = getRepository(SeasonRequest);
@ -395,8 +302,12 @@ export class MediaRequest {
const settings = getSettings(); const settings = getSettings();
if (settings.radarr.length === 0 && !settings.radarr[0]) { if (settings.radarr.length === 0 && !settings.radarr[0]) {
logger.info( logger.info(
'Skipped Radarr request as there is no Radarr server configured', 'No Radarr server configured, skipping request processing',
{ label: 'Media Request' } {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
); );
return; return;
} }
@ -415,18 +326,26 @@ export class MediaRequest {
); );
logger.info( logger.info(
`Request has an override server: ${radarrSettings?.name}`, `Request has an override server: ${radarrSettings?.name}`,
{ label: 'Media Request' } {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
); );
} }
if (!radarrSettings) { if (!radarrSettings) {
logger.info( logger.warn(
`There is no default ${ `There is no default ${
this.is4k ? '4K ' : '' this.is4k ? '4K ' : ''
}Radarr server configured. Did you set any of your ${ }Radarr server configured. Did you set any of your ${
this.is4k ? '4K ' : '' this.is4k ? '4K ' : ''
}Radarr servers as default?`, }Radarr servers as default?`,
{ label: 'Media Request' } {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
); );
return; return;
} }
@ -443,6 +362,8 @@ export class MediaRequest {
rootFolder = this.rootFolder; rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, { logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request', label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}); });
} }
@ -451,15 +372,22 @@ export class MediaRequest {
this.profileId !== radarrSettings.activeProfileId this.profileId !== radarrSettings.activeProfileId
) { ) {
qualityProfile = this.profileId; qualityProfile = this.profileId;
logger.info(`Request has an override profile id: ${qualityProfile}`, { logger.info(
label: 'Media Request', `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)) { if (this.tags && !isEqual(this.tags, radarrSettings.tags)) {
tags = this.tags; tags = this.tags;
logger.info(`Request has override tags`, { logger.info(`Request has override tags`, {
label: 'Media Request', label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
tagIds: tags, tagIds: tags,
}); });
} }
@ -476,7 +404,11 @@ export class MediaRequest {
}); });
if (!media) { if (!media) {
logger.error('Media not present'); logger.error('Media data not found', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
return; return;
} }
@ -486,20 +418,22 @@ export class MediaRequest {
throw new Error('Media already available'); throw new Error('Media already available');
} }
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 // Run this asynchronously so we don't wait for it on the UI side
radarr radarr
.addMovie({ .addMovie(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,
})
.then(async (radarrMovie) => { .then(async (radarrMovie) => {
// We grab media again here to make sure we have the latest version of it // We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({ const media = await mediaRepository.findOne({
@ -507,7 +441,7 @@ export class MediaRequest {
}); });
if (!media) { if (!media) {
throw new Error('Media data is missing'); throw new Error('Media data not found');
} }
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] = media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
@ -521,36 +455,30 @@ export class MediaRequest {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN; media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media); await mediaRepository.save(media);
logger.warn( logger.warn(
'Newly added movie request failed to add to Radarr, marking as unknown', 'Something went wrong sending movie request to Radarr, marking status as UNKNOWN',
{ {
label: 'Media Request', label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
radarrMovieOptions,
} }
); );
notificationManager.sendNotification(Notification.MEDIA_FAILED, { this.sendNotification(media, Notification.MEDIA_FAILED);
event: `${this.is4k ? '4K ' : ''}Movie Request Failed`,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: this,
notifyAdmin: true,
});
}); });
logger.info('Sent request to Radarr', { label: 'Media Request' }); logger.info('Sent request to Radarr', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
} catch (e) { } catch (e) {
const errorMessage = `Request failed to send to Radarr: ${e.message}`; logger.error('Something went wrong sending request to Radarr', {
logger.error('Request failed to send to Radarr', {
label: 'Media Request', label: 'Media Request',
errorMessage, errorMessage: e.message,
requestId: this.id,
mediaId: this.media.id,
}); });
throw new Error(errorMessage); throw new Error(e.message);
} }
} }
} }
@ -564,9 +492,13 @@ export class MediaRequest {
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
const settings = getSettings(); const settings = getSettings();
if (settings.sonarr.length === 0 && !settings.sonarr[0]) { if (settings.sonarr.length === 0 && !settings.sonarr[0]) {
logger.info( logger.warn(
'Skipped Sonarr request as there is no Sonarr server configured', 'No Sonarr server configured, skipping request processing',
{ label: 'Media Request' } {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
); );
return; return;
} }
@ -585,18 +517,26 @@ export class MediaRequest {
); );
logger.info( logger.info(
`Request has an override server: ${sonarrSettings?.name}`, `Request has an override server: ${sonarrSettings?.name}`,
{ label: 'Media Request' } {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
); );
} }
if (!sonarrSettings) { if (!sonarrSettings) {
logger.info( logger.warn(
`There is no default ${ `There is no default ${
this.is4k ? '4K ' : '' this.is4k ? '4K ' : ''
}Sonarr server configured. Did you set any of your ${ }Sonarr server configured. Did you set any of your ${
this.is4k ? '4K ' : '' this.is4k ? '4K ' : ''
}Sonarr servers as default?`, }Sonarr servers as default?`,
{ label: 'Media Request' } {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
); );
return; return;
} }
@ -607,7 +547,7 @@ export class MediaRequest {
}); });
if (!media) { if (!media) {
throw new Error('Media data is missing'); throw new Error('Media data not found');
} }
if ( if (
@ -628,7 +568,7 @@ export class MediaRequest {
const requestRepository = getRepository(MediaRequest); const requestRepository = getRepository(MediaRequest);
await mediaRepository.remove(media); await mediaRepository.remove(media);
await requestRepository.remove(this); await requestRepository.remove(this);
throw new Error('Series was missing tvdb id'); throw new Error('TVDB ID not found');
} }
let seriesType: SonarrSeries['seriesType'] = 'standard'; let seriesType: SonarrSeries['seriesType'] = 'standard';
@ -650,12 +590,10 @@ export class MediaRequest {
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
? sonarrSettings.activeAnimeProfileId ? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId; : sonarrSettings.activeProfileId;
let languageProfile = let languageProfile =
seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
? sonarrSettings.activeAnimeLanguageProfileId ? sonarrSettings.activeAnimeLanguageProfileId
: sonarrSettings.activeLanguageProfileId; : sonarrSettings.activeLanguageProfileId;
let tags = let tags =
seriesType === 'anime' seriesType === 'anime'
? sonarrSettings.animeTags ? sonarrSettings.animeTags
@ -669,14 +607,21 @@ export class MediaRequest {
rootFolder = this.rootFolder; rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, { logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request', label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}); });
} }
if (this.profileId && this.profileId !== qualityProfile) { if (this.profileId && this.profileId !== qualityProfile) {
qualityProfile = this.profileId; qualityProfile = this.profileId;
logger.info(`Request has an override profile ID: ${qualityProfile}`, { logger.info(
label: 'Media Request', `Request has an override quality profile ID: ${qualityProfile}`,
}); {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
}
);
} }
if ( if (
@ -685,9 +630,11 @@ export class MediaRequest {
) { ) {
languageProfile = this.languageProfileId; languageProfile = this.languageProfileId;
logger.info( logger.info(
`Request has an override Language Profile: ${languageProfile}`, `Request has an override language profile ID: ${languageProfile}`,
{ {
label: 'Media Request', label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
} }
); );
} }
@ -696,25 +643,29 @@ export class MediaRequest {
tags = this.tags; tags = this.tags;
logger.info(`Request has override tags`, { logger.info(`Request has override tags`, {
label: 'Media Request', label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
tagIds: tags, tagIds: tags,
}); });
} }
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 // Run this asynchronously so we don't wait for it on the UI side
sonarr sonarr
.addSeries({ .addSeries(sonarrSeriesOptions)
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,
})
.then(async (sonarrSeries) => { .then(async (sonarrSeries) => {
// We grab media again here to make sure we have the latest version of it // We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({ const media = await mediaRepository.findOne({
@ -723,7 +674,7 @@ export class MediaRequest {
}); });
if (!media) { if (!media) {
throw new Error('Media data is missing'); throw new Error('Media data not found');
} }
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] = media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
@ -737,47 +688,116 @@ export class MediaRequest {
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN; media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
await mediaRepository.save(media); await mediaRepository.save(media);
logger.warn( logger.warn(
'Newly added series request failed to add to Sonarr, marking as unknown', 'Something went wrong sending series request to Sonarr, marking status as UNKNOWN',
{ {
label: 'Media Request', label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
sonarrSeriesOptions,
} }
); );
notificationManager.sendNotification(Notification.MEDIA_FAILED, { this.sendNotification(media, Notification.MEDIA_FAILED);
event: `${this.is4k ? '4K ' : ''}Series Request Failed`,
subject: `${series.name}${
series.first_air_date
? ` (${series.first_air_date.slice(0, 4)})`
: ''
}`,
message: truncate(series.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
media,
extra: [
{
name: 'Requested Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: this,
notifyAdmin: true,
});
}); });
logger.info('Sent request to Sonarr', { label: 'Media Request' }); logger.info('Sent request to Sonarr', {
label: 'Media Request',
requestId: this.id,
mediaId: this.media.id,
});
} catch (e) { } catch (e) {
const errorMessage = `Request failed to send to Sonarr: ${e.message}`; logger.error('Something went wrong sending request to Sonarr', {
logger.error('Request failed to send to Sonarr', {
label: 'Media Request', label: 'Media Request',
errorMessage, errorMessage: e.message,
requestId: this.id,
mediaId: this.media.id,
});
throw new Error(e.message);
}
}
}
private async sendNotification(media: Media, type: Notification) {
const tmdb = new TheMovieDb();
try {
const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
let event: string | undefined;
let notifyAdmin = true;
switch (type) {
case Notification.MEDIA_APPROVED:
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Approved`;
notifyAdmin = false;
break;
case Notification.MEDIA_DECLINED:
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Declined`;
notifyAdmin = false;
break;
case Notification.MEDIA_PENDING:
event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
break;
case Notification.MEDIA_AUTO_APPROVED:
event = `${
this.is4k ? '4K ' : ''
}${mediaType} Request Automatically Approved`;
break;
case Notification.MEDIA_FAILED:
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Failed`;
break;
}
if (this.type === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
notificationManager.sendNotification(type, {
media,
request: this,
notifyAdmin,
notifyUser: notifyAdmin ? undefined : this.requestedBy,
event,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
});
} else if (this.type === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(type, {
media,
request: this,
notifyAdmin,
notifyUser: notifyAdmin ? undefined : this.requestedBy,
event,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
extra: [
{
name: 'Requested Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
}); });
throw new Error(errorMessage);
} }
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
requestId: this.id,
mediaId: this.media.id,
});
} }
} }
} }

@ -1,6 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import TheMovieDb from '../api/themoviedb'; import TheMovieDb from '../api/themoviedb';
import Media from '../entity/Media'; import Media from '../entity/Media';
import logger from '../logger';
import { mapCollection } from '../models/Collection'; import { mapCollection } from '../models/Collection';
const collectionRoutes = Router(); const collectionRoutes = Router();
@ -20,7 +21,15 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => {
return res.status(200).json(mapCollection(collection, media)); return res.status(200).json(mapCollection(collection, media));
} catch (e) { } catch (e) {
return next({ status: 404, message: 'Collection does not exist' }); logger.debug('Something went wrong retrieving collection', {
label: 'API',
errorMessage: e.message,
collectionId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve collection.',
});
} }
}); });

@ -37,54 +37,15 @@ export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
const discoverRoutes = Router(); const discoverRoutes = Router();
discoverRoutes.get('/movies', async (req, res) => { discoverRoutes.get('/movies', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user); const tmdb = createTmdbWithRegionLanguage(req.user);
const data = await tmdb.getDiscoverMovies({ try {
page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
studio: req.query.studio ? Number(req.query.studio) : undefined,
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
mapMovieResult(
result,
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});
});
discoverRoutes.get<{ language: string }>(
'/movies/language/:language',
async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
const languages = await tmdb.getLanguages();
const language = languages.find(
(lang) => lang.iso_639_1 === req.params.language
);
if (!language) {
return next({ status: 404, message: 'Unable to retrieve language' });
}
const data = await tmdb.getDiscoverMovies({ const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page), page: Number(req.query.page),
language: req.locale ?? (req.query.language as string), language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language, genre: req.query.genre ? Number(req.query.genre) : undefined,
studio: req.query.studio ? Number(req.query.studio) : undefined,
}); });
const media = await Media.getRelatedMedia( const media = await Media.getRelatedMedia(
@ -95,7 +56,6 @@ discoverRoutes.get<{ language: string }>(
page: data.page, page: data.page,
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
language,
results: data.results.map((result) => results: data.results.map((result) =>
mapMovieResult( mapMovieResult(
result, result,
@ -106,6 +66,70 @@ discoverRoutes.get<{ language: string }>(
) )
), ),
}); });
} catch (e) {
logger.debug('Something went wrong retrieving popular movies', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve popular movies.',
});
}
});
discoverRoutes.get<{ language: string }>(
'/movies/language/:language',
async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
try {
const languages = await tmdb.getLanguages();
const language = languages.find(
(lang) => lang.iso_639_1 === req.params.language
);
if (!language) {
return next({ status: 404, message: 'Language not found.' });
}
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language,
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
language,
results: data.results.map((result) =>
mapMovieResult(
result,
media.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});
} catch (e) {
logger.debug('Something went wrong retrieving movies by language', {
label: 'API',
errorMessage: e.message,
language: req.params.language,
});
return next({
status: 500,
message: 'Unable to retrieve movies by language.',
});
}
} }
); );
@ -114,43 +138,55 @@ discoverRoutes.get<{ genreId: string }>(
async (req, res, next) => { async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user); const tmdb = createTmdbWithRegionLanguage(req.user);
const genres = await tmdb.getMovieGenres({ try {
language: req.locale ?? (req.query.language as string), const genres = await tmdb.getMovieGenres({
}); language: req.locale ?? (req.query.language as string),
});
const genre = genres.find( const genre = genres.find(
(genre) => genre.id === Number(req.params.genreId) (genre) => genre.id === Number(req.params.genreId)
); );
if (!genre) { if (!genre) {
return next({ status: 404, message: 'Unable to retrieve genre' }); return next({ status: 404, message: 'Genre not found.' });
} }
const data = await tmdb.getDiscoverMovies({ const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page), page: Number(req.query.page),
language: req.locale ?? (req.query.language as string), language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId), genre: Number(req.params.genreId),
}); });
const media = await Media.getRelatedMedia( const media = await Media.getRelatedMedia(
data.results.map((result) => result.id) data.results.map((result) => result.id)
); );
return res.status(200).json({ return res.status(200).json({
page: data.page, page: data.page,
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
genre, genre,
results: data.results.map((result) => results: data.results.map((result) =>
mapMovieResult( mapMovieResult(
result, result,
media.find( media.find(
(req) => (req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
) )
) ),
), });
}); } catch (e) {
logger.debug('Something went wrong retrieving movies by genre', {
label: 'API',
errorMessage: e.message,
genreId: req.params.genreId,
});
return next({
status: 500,
message: 'Unable to retrieve movies by genre.',
});
}
} }
); );
@ -188,12 +224,20 @@ discoverRoutes.get<{ studioId: string }>(
), ),
}); });
} catch (e) { } catch (e) {
return next({ status: 404, message: 'Unable to retrieve studio' }); logger.debug('Something went wrong retrieving movies by studio', {
label: 'API',
errorMessage: e.message,
studioId: req.params.studioId,
});
return next({
status: 500,
message: 'Unable to retrieve movies by studio.',
});
} }
} }
); );
discoverRoutes.get('/movies/upcoming', async (req, res) => { discoverRoutes.get('/movies/upcoming', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user); const tmdb = createTmdbWithRegionLanguage(req.user);
const now = new Date(); const now = new Date();
@ -202,79 +246,11 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
.toISOString() .toISOString()
.split('T')[0]; .split('T')[0];
const data = await tmdb.getDiscoverMovies({ try {
page: Number(req.query.page), const data = await tmdb.getDiscoverMovies({
language: req.locale ?? (req.query.language as string),
primaryReleaseDateGte: date,
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
mapMovieResult(
result,
media.find(
(med) => med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
)
)
),
});
});
discoverRoutes.get('/tv', async (req, res) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
network: req.query.network ? Number(req.query.network) : undefined,
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
mapTvResult(
result,
media.find(
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
)
)
),
});
});
discoverRoutes.get<{ language: string }>(
'/tv/language/:language',
async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
const languages = await tmdb.getLanguages();
const language = languages.find(
(lang) => lang.iso_639_1 === req.params.language
);
if (!language) {
return next({ status: 404, message: 'Unable to retrieve language' });
}
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page), page: Number(req.query.page),
language: req.locale ?? (req.query.language as string), language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language, primaryReleaseDateGte: date,
}); });
const media = await Media.getRelatedMedia( const media = await Media.getRelatedMedia(
@ -285,40 +261,37 @@ discoverRoutes.get<{ language: string }>(
page: data.page, page: data.page,
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
language,
results: data.results.map((result) => results: data.results.map((result) =>
mapTvResult( mapMovieResult(
result, result,
media.find( media.find(
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV (med) =>
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
) )
) )
), ),
}); });
} } catch (e) {
); logger.debug('Something went wrong retrieving upcoming movies', {
label: 'API',
discoverRoutes.get<{ genreId: string }>( errorMessage: e.message,
'/tv/genre/:genreId',
async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
const genres = await tmdb.getTvGenres({
language: req.locale ?? (req.query.language as string),
}); });
return next({
status: 500,
message: 'Unable to retrieve upcoming movies.',
});
}
});
const genre = genres.find( discoverRoutes.get('/tv', async (req, res, next) => {
(genre) => genre.id === Number(req.params.genreId) const tmdb = createTmdbWithRegionLanguage(req.user);
);
if (!genre) {
return next({ status: 404, message: 'Unable to retrieve genre' });
}
try {
const data = await tmdb.getDiscoverTv({ const data = await tmdb.getDiscoverTv({
page: Number(req.query.page), page: Number(req.query.page),
language: req.locale ?? (req.query.language as string), language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId), genre: req.query.genre ? Number(req.query.genre) : undefined,
network: req.query.network ? Number(req.query.network) : undefined,
}); });
const media = await Media.getRelatedMedia( const media = await Media.getRelatedMedia(
@ -329,7 +302,6 @@ discoverRoutes.get<{ genreId: string }>(
page: data.page, page: data.page,
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
genre,
results: data.results.map((result) => results: data.results.map((result) =>
mapTvResult( mapTvResult(
result, result,
@ -339,21 +311,38 @@ discoverRoutes.get<{ genreId: string }>(
) )
), ),
}); });
} catch (e) {
logger.debug('Something went wrong retrieving popular series', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve popular series.',
});
} }
); });
discoverRoutes.get<{ networkId: string }>( discoverRoutes.get<{ language: string }>(
'/tv/network/:networkId', '/tv/language/:language',
async (req, res, next) => { async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = createTmdbWithRegionLanguage(req.user);
try { try {
const network = await tmdb.getNetwork(Number(req.params.networkId)); const languages = await tmdb.getLanguages();
const language = languages.find(
(lang) => lang.iso_639_1 === req.params.language
);
if (!language) {
return next({ status: 404, message: 'Language not found.' });
}
const data = await tmdb.getDiscoverTv({ const data = await tmdb.getDiscoverTv({
page: Number(req.query.page), page: Number(req.query.page),
language: req.locale ?? (req.query.language as string), language: req.locale ?? (req.query.language as string),
network: Number(req.params.networkId), originalLanguage: req.params.language,
}); });
const media = await Media.getRelatedMedia( const media = await Media.getRelatedMedia(
@ -364,7 +353,7 @@ discoverRoutes.get<{ networkId: string }>(
page: data.page, page: data.page,
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
network: mapNetwork(network), language,
results: data.results.map((result) => results: data.results.map((result) =>
mapTvResult( mapTvResult(
result, result,
@ -376,92 +365,137 @@ discoverRoutes.get<{ networkId: string }>(
), ),
}); });
} catch (e) { } catch (e) {
return next({ status: 404, message: 'Unable to retrieve network' }); logger.debug('Something went wrong retrieving series by language', {
label: 'API',
errorMessage: e.message,
language: req.params.language,
});
return next({
status: 500,
message: 'Unable to retrieve series by language.',
});
} }
} }
); );
discoverRoutes.get('/tv/upcoming', async (req, res) => { discoverRoutes.get<{ genreId: string }>(
const tmdb = createTmdbWithRegionLanguage(req.user); '/tv/genre/:genreId',
async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
const now = new Date(); try {
const offset = now.getTimezoneOffset(); const genres = await tmdb.getTvGenres({
const date = new Date(now.getTime() - offset * 60 * 1000) language: req.locale ?? (req.query.language as string),
.toISOString() });
.split('T')[0];
const data = await tmdb.getDiscoverTv({ const genre = genres.find(
page: Number(req.query.page), (genre) => genre.id === Number(req.params.genreId)
language: req.locale ?? (req.query.language as string), );
firstAirDateGte: date,
});
const media = await Media.getRelatedMedia( if (!genre) {
data.results.map((result) => result.id) return next({ status: 404, message: 'Genre not found.' });
); }
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
mapTvResult(
result,
media.find(
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
)
)
),
});
});
discoverRoutes.get('/trending', async (req, res) => { const data = await tmdb.getDiscoverTv({
const tmdb = createTmdbWithRegionLanguage(req.user); page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId),
});
const data = await tmdb.getAllTrending({ const media = await Media.getRelatedMedia(
page: Number(req.query.page), data.results.map((result) => result.id)
language: req.locale ?? (req.query.language as string), );
});
const media = await Media.getRelatedMedia( return res.status(200).json({
data.results.map((result) => result.id) page: data.page,
); totalPages: data.total_pages,
totalResults: data.total_results,
return res.status(200).json({ genre,
page: data.page, results: data.results.map((result) =>
totalPages: data.total_pages, mapTvResult(
totalResults: data.total_results,
results: data.results.map((result) =>
isMovie(result)
? mapMovieResult(
result, result,
media.find( media.find(
(med) => (med) =>
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE med.tmdbId === result.id && med.mediaType === MediaType.TV
) )
) )
: isPerson(result) ),
? mapPersonResult(result) });
: mapTvResult( } catch (e) {
logger.debug('Something went wrong retrieving series by genre', {
label: 'API',
errorMessage: e.message,
genreId: req.params.genreId,
});
return next({
status: 500,
message: 'Unable to retrieve series by genre.',
});
}
}
);
discoverRoutes.get<{ networkId: string }>(
'/tv/network/:networkId',
async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const network = await tmdb.getNetwork(Number(req.params.networkId));
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
network: Number(req.params.networkId),
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
network: mapNetwork(network),
results: data.results.map((result) =>
mapTvResult(
result, result,
media.find( media.find(
(med) => (med) =>
med.tmdbId === result.id && med.mediaType === MediaType.TV med.tmdbId === result.id && med.mediaType === MediaType.TV
) )
) )
), ),
}); });
}); } catch (e) {
logger.debug('Something went wrong retrieving series by network', {
label: 'API',
errorMessage: e.message,
networkId: req.params.networkId,
});
return next({
status: 500,
message: 'Unable to retrieve series by network.',
});
}
}
);
discoverRoutes.get<{ keywordId: string }>( discoverRoutes.get('/tv/upcoming', async (req, res, next) => {
'/keyword/:keywordId/movies', const tmdb = createTmdbWithRegionLanguage(req.user);
async (req, res) => {
const tmdb = new TheMovieDb();
const data = await tmdb.getMoviesByKeyword({ const now = new Date();
keywordId: Number(req.params.keywordId), const offset = now.getTimezoneOffset();
const date = new Date(now.getTime() - offset * 60 * 1000)
.toISOString()
.split('T')[0];
try {
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page), page: Number(req.query.page),
language: req.locale ?? (req.query.language as string), language: req.locale ?? (req.query.language as string),
firstAirDateGte: date,
}); });
const media = await Media.getRelatedMedia( const media = await Media.getRelatedMedia(
@ -473,15 +507,116 @@ discoverRoutes.get<{ keywordId: string }>(
totalPages: data.total_pages, totalPages: data.total_pages,
totalResults: data.total_results, totalResults: data.total_results,
results: data.results.map((result) => results: data.results.map((result) =>
mapMovieResult( mapTvResult(
result, result,
media.find( media.find(
(med) => (med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
) )
) )
), ),
}); });
} catch (e) {
logger.debug('Something went wrong retrieving upcoming series', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve upcoming series.',
});
}
});
discoverRoutes.get('/trending', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(req.user);
try {
const data = await tmdb.getAllTrending({
page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
isMovie(result)
? mapMovieResult(
result,
media.find(
(med) =>
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
)
)
: isPerson(result)
? mapPersonResult(result)
: mapTvResult(
result,
media.find(
(med) =>
med.tmdbId === result.id && med.mediaType === MediaType.TV
)
)
),
});
} catch (e) {
logger.debug('Something went wrong retrieving trending items', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve trending items.',
});
}
});
discoverRoutes.get<{ keywordId: string }>(
'/keyword/:keywordId/movies',
async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const data = await tmdb.getMoviesByKeyword({
keywordId: Number(req.params.keywordId),
page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
mapMovieResult(
result,
media.find(
(med) =>
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
)
)
),
});
} catch (e) {
logger.debug('Something went wrong retrieving movies by keyword', {
label: 'API',
errorMessage: e.message,
keywordId: req.params.keywordId,
});
return next({
status: 500,
message: 'Unable to retrieve movies by keyword.',
});
}
} }
); );
@ -515,7 +650,8 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
return res.status(200).json(sortedData); return res.status(200).json(sortedData);
} catch (e) { } catch (e) {
logger.error('Something went wrong retrieving the movie genre slider', { logger.debug('Something went wrong retrieving the movie genre slider', {
label: 'API',
errorMessage: e.message, errorMessage: e.message,
}); });
return next({ return next({
@ -556,12 +692,13 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
return res.status(200).json(sortedData); return res.status(200).json(sortedData);
} catch (e) { } catch (e) {
logger.error('Something went wrong retrieving the tv genre slider', { logger.debug('Something went wrong retrieving the series genre slider', {
label: 'API',
errorMessage: e.message, errorMessage: e.message,
}); });
return next({ return next({
status: 500, status: 500,
message: 'Unable to retrieve tv genre slider.', message: 'Unable to retrieve series genre slider.',
}); });
} }
} }

@ -5,6 +5,7 @@ import { TmdbMovieResult, TmdbTvResult } from '../api/themoviedb/interfaces';
import { StatusResponse } from '../interfaces/api/settingsInterfaces'; import { StatusResponse } from '../interfaces/api/settingsInterfaces';
import { Permission } from '../lib/permissions'; import { Permission } from '../lib/permissions';
import { getSettings } from '../lib/settings'; import { getSettings } from '../lib/settings';
import logger from '../logger';
import { checkUser, isAuthenticated } from '../middleware/auth'; import { checkUser, isAuthenticated } from '../middleware/auth';
import { mapProductionCompany } from '../models/Movie'; import { mapProductionCompany } from '../models/Movie';
import { mapNetwork } from '../models/Tv'; import { mapNetwork } from '../models/Tv';
@ -114,78 +115,157 @@ router.use('/issue', isAuthenticated(), issueRoutes);
router.use('/issueComment', isAuthenticated(), issueCommentRoutes); router.use('/issueComment', isAuthenticated(), issueCommentRoutes);
router.use('/auth', authRoutes); router.use('/auth', authRoutes);
router.get('/regions', isAuthenticated(), async (req, res) => { router.get('/regions', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const regions = await tmdb.getRegions(); try {
const regions = await tmdb.getRegions();
return res.status(200).json(regions);
return res.status(200).json(regions);
} catch (e) {
logger.debug('Something went wrong retrieving regions', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve regions.',
});
}
}); });
router.get('/languages', isAuthenticated(), async (req, res) => { router.get('/languages', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const languages = await tmdb.getLanguages(); try {
const languages = await tmdb.getLanguages();
return res.status(200).json(languages);
return res.status(200).json(languages);
} catch (e) {
logger.debug('Something went wrong retrieving languages', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve languages.',
});
}
}); });
router.get<{ id: string }>('/studio/:id', async (req, res) => { router.get<{ id: string }>('/studio/:id', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const studio = await tmdb.getStudio(Number(req.params.id)); try {
const studio = await tmdb.getStudio(Number(req.params.id));
return res.status(200).json(mapProductionCompany(studio));
return res.status(200).json(mapProductionCompany(studio));
} catch (e) {
logger.debug('Something went wrong retrieving studio', {
label: 'API',
errorMessage: e.message,
studioId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve studio.',
});
}
}); });
router.get<{ id: string }>('/network/:id', async (req, res) => { router.get<{ id: string }>('/network/:id', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const network = await tmdb.getNetwork(Number(req.params.id)); try {
const network = await tmdb.getNetwork(Number(req.params.id));
return res.status(200).json(mapNetwork(network));
return res.status(200).json(mapNetwork(network));
} catch (e) {
logger.debug('Something went wrong retrieving network', {
label: 'API',
errorMessage: e.message,
networkId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve network.',
});
}
}); });
router.get('/genres/movie', isAuthenticated(), async (req, res) => { router.get('/genres/movie', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const genres = await tmdb.getMovieGenres({ try {
language: req.locale ?? (req.query.language as string), const genres = await tmdb.getMovieGenres({
}); language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);
return res.status(200).json(genres);
} catch (e) {
logger.debug('Something went wrong retrieving movie genres', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve movie genres.',
});
}
}); });
router.get('/genres/tv', isAuthenticated(), async (req, res) => { router.get('/genres/tv', isAuthenticated(), async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const genres = await tmdb.getTvGenres({ try {
language: req.locale ?? (req.query.language as string), const genres = await tmdb.getTvGenres({
}); language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);
return res.status(200).json(genres);
} catch (e) {
logger.debug('Something went wrong retrieving series genres', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve series genres.',
});
}
}); });
router.get('/backdrops', async (req, res) => { router.get('/backdrops', async (req, res, next) => {
const tmdb = createTmdbWithRegionLanguage(); const tmdb = createTmdbWithRegionLanguage();
const data = ( try {
await tmdb.getAllTrending({ const data = (
page: 1, await tmdb.getAllTrending({
timeWindow: 'week', page: 1,
}) timeWindow: 'week',
).results.filter((result) => !isPerson(result)) as ( })
| TmdbMovieResult ).results.filter((result) => !isPerson(result)) as (
| TmdbTvResult | TmdbMovieResult
)[]; | TmdbTvResult
)[];
return res
.status(200) return res
.json( .status(200)
data .json(
.map((result) => result.backdrop_path) data
.filter((backdropPath) => !!backdropPath) .map((result) => result.backdrop_path)
); .filter((backdropPath) => !!backdropPath)
);
} catch (e) {
logger.debug('Something went wrong retrieving backdrops', {
label: 'API',
errorMessage: e.message,
});
return next({
status: 500,
message: 'Unable to retrieve backdrops.',
});
}
}); });
router.get('/', (_req, res) => { router.get('/', (_req, res) => {

@ -22,75 +22,105 @@ movieRoutes.get('/:id', async (req, res, next) => {
return res.status(200).json(mapMovieDetails(tmdbMovie, media)); return res.status(200).json(mapMovieDetails(tmdbMovie, media));
} catch (e) { } catch (e) {
logger.error('Something went wrong getting movie', { logger.debug('Something went wrong retrieving movie', {
label: 'Movie', label: 'API',
message: e.message, errorMessage: e.message,
movieId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve movie.',
}); });
return next({ status: 404, message: 'Movie does not exist' });
} }
}); });
movieRoutes.get('/:id/recommendations', async (req, res) => { movieRoutes.get('/:id/recommendations', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const results = await tmdb.getMovieRecommendations({ try {
movieId: Number(req.params.id), const results = await tmdb.getMovieRecommendations({
page: Number(req.query.page), movieId: Number(req.params.id),
language: req.locale ?? (req.query.language as string), page: Number(req.query.page),
}); language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id) const media = await Media.getRelatedMedia(
); results.results.map((result) => result.id)
);
return res.status(200).json({
page: results.page, return res.status(200).json({
totalPages: results.total_pages, page: results.page,
totalResults: results.total_results, totalPages: results.total_pages,
results: results.results.map((result) => totalResults: results.total_results,
mapMovieResult( results: results.results.map((result) =>
result, mapMovieResult(
media.find( result,
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE media.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
) )
) ),
), });
}); } catch (e) {
logger.debug('Something went wrong retrieving movie recommendations', {
label: 'API',
errorMessage: e.message,
movieId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve movie recommendations.',
});
}
}); });
movieRoutes.get('/:id/similar', async (req, res) => { movieRoutes.get('/:id/similar', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const results = await tmdb.getMovieSimilar({ try {
movieId: Number(req.params.id), const results = await tmdb.getMovieSimilar({
page: Number(req.query.page), movieId: Number(req.params.id),
language: req.locale ?? (req.query.language as string), page: Number(req.query.page),
}); language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id) const media = await Media.getRelatedMedia(
); results.results.map((result) => result.id)
);
return res.status(200).json({
page: results.page, return res.status(200).json({
totalPages: results.total_pages, page: results.page,
totalResults: results.total_results, totalPages: results.total_pages,
results: results.results.map((result) => totalResults: results.total_results,
mapMovieResult( results: results.results.map((result) =>
result, mapMovieResult(
media.find( result,
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE media.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
) )
) ),
), });
}); } catch (e) {
logger.debug('Something went wrong retrieving similar movies', {
label: 'API',
errorMessage: e.message,
movieId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve similar movies.',
});
}
}); });
movieRoutes.get('/:id/ratings', async (req, res, next) => { movieRoutes.get('/:id/ratings', async (req, res, next) => {
try { const tmdb = new TheMovieDb();
const tmdb = new TheMovieDb(); const rtapi = new RottenTomatoes();
const rtapi = new RottenTomatoes();
try {
const movie = await tmdb.getMovie({ const movie = await tmdb.getMovie({
movieId: Number(req.params.id), movieId: Number(req.params.id),
}); });
@ -101,12 +131,23 @@ movieRoutes.get('/:id/ratings', async (req, res, next) => {
); );
if (!rtratings) { if (!rtratings) {
return next({ status: 404, message: 'Unable to retrieve ratings' }); return next({
status: 404,
message: 'Rotten Tomatoes ratings not found.',
});
} }
return res.status(200).json(rtratings); return res.status(200).json(rtratings);
} catch (e) { } catch (e) {
return next({ status: 404, message: 'Movie does not exist' }); logger.debug('Something went wrong retrieving movie ratings', {
label: 'API',
errorMessage: e.message,
movieId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve movie ratings.',
});
} }
}); });

@ -20,52 +20,71 @@ personRoutes.get('/:id', async (req, res, next) => {
}); });
return res.status(200).json(mapPersonDetails(person)); return res.status(200).json(mapPersonDetails(person));
} catch (e) { } catch (e) {
logger.error(e.message); logger.debug('Something went wrong retrieving person', {
next({ status: 404, message: 'Person not found' }); label: 'API',
errorMessage: e.message,
personId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve person.',
});
} }
}); });
personRoutes.get('/:id/combined_credits', async (req, res) => { personRoutes.get('/:id/combined_credits', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const combinedCredits = await tmdb.getPersonCombinedCredits({ try {
personId: Number(req.params.id), const combinedCredits = await tmdb.getPersonCombinedCredits({
language: req.locale ?? (req.query.language as string), personId: Number(req.params.id),
}); language: req.locale ?? (req.query.language as string),
});
const castMedia = await Media.getRelatedMedia( const castMedia = await Media.getRelatedMedia(
combinedCredits.cast.map((result) => result.id) combinedCredits.cast.map((result) => result.id)
); );
const crewMedia = await Media.getRelatedMedia( const crewMedia = await Media.getRelatedMedia(
combinedCredits.crew.map((result) => result.id) combinedCredits.crew.map((result) => result.id)
); );
return res.status(200).json({ return res.status(200).json({
cast: combinedCredits.cast cast: combinedCredits.cast
.map((result) => .map((result) =>
mapCastCredits( mapCastCredits(
result, result,
castMedia.find( castMedia.find(
(med) => (med) =>
med.tmdbId === result.id && med.mediaType === result.media_type med.tmdbId === result.id && med.mediaType === result.media_type
)
) )
) )
) .filter((item) => !item.adult),
.filter((item) => !item.adult), crew: combinedCredits.crew
crew: combinedCredits.crew .map((result) =>
.map((result) => mapCrewCredits(
mapCrewCredits( result,
result, crewMedia.find(
crewMedia.find( (med) =>
(med) => med.tmdbId === result.id && med.mediaType === result.media_type
med.tmdbId === result.id && med.mediaType === result.media_type )
) )
) )
) .filter((item) => !item.adult),
.filter((item) => !item.adult), id: combinedCredits.id,
id: combinedCredits.id, });
}); } catch (e) {
logger.debug('Something went wrong retrieving combined credits', {
label: 'API',
errorMessage: e.message,
personId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve combined credits.',
});
}
}); });
export default personRoutes; export default personRoutes;

@ -3,43 +3,56 @@ import TheMovieDb from '../api/themoviedb';
import { TmdbSearchMultiResponse } from '../api/themoviedb/interfaces'; import { TmdbSearchMultiResponse } from '../api/themoviedb/interfaces';
import Media from '../entity/Media'; import Media from '../entity/Media';
import { findSearchProvider } from '../lib/search'; import { findSearchProvider } from '../lib/search';
import logger from '../logger';
import { mapSearchResults } from '../models/Search'; import { mapSearchResults } from '../models/Search';
const searchRoutes = Router(); const searchRoutes = Router();
searchRoutes.get('/', async (req, res) => { searchRoutes.get('/', async (req, res, next) => {
const queryString = req.query.query as string; const queryString = req.query.query as string;
const searchProvider = findSearchProvider(queryString.toLowerCase()); const searchProvider = findSearchProvider(queryString.toLowerCase());
let results: TmdbSearchMultiResponse; let results: TmdbSearchMultiResponse;
if (searchProvider) { try {
const [id] = queryString if (searchProvider) {
.toLowerCase() const [id] = queryString
.match(searchProvider.pattern) as RegExpMatchArray; .toLowerCase()
results = await searchProvider.search( .match(searchProvider.pattern) as RegExpMatchArray;
id, results = await searchProvider.search(
req.locale ?? (req.query.language as string) id,
req.locale ?? (req.query.language as string)
);
} else {
const tmdb = new TheMovieDb();
results = await tmdb.searchMulti({
query: queryString,
page: Number(req.query.page),
language: req.locale ?? (req.query.language as string),
});
}
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id)
); );
} else {
const tmdb = new TheMovieDb();
results = await tmdb.searchMulti({ return res.status(200).json({
query: queryString, page: results.page,
page: Number(req.query.page), totalPages: results.total_pages,
language: req.locale ?? (req.query.language as string), totalResults: results.total_results,
results: mapSearchResults(results.results, media),
});
} catch (e) {
logger.debug('Something went wrong retrieving search results', {
label: 'API',
errorMessage: e.message,
query: req.query.query,
});
return next({
status: 500,
message: 'Unable to retrieve search results.',
}); });
} }
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id)
);
return res.status(200).json({
page: results.page,
totalPages: results.total_pages,
totalResults: results.total_results,
results: mapSearchResults(results.results, media),
});
}); });
export default searchRoutes; export default searchRoutes;

@ -243,52 +243,63 @@ settingsRoutes.post('/tautulli', async (req, res) => {
settingsRoutes.get( settingsRoutes.get(
'/plex/users', '/plex/users',
isAuthenticated(Permission.MANAGE_USERS), isAuthenticated(Permission.MANAGE_USERS),
async (req, res) => { async (req, res, next) => {
const userRepository = getRepository(User); const userRepository = getRepository(User);
const qb = userRepository.createQueryBuilder('user'); const qb = userRepository.createQueryBuilder('user');
const admin = await userRepository.findOneOrFail({ try {
select: ['id', 'plexToken'], const admin = await userRepository.findOneOrFail({
order: { id: 'ASC' }, select: ['id', 'plexToken'],
}); order: { id: 'ASC' },
const plexApi = new PlexTvAPI(admin.plexToken ?? ''); });
const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map( const plexApi = new PlexTvAPI(admin.plexToken ?? '');
(user) => user.$ const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map(
).filter((user) => user.email); (user) => user.$
).filter((user) => user.email);
const unimportedPlexUsers: {
id: string; const unimportedPlexUsers: {
title: string; id: string;
username: string; title: string;
email: string; username: string;
thumb: string; email: string;
}[] = []; thumb: string;
}[] = [];
const existingUsers = await qb
.where('user.plexId IN (:...plexIds)', { const existingUsers = await qb
plexIds: plexUsers.map((plexUser) => plexUser.id), .where('user.plexId IN (:...plexIds)', {
}) plexIds: plexUsers.map((plexUser) => plexUser.id),
.orWhere('user.email IN (:...plexEmails)', { })
plexEmails: plexUsers.map((plexUser) => plexUser.email.toLowerCase()), .orWhere('user.email IN (:...plexEmails)', {
}) plexEmails: plexUsers.map((plexUser) => plexUser.email.toLowerCase()),
.getMany(); })
.getMany();
await Promise.all(
plexUsers.map(async (plexUser) => { await Promise.all(
if ( plexUsers.map(async (plexUser) => {
!existingUsers.find( if (
(user) => !existingUsers.find(
user.plexId === parseInt(plexUser.id) || (user) =>
user.email === plexUser.email.toLowerCase() user.plexId === parseInt(plexUser.id) ||
) && user.email === plexUser.email.toLowerCase()
(await plexApi.checkUserAccess(parseInt(plexUser.id))) ) &&
) { (await plexApi.checkUserAccess(parseInt(plexUser.id)))
unimportedPlexUsers.push(plexUser); ) {
} unimportedPlexUsers.push(plexUser);
}) }
); })
);
return res.status(200).json(sortBy(unimportedPlexUsers, 'username')); return res.status(200).json(sortBy(unimportedPlexUsers, 'username'));
} catch (e) {
logger.error('Something went wrong getting unimported Plex users', {
label: 'API',
errorMessage: e.message,
});
next({
status: 500,
message: 'Unable to retrieve unimported Plex users.',
});
}
} }
); );

@ -21,104 +21,156 @@ tvRoutes.get('/:id', async (req, res, next) => {
return res.status(200).json(mapTvDetails(tv, media)); return res.status(200).json(mapTvDetails(tv, media));
} catch (e) { } catch (e) {
logger.error('Failed to get tv show', { logger.debug('Something went wrong retrieving series', {
label: 'API', label: 'API',
errorMessage: e.message, errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve series.',
}); });
return next({ status: 404, message: 'TV Show does not exist' });
} }
}); });
tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => { tvRoutes.get('/:id/season/:seasonNumber', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const season = await tmdb.getTvSeason({ try {
tvId: Number(req.params.id), const season = await tmdb.getTvSeason({
seasonNumber: Number(req.params.seasonNumber), tvId: Number(req.params.id),
language: req.locale ?? (req.query.language as string), seasonNumber: Number(req.params.seasonNumber),
}); language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(mapSeasonWithEpisodes(season)); return res.status(200).json(mapSeasonWithEpisodes(season));
} catch (e) {
logger.debug('Something went wrong retrieving season', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
seasonNumber: req.params.seasonNumber,
});
return next({
status: 500,
message: 'Unable to retrieve season.',
});
}
}); });
tvRoutes.get('/:id/recommendations', async (req, res) => { tvRoutes.get('/:id/recommendations', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const results = await tmdb.getTvRecommendations({ try {
tvId: Number(req.params.id), const results = await tmdb.getTvRecommendations({
page: Number(req.query.page), tvId: Number(req.params.id),
language: req.locale ?? (req.query.language as string), page: Number(req.query.page),
}); language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id) const media = await Media.getRelatedMedia(
); results.results.map((result) => result.id)
);
return res.status(200).json({
page: results.page, return res.status(200).json({
totalPages: results.total_pages, page: results.page,
totalResults: results.total_results, totalPages: results.total_pages,
results: results.results.map((result) => totalResults: results.total_results,
mapTvResult( results: results.results.map((result) =>
result, mapTvResult(
media.find( result,
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV
)
) )
) ),
), });
}); } catch (e) {
logger.debug('Something went wrong retrieving series recommendations', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve series recommendations.',
});
}
}); });
tvRoutes.get('/:id/similar', async (req, res) => { tvRoutes.get('/:id/similar', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const results = await tmdb.getTvSimilar({ try {
tvId: Number(req.params.id), const results = await tmdb.getTvSimilar({
page: Number(req.query.page), tvId: Number(req.params.id),
language: req.locale ?? (req.query.language as string), page: Number(req.query.page),
}); language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
results.results.map((result) => result.id) const media = await Media.getRelatedMedia(
); results.results.map((result) => result.id)
);
return res.status(200).json({
page: results.page, return res.status(200).json({
totalPages: results.total_pages, page: results.page,
totalResults: results.total_results, totalPages: results.total_pages,
results: results.results.map((result) => totalResults: results.total_results,
mapTvResult( results: results.results.map((result) =>
result, mapTvResult(
media.find( result,
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV
)
) )
) ),
), });
}); } catch (e) {
logger.debug('Something went wrong retrieving similar series', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve similar series.',
});
}
}); });
tvRoutes.get('/:id/ratings', async (req, res, next) => { tvRoutes.get('/:id/ratings', async (req, res, next) => {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const rtapi = new RottenTomatoes(); const rtapi = new RottenTomatoes();
const tv = await tmdb.getTvShow({ try {
tvId: Number(req.params.id), const tv = await tmdb.getTvShow({
}); tvId: Number(req.params.id),
});
if (!tv) { const rtratings = await rtapi.getTVRatings(
return next({ status: 404, message: 'TV Show does not exist' }); tv.name,
} tv.first_air_date ? Number(tv.first_air_date.slice(0, 4)) : undefined
);
const rtratings = await rtapi.getTVRatings( if (!rtratings) {
tv.name, return next({
tv.first_air_date ? Number(tv.first_air_date.slice(0, 4)) : undefined status: 404,
); message: 'Rotten Tomatoes ratings not found.',
});
}
if (!rtratings) { return res.status(200).json(rtratings);
return next({ status: 404, message: 'Unable to retrieve ratings' }); } catch (e) {
logger.debug('Something went wrong retrieving series ratings', {
label: 'API',
errorMessage: e.message,
tvId: req.params.id,
});
return next({
status: 500,
message: 'Unable to retrieve series ratings.',
});
} }
return res.status(200).json(rtratings);
}); });
export default tvRoutes; export default tvRoutes;

@ -12,6 +12,7 @@ import IssueComment from '../entity/IssueComment';
import Media from '../entity/Media'; import Media from '../entity/Media';
import notificationManager, { Notification } from '../lib/notifications'; import notificationManager, { Notification } from '../lib/notifications';
import { Permission } from '../lib/permissions'; import { Permission } from '../lib/permissions';
import logger from '../logger';
@EventSubscriber() @EventSubscriber()
export class IssueCommentSubscriber export class IssueCommentSubscriber
@ -26,62 +27,67 @@ export class IssueCommentSubscriber
let image: string; let image: string;
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const issue = ( try {
await getRepository(IssueComment).findOne({ const issue = (
where: { id: entity.id }, await getRepository(IssueComment).findOneOrFail({
relations: ['issue'], where: { id: entity.id },
}) relations: ['issue'],
)?.issue; })
if (!issue) { ).issue;
return;
}
const media = await getRepository(Media).findOne({ const media = await getRepository(Media).findOneOrFail({
where: { id: issue.media.id }, where: { id: issue.media.id },
}); });
if (!media) {
return;
}
if (media.mediaType === MediaType.MOVIE) { if (media.mediaType === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: media.tmdbId }); const movie = await tmdb.getMovie({ movieId: media.tmdbId });
title = `${movie.title}${ title = `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`; }`;
image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`;
} else { } else {
const tvshow = await tmdb.getTvShow({ tvId: media.tmdbId }); const tvshow = await tmdb.getTvShow({ tvId: media.tmdbId });
title = `${tvshow.name}${ title = `${tvshow.name}${
tvshow.first_air_date ? ` (${tvshow.first_air_date.slice(0, 4)})` : '' tvshow.first_air_date ? ` (${tvshow.first_air_date.slice(0, 4)})` : ''
}`; }`;
image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tvshow.poster_path}`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tvshow.poster_path}`;
} }
const [firstComment] = sortBy(issue.comments, 'id'); const [firstComment] = sortBy(issue.comments, 'id');
if (entity.id !== firstComment.id) { if (entity.id !== firstComment.id) {
// Send notifications to all issue managers // Send notifications to all issue managers
notificationManager.sendNotification(Notification.ISSUE_COMMENT, { notificationManager.sendNotification(Notification.ISSUE_COMMENT, {
event: `New Comment on ${ event: `New Comment on ${
issue.issueType !== IssueType.OTHER issue.issueType !== IssueType.OTHER
? `${IssueTypeName[issue.issueType]} ` ? `${IssueTypeName[issue.issueType]} `
: '' : ''
}Issue`, }Issue`,
subject: title, subject: title,
message: firstComment.message, message: firstComment.message,
comment: entity, comment: entity,
issue, issue,
media, media,
image, image,
notifyAdmin: true, notifyAdmin: true,
notifyUser: notifyUser:
!issue.createdBy.hasPermission(Permission.MANAGE_ISSUES) && !issue.createdBy.hasPermission(Permission.MANAGE_ISSUES) &&
issue.createdBy.id !== entity.user.id issue.createdBy.id !== entity.user.id
? issue.createdBy ? issue.createdBy
: undefined, : undefined,
}); });
}
} catch (e) {
logger.error(
'Something went wrong sending issue comment notification(s)',
{
label: 'Notifications',
errorMessage: e.message,
commentId: entity.id,
}
);
} }
} }

@ -11,6 +11,7 @@ import { MediaType } from '../constants/media';
import Issue from '../entity/Issue'; import Issue from '../entity/Issue';
import notificationManager, { Notification } from '../lib/notifications'; import notificationManager, { Notification } from '../lib/notifications';
import { Permission } from '../lib/permissions'; import { Permission } from '../lib/permissions';
import logger from '../logger';
@EventSubscriber() @EventSubscriber()
export class IssueSubscriber implements EntitySubscriberInterface<Issue> { export class IssueSubscriber implements EntitySubscriberInterface<Issue> {
@ -22,72 +23,81 @@ export class IssueSubscriber implements EntitySubscriberInterface<Issue> {
let title: string; let title: string;
let image: string; let image: string;
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
if (entity.media.mediaType === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: entity.media.tmdbId });
title = `${movie.title}${ try {
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' if (entity.media.mediaType === MediaType.MOVIE) {
}`; const movie = await tmdb.getMovie({ movieId: entity.media.tmdbId });
image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`;
} else {
const tvshow = await tmdb.getTvShow({ tvId: entity.media.tmdbId });
title = `${tvshow.name}${ title = `${movie.title}${
tvshow.first_air_date ? ` (${tvshow.first_air_date.slice(0, 4)})` : '' movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`; }`;
image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tvshow.poster_path}`; image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`;
} } else {
const tvshow = await tmdb.getTvShow({ tvId: entity.media.tmdbId });
const [firstComment] = sortBy(entity.comments, 'id'); title = `${tvshow.name}${
const extra: { name: string; value: string }[] = []; tvshow.first_air_date ? ` (${tvshow.first_air_date.slice(0, 4)})` : ''
}`;
image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tvshow.poster_path}`;
}
if (entity.media.mediaType === MediaType.TV && entity.problemSeason > 0) { const [firstComment] = sortBy(entity.comments, 'id');
extra.push({ const extra: { name: string; value: string }[] = [];
name: 'Affected Season',
value: entity.problemSeason.toString(),
});
if (entity.problemEpisode > 0) { if (entity.media.mediaType === MediaType.TV && entity.problemSeason > 0) {
extra.push({ extra.push({
name: 'Affected Episode', name: 'Affected Season',
value: entity.problemEpisode.toString(), value: entity.problemSeason.toString(),
}); });
if (entity.problemEpisode > 0) {
extra.push({
name: 'Affected Episode',
value: entity.problemEpisode.toString(),
});
}
} }
}
notificationManager.sendNotification(type, { notificationManager.sendNotification(type, {
event: event:
type === Notification.ISSUE_CREATED type === Notification.ISSUE_CREATED
? `New ${ ? `New ${
entity.issueType !== IssueType.OTHER entity.issueType !== IssueType.OTHER
? `${IssueTypeName[entity.issueType]} ` ? `${IssueTypeName[entity.issueType]} `
: '' : ''
}Issue Reported` }Issue Reported`
: type === Notification.ISSUE_RESOLVED : type === Notification.ISSUE_RESOLVED
? `${ ? `${
entity.issueType !== IssueType.OTHER entity.issueType !== IssueType.OTHER
? `${IssueTypeName[entity.issueType]} ` ? `${IssueTypeName[entity.issueType]} `
: '' : ''
}Issue Resolved` }Issue Resolved`
: `${ : `${
entity.issueType !== IssueType.OTHER entity.issueType !== IssueType.OTHER
? `${IssueTypeName[entity.issueType]} ` ? `${IssueTypeName[entity.issueType]} `
: '' : ''
}Issue Reopened`, }Issue Reopened`,
subject: title, subject: title,
message: firstComment.message, message: firstComment.message,
issue: entity, issue: entity,
media: entity.media, media: entity.media,
image, image,
extra, extra,
notifyAdmin: true, notifyAdmin: true,
notifyUser: notifyUser:
!entity.createdBy.hasPermission(Permission.MANAGE_ISSUES) && !entity.createdBy.hasPermission(Permission.MANAGE_ISSUES) &&
(type === Notification.ISSUE_RESOLVED || (type === Notification.ISSUE_RESOLVED ||
type === Notification.ISSUE_REOPENED) type === Notification.ISSUE_REOPENED)
? entity.createdBy ? entity.createdBy
: undefined, : undefined,
}); });
} catch (e) {
logger.error('Something went wrong sending issue notification(s)', {
label: 'Notifications',
errorMessage: e.message,
issueId: entity.id,
});
}
} }
public afterInsert(event: InsertEvent<Issue>): void { public afterInsert(event: InsertEvent<Issue>): void {

@ -12,6 +12,7 @@ import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest'; import { MediaRequest } from '../entity/MediaRequest';
import Season from '../entity/Season'; import Season from '../entity/Season';
import notificationManager, { Notification } from '../lib/notifications'; import notificationManager, { Notification } from '../lib/notifications';
import logger from '../logger';
@EventSubscriber() @EventSubscriber()
export class MediaSubscriber implements EntitySubscriberInterface<Media> { export class MediaSubscriber implements EntitySubscriberInterface<Media> {
@ -36,26 +37,40 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
if (relatedRequests.length > 0) { if (relatedRequests.length > 0) {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
relatedRequests.forEach((request) => { try {
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
event: `${is4k ? '4K ' : ''}Movie Request Now Available`,
notifyAdmin: false, relatedRequests.forEach((request) => {
notifyUser: request.requestedBy, notificationManager.sendNotification(
subject: `${movie.title}${ Notification.MEDIA_AVAILABLE,
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' {
}`, event: `${is4k ? '4K ' : ''}Movie Request Now Available`,
message: truncate(movie.overview, { notifyAdmin: false,
length: 500, notifyUser: request.requestedBy,
separator: /\s/, subject: `${movie.title}${
omission: '…', movie.release_date
}), ? ` (${movie.release_date.slice(0, 4)})`
media: entity, : ''
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, }`,
request, message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media: entity,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request,
}
);
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
}); });
}); }
} }
} }
} }
@ -114,31 +129,40 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
processedSeasons.push( processedSeasons.push(
...request.seasons.map((season) => season.seasonNumber) ...request.seasons.map((season) => season.seasonNumber)
); );
const tv = await tmdb.getTvShow({ tvId: entity.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { try {
event: `${is4k ? '4K ' : ''}Series Request Now Available`, const tv = await tmdb.getTvShow({ tvId: entity.tmdbId });
subject: `${tv.name}${ notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' event: `${is4k ? '4K ' : ''}Series Request Now Available`,
}`, subject: `${tv.name}${
message: truncate(tv.overview, { tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
length: 500, }`,
separator: /\s/, message: truncate(tv.overview, {
omission: '…', length: 500,
}), separator: /\s/,
notifyAdmin: false, omission: '…',
notifyUser: request.requestedBy, }),
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, notifyAdmin: false,
media: entity, notifyUser: request.requestedBy,
extra: [ image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
{ media: entity,
name: 'Requested Seasons', extra: [
value: request.seasons {
.map((season) => season.seasonNumber) name: 'Requested Seasons',
.join(', '), value: request.seasons
}, .map((season) => season.seasonNumber)
], .join(', '),
request, },
}); ],
request,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
}
} }
} }
} }

@ -11,7 +11,7 @@ confinement: strict
parts: parts:
overseerr: overseerr:
plugin: nodejs plugin: nodejs
nodejs-version: '14.18.1' nodejs-version: '16.13.1'
nodejs-package-manager: 'yarn' nodejs-package-manager: 'yarn'
nodejs-yarn-version: v1.22.10 nodejs-yarn-version: v1.22.10
build-packages: build-packages:

Loading…
Cancel
Save