diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec9692c4..30af5096 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: test: name: Lint & Test Build runs-on: ubuntu-20.04 - container: node:14.18-alpine + container: node:16.13-alpine steps: - name: Checkout uses: actions/checkout@v2.4.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8db8fee..52d31fad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: test: name: Lint & Test Build runs-on: ubuntu-20.04 - container: node:14.18-alpine + container: node:16.13-alpine steps: - name: Checkout uses: actions/checkout@v2.4.0 diff --git a/.github/workflows/snap.yaml b/.github/workflows/snap.yaml index 43d0ac50..cf3a933c 100644 --- a/.github/workflows/snap.yaml +++ b/.github/workflows/snap.yaml @@ -20,7 +20,7 @@ jobs: name: Lint & Test Build needs: jobs runs-on: ubuntu-20.04 - container: node:14.18-alpine + container: node:16.13-alpine steps: - name: Checkout uses: actions/checkout@v2.4.0 diff --git a/Dockerfile b/Dockerfile index a529a423..bd7e56bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.18-alpine AS BUILD_IMAGE +FROM node:16.13-alpine AS BUILD_IMAGE WORKDIR /app @@ -33,7 +33,7 @@ RUN touch config/DOCKER RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json -FROM node:14.18-alpine +FROM node:16.13-alpine WORKDIR /app diff --git a/Dockerfile.local b/Dockerfile.local index bb8536be..b83ea5bb 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,4 +1,4 @@ -FROM node:14.18-alpine +FROM node:16.13-alpine COPY . /app WORKDIR /app diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 0e0a41f1..33e1a7af 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -1,7 +1,7 @@ import logger from '../../logger'; import ServarrBase from './base'; -interface RadarrMovieOptions { +export interface RadarrMovieOptions { title: string; qualityProfileId: number; minimumAvailability: string; diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index b6793ed3..01d348a4 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -63,7 +63,7 @@ export interface SonarrSeries { }; } -interface AddSeriesOptions { +export interface AddSeriesOptions { tvdbid: number; title: string; profileId: number; diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 0e97f3d6..f7f82115 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -13,8 +13,11 @@ import { RelationCount, UpdateDateColumn, } from 'typeorm'; -import RadarrAPI from '../api/servarr/radarr'; -import SonarrAPI, { SonarrSeries } from '../api/servarr/sonarr'; +import RadarrAPI, { RadarrMovieOptions } from '../api/servarr/radarr'; +import SonarrAPI, { + AddSeriesOptions, + SonarrSeries, +} from '../api/servarr/sonarr'; import TheMovieDb from '../api/themoviedb'; import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants'; import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media'; @@ -135,55 +138,15 @@ export class MediaRequest { where: { id: this.media.id }, }); if (!media) { - logger.error('No parent media!', { label: 'Media Request' }); - return; - } - const tmdb = new TheMovieDb(); - 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, + logger.error('Media data not found', { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, }); + return; } - if (this.type === MediaType.TV) { - 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, - }); - } + this.sendNotification(media, Notification.MEDIA_PENDING); } } @@ -204,90 +167,30 @@ export class MediaRequest { where: { id: this.media.id }, }); 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; } if (media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE) { logger.warn( - 'Media became available before request was approved. Approval notification will be skipped.', - { label: 'Media Request' } + 'Media became available before request was approved. Skipping approval notification', + { label: 'Media Request', requestId: this.id, mediaId: this.media.id } ); return; } - const tmdb = new TheMovieDb(); - if (this.media.mediaType === MediaType.MOVIE) { - const movie = await tmdb.getMovie({ movieId: this.media.tmdbId }); - notificationManager.sendNotification( - this.status === MediaRequestStatus.APPROVED - ? autoApproved - ? Notification.MEDIA_AUTO_APPROVED - : 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, - } - ); - } + this.sendNotification( + media, + this.status === MediaRequestStatus.APPROVED + ? autoApproved + ? Notification.MEDIA_AUTO_APPROVED + : Notification.MEDIA_APPROVED + : Notification.MEDIA_DECLINED + ); } } @@ -307,7 +210,11 @@ export class MediaRequest { relations: ['requests'], }); 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; } const seasonRequestRepository = getRepository(SeasonRequest); @@ -395,8 +302,12 @@ export class MediaRequest { const settings = getSettings(); if (settings.radarr.length === 0 && !settings.radarr[0]) { logger.info( - 'Skipped Radarr request as there is no Radarr server configured', - { label: 'Media Request' } + 'No Radarr server configured, skipping request processing', + { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + } ); return; } @@ -415,18 +326,26 @@ export class MediaRequest { ); logger.info( `Request has an override server: ${radarrSettings?.name}`, - { label: 'Media Request' } + { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + } ); } if (!radarrSettings) { - logger.info( + 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' } + { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + } ); return; } @@ -443,6 +362,8 @@ export class MediaRequest { rootFolder = this.rootFolder; logger.info(`Request has an override root folder: ${rootFolder}`, { label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, }); } @@ -451,15 +372,22 @@ export class MediaRequest { this.profileId !== radarrSettings.activeProfileId ) { qualityProfile = this.profileId; - logger.info(`Request has an override profile id: ${qualityProfile}`, { - label: 'Media Request', - }); + 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, }); } @@ -476,7 +404,11 @@ export class MediaRequest { }); if (!media) { - logger.error('Media not present'); + logger.error('Media data not found', { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + }); return; } @@ -486,20 +418,22 @@ export class MediaRequest { 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 radarr - .addMovie({ - 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, - }) + .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({ @@ -507,7 +441,7 @@ export class MediaRequest { }); if (!media) { - throw new Error('Media data is missing'); + throw new Error('Media data not found'); } media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] = @@ -521,36 +455,30 @@ export class MediaRequest { media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN; await mediaRepository.save(media); 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', + requestId: this.id, + mediaId: this.media.id, + radarrMovieOptions, } ); - notificationManager.sendNotification(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, - }); + this.sendNotification(media, Notification.MEDIA_FAILED); }); - 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) { - const errorMessage = `Request failed to send to Radarr: ${e.message}`; - logger.error('Request failed to send to Radarr', { + logger.error('Something went wrong sending request to Radarr', { 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 settings = getSettings(); if (settings.sonarr.length === 0 && !settings.sonarr[0]) { - logger.info( - 'Skipped Sonarr request as there is no Sonarr server configured', - { label: 'Media Request' } + logger.warn( + 'No Sonarr server configured, skipping request processing', + { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + } ); return; } @@ -585,18 +517,26 @@ export class MediaRequest { ); logger.info( `Request has an override server: ${sonarrSettings?.name}`, - { label: 'Media Request' } + { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + } ); } if (!sonarrSettings) { - logger.info( + 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' } + { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + } ); return; } @@ -607,7 +547,7 @@ export class MediaRequest { }); if (!media) { - throw new Error('Media data is missing'); + throw new Error('Media data not found'); } if ( @@ -628,7 +568,7 @@ export class MediaRequest { const requestRepository = getRepository(MediaRequest); await mediaRepository.remove(media); await requestRepository.remove(this); - throw new Error('Series was missing tvdb id'); + throw new Error('TVDB ID not found'); } let seriesType: SonarrSeries['seriesType'] = 'standard'; @@ -650,12 +590,10 @@ export class MediaRequest { seriesType === 'anime' && sonarrSettings.activeAnimeProfileId ? sonarrSettings.activeAnimeProfileId : sonarrSettings.activeProfileId; - let languageProfile = seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId ? sonarrSettings.activeAnimeLanguageProfileId : sonarrSettings.activeLanguageProfileId; - let tags = seriesType === 'anime' ? sonarrSettings.animeTags @@ -669,14 +607,21 @@ export class MediaRequest { 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 profile ID: ${qualityProfile}`, { - label: 'Media Request', - }); + logger.info( + `Request has an override quality profile ID: ${qualityProfile}`, + { + label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, + } + ); } if ( @@ -685,9 +630,11 @@ export class MediaRequest { ) { languageProfile = this.languageProfileId; logger.info( - `Request has an override Language Profile: ${languageProfile}`, + `Request has an override language profile ID: ${languageProfile}`, { label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, } ); } @@ -696,25 +643,29 @@ export class MediaRequest { tags = this.tags; logger.info(`Request has override tags`, { label: 'Media Request', + requestId: this.id, + mediaId: this.media.id, 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 sonarr - .addSeries({ - 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, - }) + .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({ @@ -723,7 +674,7 @@ export class MediaRequest { }); if (!media) { - throw new Error('Media data is missing'); + throw new Error('Media data not found'); } media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] = @@ -737,47 +688,116 @@ export class MediaRequest { media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN; await mediaRepository.save(media); 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', + requestId: this.id, + mediaId: this.media.id, + sonarrSeriesOptions, } ); - notificationManager.sendNotification(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, - }); + this.sendNotification(media, Notification.MEDIA_FAILED); }); - 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) { - const errorMessage = `Request failed to send to Sonarr: ${e.message}`; - logger.error('Request failed to send to Sonarr', { + logger.error('Something went wrong sending request to Sonarr', { 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, + }); } } } diff --git a/server/routes/collection.ts b/server/routes/collection.ts index 8ffbb51c..aa894873 100644 --- a/server/routes/collection.ts +++ b/server/routes/collection.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import TheMovieDb from '../api/themoviedb'; import Media from '../entity/Media'; +import logger from '../logger'; import { mapCollection } from '../models/Collection'; const collectionRoutes = Router(); @@ -20,7 +21,15 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => { return res.status(200).json(mapCollection(collection, media)); } 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.', + }); } }); diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 16654e81..ea78bf03 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -37,54 +37,15 @@ export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => { const discoverRoutes = Router(); -discoverRoutes.get('/movies', async (req, res) => { +discoverRoutes.get('/movies', async (req, res, next) => { const tmdb = createTmdbWithRegionLanguage(req.user); - const data = await tmdb.getDiscoverMovies({ - 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' }); - } - + try { const data = await tmdb.getDiscoverMovies({ page: Number(req.query.page), 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( @@ -95,7 +56,6 @@ discoverRoutes.get<{ language: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - language, results: data.results.map((result) => mapMovieResult( 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) => { const tmdb = createTmdbWithRegionLanguage(req.user); - const genres = await tmdb.getMovieGenres({ - language: req.locale ?? (req.query.language as string), - }); + try { + const genres = await tmdb.getMovieGenres({ + language: req.locale ?? (req.query.language as string), + }); - const genre = genres.find( - (genre) => genre.id === Number(req.params.genreId) - ); + const genre = genres.find( + (genre) => genre.id === Number(req.params.genreId) + ); - if (!genre) { - return next({ status: 404, message: 'Unable to retrieve genre' }); - } + if (!genre) { + return next({ status: 404, message: 'Genre not found.' }); + } - const data = await tmdb.getDiscoverMovies({ - page: Number(req.query.page), - language: req.locale ?? (req.query.language as string), - genre: Number(req.params.genreId), - }); + const data = await tmdb.getDiscoverMovies({ + page: Number(req.query.page), + language: req.locale ?? (req.query.language as string), + genre: Number(req.params.genreId), + }); - const media = await Media.getRelatedMedia( - data.results.map((result) => result.id) - ); + 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, - genre, - results: data.results.map((result) => - mapMovieResult( - result, - media.find( - (req) => - req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + return res.status(200).json({ + page: data.page, + totalPages: data.total_pages, + totalResults: data.total_results, + genre, + 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 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) { - 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 now = new Date(); @@ -202,79 +246,11 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => { .toISOString() .split('T')[0]; - const data = await tmdb.getDiscoverMovies({ - page: Number(req.query.page), - 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({ + try { + const data = await tmdb.getDiscoverMovies({ page: Number(req.query.page), language: req.locale ?? (req.query.language as string), - originalLanguage: req.params.language, + primaryReleaseDateGte: date, }); const media = await Media.getRelatedMedia( @@ -285,40 +261,37 @@ discoverRoutes.get<{ language: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - language, results: data.results.map((result) => - mapTvResult( + mapMovieResult( result, media.find( - (med) => med.tmdbId === result.id && med.mediaType === MediaType.TV + (med) => + med.tmdbId === result.id && med.mediaType === MediaType.MOVIE ) ) ), }); - } -); - -discoverRoutes.get<{ genreId: string }>( - '/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), + } catch (e) { + logger.debug('Something went wrong retrieving upcoming movies', { + label: 'API', + errorMessage: e.message, }); + return next({ + status: 500, + message: 'Unable to retrieve upcoming movies.', + }); + } +}); - const genre = genres.find( - (genre) => genre.id === Number(req.params.genreId) - ); - - if (!genre) { - return next({ status: 404, message: 'Unable to retrieve genre' }); - } +discoverRoutes.get('/tv', async (req, res, next) => { + const tmdb = createTmdbWithRegionLanguage(req.user); + try { const data = await tmdb.getDiscoverTv({ page: Number(req.query.page), 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( @@ -329,7 +302,6 @@ discoverRoutes.get<{ genreId: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - genre, results: data.results.map((result) => mapTvResult( 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 }>( - '/tv/network/:networkId', +discoverRoutes.get<{ language: string }>( + '/tv/language/:language', async (req, res, next) => { - const tmdb = new TheMovieDb(); + const tmdb = createTmdbWithRegionLanguage(req.user); 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({ page: Number(req.query.page), language: req.locale ?? (req.query.language as string), - network: Number(req.params.networkId), + originalLanguage: req.params.language, }); const media = await Media.getRelatedMedia( @@ -364,7 +353,7 @@ discoverRoutes.get<{ networkId: string }>( page: data.page, totalPages: data.total_pages, totalResults: data.total_results, - network: mapNetwork(network), + language, results: data.results.map((result) => mapTvResult( result, @@ -376,92 +365,137 @@ discoverRoutes.get<{ networkId: string }>( ), }); } 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) => { - const tmdb = createTmdbWithRegionLanguage(req.user); +discoverRoutes.get<{ genreId: string }>( + '/tv/genre/:genreId', + async (req, res, next) => { + const tmdb = createTmdbWithRegionLanguage(req.user); - const now = new Date(); - const offset = now.getTimezoneOffset(); - const date = new Date(now.getTime() - offset * 60 * 1000) - .toISOString() - .split('T')[0]; + try { + const genres = await tmdb.getTvGenres({ + language: req.locale ?? (req.query.language as string), + }); - const data = await tmdb.getDiscoverTv({ - page: Number(req.query.page), - language: req.locale ?? (req.query.language as string), - firstAirDateGte: date, - }); + const genre = genres.find( + (genre) => genre.id === Number(req.params.genreId) + ); - 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 - ) - ) - ), - }); -}); + if (!genre) { + return next({ status: 404, message: 'Genre not found.' }); + } -discoverRoutes.get('/trending', 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: Number(req.params.genreId), + }); - 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) + ); - 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( + return res.status(200).json({ + page: data.page, + totalPages: data.total_pages, + totalResults: data.total_results, + genre, + results: data.results.map((result) => + mapTvResult( result, media.find( (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, media.find( (med) => 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 }>( - '/keyword/:keywordId/movies', - async (req, res) => { - const tmdb = new TheMovieDb(); +discoverRoutes.get('/tv/upcoming', async (req, res, next) => { + const tmdb = createTmdbWithRegionLanguage(req.user); - const data = await tmdb.getMoviesByKeyword({ - keywordId: Number(req.params.keywordId), + const now = new Date(); + 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), language: req.locale ?? (req.query.language as string), + firstAirDateGte: date, }); const media = await Media.getRelatedMedia( @@ -473,15 +507,116 @@ discoverRoutes.get<{ keywordId: string }>( totalPages: data.total_pages, totalResults: data.total_results, results: data.results.map((result) => - mapMovieResult( + mapTvResult( result, media.find( - (med) => - med.tmdbId === result.id && med.mediaType === MediaType.MOVIE + (med) => med.tmdbId === result.id && med.mediaType === MediaType.TV ) ) ), }); + } 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); } 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, }); return next({ @@ -556,12 +692,13 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>( return res.status(200).json(sortedData); } 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, }); return next({ status: 500, - message: 'Unable to retrieve tv genre slider.', + message: 'Unable to retrieve series genre slider.', }); } } diff --git a/server/routes/index.ts b/server/routes/index.ts index 3f57e815..e2866638 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -5,6 +5,7 @@ import { TmdbMovieResult, TmdbTvResult } from '../api/themoviedb/interfaces'; import { StatusResponse } from '../interfaces/api/settingsInterfaces'; import { Permission } from '../lib/permissions'; import { getSettings } from '../lib/settings'; +import logger from '../logger'; import { checkUser, isAuthenticated } from '../middleware/auth'; import { mapProductionCompany } from '../models/Movie'; import { mapNetwork } from '../models/Tv'; @@ -114,78 +115,157 @@ router.use('/issue', isAuthenticated(), issueRoutes); router.use('/issueComment', isAuthenticated(), issueCommentRoutes); router.use('/auth', authRoutes); -router.get('/regions', isAuthenticated(), async (req, res) => { +router.get('/regions', isAuthenticated(), async (req, res, next) => { const tmdb = new TheMovieDb(); - const regions = await tmdb.getRegions(); - - return res.status(200).json(regions); + try { + const regions = await tmdb.getRegions(); + + 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 languages = await tmdb.getLanguages(); - - return res.status(200).json(languages); + try { + const languages = await tmdb.getLanguages(); + + 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 studio = await tmdb.getStudio(Number(req.params.id)); - - return res.status(200).json(mapProductionCompany(studio)); + try { + const studio = await tmdb.getStudio(Number(req.params.id)); + + 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 network = await tmdb.getNetwork(Number(req.params.id)); - - return res.status(200).json(mapNetwork(network)); + try { + const network = await tmdb.getNetwork(Number(req.params.id)); + + 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 genres = await tmdb.getMovieGenres({ - language: req.locale ?? (req.query.language as string), - }); - - return res.status(200).json(genres); + try { + const genres = await tmdb.getMovieGenres({ + language: req.locale ?? (req.query.language as string), + }); + + 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 genres = await tmdb.getTvGenres({ - language: req.locale ?? (req.query.language as string), - }); - - return res.status(200).json(genres); + try { + const genres = await tmdb.getTvGenres({ + language: req.locale ?? (req.query.language as string), + }); + + 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 data = ( - await tmdb.getAllTrending({ - page: 1, - timeWindow: 'week', - }) - ).results.filter((result) => !isPerson(result)) as ( - | TmdbMovieResult - | TmdbTvResult - )[]; - - return res - .status(200) - .json( - data - .map((result) => result.backdrop_path) - .filter((backdropPath) => !!backdropPath) - ); + try { + const data = ( + await tmdb.getAllTrending({ + page: 1, + timeWindow: 'week', + }) + ).results.filter((result) => !isPerson(result)) as ( + | TmdbMovieResult + | TmdbTvResult + )[]; + + return res + .status(200) + .json( + data + .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) => { diff --git a/server/routes/movie.ts b/server/routes/movie.ts index d871652a..98474c78 100644 --- a/server/routes/movie.ts +++ b/server/routes/movie.ts @@ -22,75 +22,105 @@ movieRoutes.get('/:id', async (req, res, next) => { return res.status(200).json(mapMovieDetails(tmdbMovie, media)); } catch (e) { - logger.error('Something went wrong getting movie', { - label: 'Movie', - message: e.message, + logger.debug('Something went wrong retrieving movie', { + label: 'API', + 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 results = await tmdb.getMovieRecommendations({ - movieId: Number(req.params.id), - page: Number(req.query.page), - language: req.locale ?? (req.query.language as string), - }); - - 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: results.results.map((result) => - mapMovieResult( - result, - media.find( - (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + try { + const results = await tmdb.getMovieRecommendations({ + movieId: Number(req.params.id), + page: Number(req.query.page), + language: req.locale ?? (req.query.language as string), + }); + + 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: results.results.map((result) => + mapMovieResult( + result, + 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 results = await tmdb.getMovieSimilar({ - movieId: Number(req.params.id), - page: Number(req.query.page), - language: req.locale ?? (req.query.language as string), - }); - - 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: results.results.map((result) => - mapMovieResult( - result, - media.find( - (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + try { + const results = await tmdb.getMovieSimilar({ + movieId: Number(req.params.id), + page: Number(req.query.page), + language: req.locale ?? (req.query.language as string), + }); + + 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: results.results.map((result) => + mapMovieResult( + result, + 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) => { - try { - const tmdb = new TheMovieDb(); - const rtapi = new RottenTomatoes(); + const tmdb = new TheMovieDb(); + const rtapi = new RottenTomatoes(); + try { const movie = await tmdb.getMovie({ movieId: Number(req.params.id), }); @@ -101,12 +131,23 @@ movieRoutes.get('/:id/ratings', async (req, res, next) => { ); 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); } 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.', + }); } }); diff --git a/server/routes/person.ts b/server/routes/person.ts index e18e55c8..5093ae46 100644 --- a/server/routes/person.ts +++ b/server/routes/person.ts @@ -20,52 +20,71 @@ personRoutes.get('/:id', async (req, res, next) => { }); return res.status(200).json(mapPersonDetails(person)); } catch (e) { - logger.error(e.message); - next({ status: 404, message: 'Person not found' }); + logger.debug('Something went wrong retrieving person', { + 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 combinedCredits = await tmdb.getPersonCombinedCredits({ - personId: Number(req.params.id), - language: req.locale ?? (req.query.language as string), - }); + try { + const combinedCredits = await tmdb.getPersonCombinedCredits({ + personId: Number(req.params.id), + language: req.locale ?? (req.query.language as string), + }); - const castMedia = await Media.getRelatedMedia( - combinedCredits.cast.map((result) => result.id) - ); + const castMedia = await Media.getRelatedMedia( + combinedCredits.cast.map((result) => result.id) + ); - const crewMedia = await Media.getRelatedMedia( - combinedCredits.crew.map((result) => result.id) - ); + const crewMedia = await Media.getRelatedMedia( + combinedCredits.crew.map((result) => result.id) + ); - return res.status(200).json({ - cast: combinedCredits.cast - .map((result) => - mapCastCredits( - result, - castMedia.find( - (med) => - med.tmdbId === result.id && med.mediaType === result.media_type + return res.status(200).json({ + cast: combinedCredits.cast + .map((result) => + mapCastCredits( + result, + castMedia.find( + (med) => + med.tmdbId === result.id && med.mediaType === result.media_type + ) ) ) - ) - .filter((item) => !item.adult), - crew: combinedCredits.crew - .map((result) => - mapCrewCredits( - result, - crewMedia.find( - (med) => - med.tmdbId === result.id && med.mediaType === result.media_type + .filter((item) => !item.adult), + crew: combinedCredits.crew + .map((result) => + mapCrewCredits( + result, + crewMedia.find( + (med) => + med.tmdbId === result.id && med.mediaType === result.media_type + ) ) ) - ) - .filter((item) => !item.adult), - id: combinedCredits.id, - }); + .filter((item) => !item.adult), + 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; diff --git a/server/routes/search.ts b/server/routes/search.ts index 466045d0..ca59976a 100644 --- a/server/routes/search.ts +++ b/server/routes/search.ts @@ -3,43 +3,56 @@ import TheMovieDb from '../api/themoviedb'; import { TmdbSearchMultiResponse } from '../api/themoviedb/interfaces'; import Media from '../entity/Media'; import { findSearchProvider } from '../lib/search'; +import logger from '../logger'; import { mapSearchResults } from '../models/Search'; const searchRoutes = Router(); -searchRoutes.get('/', async (req, res) => { +searchRoutes.get('/', async (req, res, next) => { const queryString = req.query.query as string; const searchProvider = findSearchProvider(queryString.toLowerCase()); let results: TmdbSearchMultiResponse; - if (searchProvider) { - const [id] = queryString - .toLowerCase() - .match(searchProvider.pattern) as RegExpMatchArray; - results = await searchProvider.search( - id, - req.locale ?? (req.query.language as string) + try { + if (searchProvider) { + const [id] = queryString + .toLowerCase() + .match(searchProvider.pattern) as RegExpMatchArray; + results = await searchProvider.search( + 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({ - query: queryString, - page: Number(req.query.page), - language: req.locale ?? (req.query.language as string), + return res.status(200).json({ + page: results.page, + totalPages: results.total_pages, + 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; diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index fdc32b1b..ccd25d2c 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -243,52 +243,63 @@ settingsRoutes.post('/tautulli', async (req, res) => { settingsRoutes.get( '/plex/users', isAuthenticated(Permission.MANAGE_USERS), - async (req, res) => { + async (req, res, next) => { const userRepository = getRepository(User); const qb = userRepository.createQueryBuilder('user'); - const admin = await userRepository.findOneOrFail({ - select: ['id', 'plexToken'], - order: { id: 'ASC' }, - }); - const plexApi = new PlexTvAPI(admin.plexToken ?? ''); - const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map( - (user) => user.$ - ).filter((user) => user.email); - - const unimportedPlexUsers: { - id: string; - title: string; - username: string; - email: string; - thumb: string; - }[] = []; - - const existingUsers = await qb - .where('user.plexId IN (:...plexIds)', { - plexIds: plexUsers.map((plexUser) => plexUser.id), - }) - .orWhere('user.email IN (:...plexEmails)', { - plexEmails: plexUsers.map((plexUser) => plexUser.email.toLowerCase()), - }) - .getMany(); - - await Promise.all( - plexUsers.map(async (plexUser) => { - if ( - !existingUsers.find( - (user) => - user.plexId === parseInt(plexUser.id) || - user.email === plexUser.email.toLowerCase() - ) && - (await plexApi.checkUserAccess(parseInt(plexUser.id))) - ) { - unimportedPlexUsers.push(plexUser); - } - }) - ); + try { + const admin = await userRepository.findOneOrFail({ + select: ['id', 'plexToken'], + order: { id: 'ASC' }, + }); + const plexApi = new PlexTvAPI(admin.plexToken ?? ''); + const plexUsers = (await plexApi.getUsers()).MediaContainer.User.map( + (user) => user.$ + ).filter((user) => user.email); + + const unimportedPlexUsers: { + id: string; + title: string; + username: string; + email: string; + thumb: string; + }[] = []; + + const existingUsers = await qb + .where('user.plexId IN (:...plexIds)', { + plexIds: plexUsers.map((plexUser) => plexUser.id), + }) + .orWhere('user.email IN (:...plexEmails)', { + plexEmails: plexUsers.map((plexUser) => plexUser.email.toLowerCase()), + }) + .getMany(); + + await Promise.all( + plexUsers.map(async (plexUser) => { + if ( + !existingUsers.find( + (user) => + user.plexId === parseInt(plexUser.id) || + user.email === plexUser.email.toLowerCase() + ) && + (await plexApi.checkUserAccess(parseInt(plexUser.id))) + ) { + 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.', + }); + } } ); diff --git a/server/routes/tv.ts b/server/routes/tv.ts index 043e610f..201e7afe 100644 --- a/server/routes/tv.ts +++ b/server/routes/tv.ts @@ -21,104 +21,156 @@ tvRoutes.get('/:id', async (req, res, next) => { return res.status(200).json(mapTvDetails(tv, media)); } catch (e) { - logger.error('Failed to get tv show', { + logger.debug('Something went wrong retrieving series', { label: 'API', 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 season = await tmdb.getTvSeason({ - tvId: Number(req.params.id), - seasonNumber: Number(req.params.seasonNumber), - language: req.locale ?? (req.query.language as string), - }); + try { + const season = await tmdb.getTvSeason({ + tvId: Number(req.params.id), + 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 results = await tmdb.getTvRecommendations({ - tvId: Number(req.params.id), - page: Number(req.query.page), - language: req.locale ?? (req.query.language as string), - }); - - 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: results.results.map((result) => - mapTvResult( - result, - media.find( - (req) => req.tmdbId === result.id && req.mediaType === MediaType.TV + try { + const results = await tmdb.getTvRecommendations({ + tvId: Number(req.params.id), + page: Number(req.query.page), + language: req.locale ?? (req.query.language as string), + }); + + 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: results.results.map((result) => + mapTvResult( + result, + 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 results = await tmdb.getTvSimilar({ - tvId: Number(req.params.id), - page: Number(req.query.page), - language: req.locale ?? (req.query.language as string), - }); - - 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: results.results.map((result) => - mapTvResult( - result, - media.find( - (req) => req.tmdbId === result.id && req.mediaType === MediaType.TV + try { + const results = await tmdb.getTvSimilar({ + tvId: Number(req.params.id), + page: Number(req.query.page), + language: req.locale ?? (req.query.language as string), + }); + + 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: results.results.map((result) => + mapTvResult( + result, + 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) => { const tmdb = new TheMovieDb(); const rtapi = new RottenTomatoes(); - const tv = await tmdb.getTvShow({ - tvId: Number(req.params.id), - }); + try { + const tv = await tmdb.getTvShow({ + tvId: Number(req.params.id), + }); - if (!tv) { - return next({ status: 404, message: 'TV Show does not exist' }); - } + const rtratings = await rtapi.getTVRatings( + tv.name, + tv.first_air_date ? Number(tv.first_air_date.slice(0, 4)) : undefined + ); - const rtratings = await rtapi.getTVRatings( - tv.name, - tv.first_air_date ? Number(tv.first_air_date.slice(0, 4)) : undefined - ); + if (!rtratings) { + return next({ + status: 404, + message: 'Rotten Tomatoes ratings not found.', + }); + } - if (!rtratings) { - return next({ status: 404, message: 'Unable to retrieve ratings' }); + return res.status(200).json(rtratings); + } 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; diff --git a/server/subscriber/IssueCommentSubscriber.ts b/server/subscriber/IssueCommentSubscriber.ts index c844b614..1b1b7b55 100644 --- a/server/subscriber/IssueCommentSubscriber.ts +++ b/server/subscriber/IssueCommentSubscriber.ts @@ -12,6 +12,7 @@ import IssueComment from '../entity/IssueComment'; import Media from '../entity/Media'; import notificationManager, { Notification } from '../lib/notifications'; import { Permission } from '../lib/permissions'; +import logger from '../logger'; @EventSubscriber() export class IssueCommentSubscriber @@ -26,62 +27,67 @@ export class IssueCommentSubscriber let image: string; const tmdb = new TheMovieDb(); - const issue = ( - await getRepository(IssueComment).findOne({ - where: { id: entity.id }, - relations: ['issue'], - }) - )?.issue; - if (!issue) { - return; - } + try { + const issue = ( + await getRepository(IssueComment).findOneOrFail({ + where: { id: entity.id }, + relations: ['issue'], + }) + ).issue; - const media = await getRepository(Media).findOne({ - where: { id: issue.media.id }, - }); - if (!media) { - return; - } + const media = await getRepository(Media).findOneOrFail({ + where: { id: issue.media.id }, + }); - if (media.mediaType === MediaType.MOVIE) { - const movie = await tmdb.getMovie({ movieId: media.tmdbId }); + if (media.mediaType === MediaType.MOVIE) { + const movie = await tmdb.getMovie({ movieId: media.tmdbId }); - title = `${movie.title}${ - movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' - }`; - image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`; - } else { - const tvshow = await tmdb.getTvShow({ tvId: media.tmdbId }); + title = `${movie.title}${ + movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' + }`; + image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`; + } else { + const tvshow = await tmdb.getTvShow({ tvId: media.tmdbId }); - title = `${tvshow.name}${ - 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}`; - } + title = `${tvshow.name}${ + 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}`; + } - const [firstComment] = sortBy(issue.comments, 'id'); + const [firstComment] = sortBy(issue.comments, 'id'); - if (entity.id !== firstComment.id) { - // Send notifications to all issue managers - notificationManager.sendNotification(Notification.ISSUE_COMMENT, { - event: `New Comment on ${ - issue.issueType !== IssueType.OTHER - ? `${IssueTypeName[issue.issueType]} ` - : '' - }Issue`, - subject: title, - message: firstComment.message, - comment: entity, - issue, - media, - image, - notifyAdmin: true, - notifyUser: - !issue.createdBy.hasPermission(Permission.MANAGE_ISSUES) && - issue.createdBy.id !== entity.user.id - ? issue.createdBy - : undefined, - }); + if (entity.id !== firstComment.id) { + // Send notifications to all issue managers + notificationManager.sendNotification(Notification.ISSUE_COMMENT, { + event: `New Comment on ${ + issue.issueType !== IssueType.OTHER + ? `${IssueTypeName[issue.issueType]} ` + : '' + }Issue`, + subject: title, + message: firstComment.message, + comment: entity, + issue, + media, + image, + notifyAdmin: true, + notifyUser: + !issue.createdBy.hasPermission(Permission.MANAGE_ISSUES) && + issue.createdBy.id !== entity.user.id + ? issue.createdBy + : undefined, + }); + } + } catch (e) { + logger.error( + 'Something went wrong sending issue comment notification(s)', + { + label: 'Notifications', + errorMessage: e.message, + commentId: entity.id, + } + ); } } diff --git a/server/subscriber/IssueSubscriber.ts b/server/subscriber/IssueSubscriber.ts index 5cf2be59..b593095c 100644 --- a/server/subscriber/IssueSubscriber.ts +++ b/server/subscriber/IssueSubscriber.ts @@ -11,6 +11,7 @@ import { MediaType } from '../constants/media'; import Issue from '../entity/Issue'; import notificationManager, { Notification } from '../lib/notifications'; import { Permission } from '../lib/permissions'; +import logger from '../logger'; @EventSubscriber() export class IssueSubscriber implements EntitySubscriberInterface { @@ -22,72 +23,81 @@ export class IssueSubscriber implements EntitySubscriberInterface { let title: string; let image: string; const tmdb = new TheMovieDb(); - if (entity.media.mediaType === MediaType.MOVIE) { - const movie = await tmdb.getMovie({ movieId: entity.media.tmdbId }); - title = `${movie.title}${ - movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' - }`; - image = `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`; - } else { - const tvshow = await tmdb.getTvShow({ tvId: entity.media.tmdbId }); + try { + if (entity.media.mediaType === MediaType.MOVIE) { + const movie = await tmdb.getMovie({ movieId: entity.media.tmdbId }); - title = `${tvshow.name}${ - 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}`; - } + title = `${movie.title}${ + movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' + }`; + 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'); - const extra: { name: string; value: string }[] = []; + title = `${tvshow.name}${ + 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) { - extra.push({ - name: 'Affected Season', - value: entity.problemSeason.toString(), - }); + const [firstComment] = sortBy(entity.comments, 'id'); + const extra: { name: string; value: string }[] = []; - if (entity.problemEpisode > 0) { + if (entity.media.mediaType === MediaType.TV && entity.problemSeason > 0) { extra.push({ - name: 'Affected Episode', - value: entity.problemEpisode.toString(), + name: 'Affected Season', + value: entity.problemSeason.toString(), }); + + if (entity.problemEpisode > 0) { + extra.push({ + name: 'Affected Episode', + value: entity.problemEpisode.toString(), + }); + } } - } - notificationManager.sendNotification(type, { - event: - type === Notification.ISSUE_CREATED - ? `New ${ - entity.issueType !== IssueType.OTHER - ? `${IssueTypeName[entity.issueType]} ` - : '' - }Issue Reported` - : type === Notification.ISSUE_RESOLVED - ? `${ - entity.issueType !== IssueType.OTHER - ? `${IssueTypeName[entity.issueType]} ` - : '' - }Issue Resolved` - : `${ - entity.issueType !== IssueType.OTHER - ? `${IssueTypeName[entity.issueType]} ` - : '' - }Issue Reopened`, - subject: title, - message: firstComment.message, - issue: entity, - media: entity.media, - image, - extra, - notifyAdmin: true, - notifyUser: - !entity.createdBy.hasPermission(Permission.MANAGE_ISSUES) && - (type === Notification.ISSUE_RESOLVED || - type === Notification.ISSUE_REOPENED) - ? entity.createdBy - : undefined, - }); + notificationManager.sendNotification(type, { + event: + type === Notification.ISSUE_CREATED + ? `New ${ + entity.issueType !== IssueType.OTHER + ? `${IssueTypeName[entity.issueType]} ` + : '' + }Issue Reported` + : type === Notification.ISSUE_RESOLVED + ? `${ + entity.issueType !== IssueType.OTHER + ? `${IssueTypeName[entity.issueType]} ` + : '' + }Issue Resolved` + : `${ + entity.issueType !== IssueType.OTHER + ? `${IssueTypeName[entity.issueType]} ` + : '' + }Issue Reopened`, + subject: title, + message: firstComment.message, + issue: entity, + media: entity.media, + image, + extra, + notifyAdmin: true, + notifyUser: + !entity.createdBy.hasPermission(Permission.MANAGE_ISSUES) && + (type === Notification.ISSUE_RESOLVED || + type === Notification.ISSUE_REOPENED) + ? entity.createdBy + : undefined, + }); + } catch (e) { + logger.error('Something went wrong sending issue notification(s)', { + label: 'Notifications', + errorMessage: e.message, + issueId: entity.id, + }); + } } public afterInsert(event: InsertEvent): void { diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index 1e279377..01752b0d 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -12,6 +12,7 @@ import Media from '../entity/Media'; import { MediaRequest } from '../entity/MediaRequest'; import Season from '../entity/Season'; import notificationManager, { Notification } from '../lib/notifications'; +import logger from '../logger'; @EventSubscriber() export class MediaSubscriber implements EntitySubscriberInterface { @@ -36,26 +37,40 @@ export class MediaSubscriber implements EntitySubscriberInterface { if (relatedRequests.length > 0) { const tmdb = new TheMovieDb(); - const movie = await tmdb.getMovie({ movieId: entity.tmdbId }); - relatedRequests.forEach((request) => { - notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { - event: `${is4k ? '4K ' : ''}Movie Request Now Available`, - notifyAdmin: false, - notifyUser: request.requestedBy, - subject: `${movie.title}${ - movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' - }`, - 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, + try { + const movie = await tmdb.getMovie({ movieId: entity.tmdbId }); + + relatedRequests.forEach((request) => { + notificationManager.sendNotification( + Notification.MEDIA_AVAILABLE, + { + event: `${is4k ? '4K ' : ''}Movie Request Now Available`, + notifyAdmin: false, + notifyUser: request.requestedBy, + subject: `${movie.title}${ + movie.release_date + ? ` (${movie.release_date.slice(0, 4)})` + : '' + }`, + 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 { processedSeasons.push( ...request.seasons.map((season) => season.seasonNumber) ); - const tv = await tmdb.getTvShow({ tvId: entity.tmdbId }); - notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { - event: `${is4k ? '4K ' : ''}Series Request Now Available`, - subject: `${tv.name}${ - tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' - }`, - message: truncate(tv.overview, { - length: 500, - separator: /\s/, - omission: '…', - }), - notifyAdmin: false, - notifyUser: request.requestedBy, - image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, - media: entity, - extra: [ - { - name: 'Requested Seasons', - value: request.seasons - .map((season) => season.seasonNumber) - .join(', '), - }, - ], - request, - }); + + try { + const tv = await tmdb.getTvShow({ tvId: entity.tmdbId }); + notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { + event: `${is4k ? '4K ' : ''}Series Request Now Available`, + subject: `${tv.name}${ + tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' + }`, + message: truncate(tv.overview, { + length: 500, + separator: /\s/, + omission: '…', + }), + notifyAdmin: false, + notifyUser: request.requestedBy, + image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, + media: entity, + extra: [ + { + name: 'Requested Seasons', + value: request.seasons + .map((season) => season.seasonNumber) + .join(', '), + }, + ], + request, + }); + } catch (e) { + logger.error('Something went wrong sending media notification(s)', { + label: 'Notifications', + errorMessage: e.message, + mediaId: entity.id, + }); + } } } } diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 0cb277d2..5dc3f57a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -11,7 +11,7 @@ confinement: strict parts: overseerr: plugin: nodejs - nodejs-version: '14.18.1' + nodejs-version: '16.13.1' nodejs-package-manager: 'yarn' nodejs-yarn-version: v1.22.10 build-packages: