From 721ed9a93087a57ae749388bddcacf26022e3df6 Mon Sep 17 00:00:00 2001 From: sct Date: Tue, 29 Dec 2020 10:26:37 +0000 Subject: [PATCH] fix: correctly deal with tmdb id duplicates between movies/series fixes #526 --- server/entity/Media.ts | 11 ++-- server/job/plexsync/index.ts | 15 +++--- ...9236552057-RemoveTmdbIdUniqueConstraint.ts | 52 +++++++++++++++++++ server/models/Collection.ts | 5 +- server/models/Search.ts | 11 +++- server/routes/discover.ts | 4 +- server/routes/movie.ts | 11 ++-- server/routes/person.ts | 10 +++- server/routes/request.ts | 2 +- server/routes/tv.ts | 11 ++-- 10 files changed, 108 insertions(+), 24 deletions(-) create mode 100644 server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 7723fb9c9..92a747742 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -40,12 +40,15 @@ class Media { } } - public static async getMedia(id: number): Promise { + public static async getMedia( + id: number, + mediaType: MediaType + ): Promise { const mediaRepository = getRepository(Media); try { const media = await mediaRepository.findOne({ - where: { tmdbId: id }, + where: { tmdbId: id, mediaType }, relations: ['requests'], }); @@ -62,7 +65,7 @@ class Media { @Column({ type: 'varchar' }) public mediaType: MediaType; - @Column({ unique: true }) + @Column() @Index() public tmdbId: number; @@ -70,7 +73,7 @@ class Media { @Index() public tvdbId?: number; - @Column({ unique: true, nullable: true }) + @Column({ nullable: true }) @Index() public imdbId?: string; diff --git a/server/job/plexsync/index.ts b/server/job/plexsync/index.ts index 3f600c383..f92f5ee04 100644 --- a/server/job/plexsync/index.ts +++ b/server/job/plexsync/index.ts @@ -44,11 +44,11 @@ class JobPlexSync { this.isRecentOnly = isRecentOnly ?? false; } - private async getExisting(tmdbId: number) { + private async getExisting(tmdbId: number, mediaType: MediaType) { const mediaRepository = getRepository(Media); const existing = await mediaRepository.findOne({ - where: { tmdbId: tmdbId }, + where: { tmdbId: tmdbId, mediaType }, }); return existing; @@ -78,7 +78,10 @@ class JobPlexSync { } }); - const existing = await this.getExisting(newMedia.tmdbId); + const existing = await this.getExisting( + newMedia.tmdbId, + MediaType.MOVIE + ); if (existing && existing.status === MediaStatus.AVAILABLE) { this.log(`Title exists and is already available ${metadata.title}`); @@ -115,7 +118,7 @@ class JobPlexSync { throw new Error('Unable to find TMDB ID'); } - const existing = await this.getExisting(tmdbMovieId); + const existing = await this.getExisting(tmdbMovieId, MediaType.MOVIE); if (existing && existing.status === MediaStatus.AVAILABLE) { this.log(`Title exists and is already available ${plexitem.title}`); } else if (existing && existing.status !== MediaStatus.AVAILABLE) { @@ -184,9 +187,7 @@ class JobPlexSync { if (tvShow && metadata) { // Lets get the available seasons from plex const seasons = tvShow.seasons; - const media = await mediaRepository.findOne({ - where: { tmdbId: tvShow.id, mediaType: MediaType.TV }, - }); + const media = await this.getExisting(tvShow.id, MediaType.TV); const newSeasons: Season[] = []; diff --git a/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts b/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts new file mode 100644 index 000000000..0be266993 --- /dev/null +++ b/server/migration/1609236552057-RemoveTmdbIdUniqueConstraint.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveTmdbIdUniqueConstraint1609236552057 + implements MigrationInterface { + name = 'RemoveTmdbIdUniqueConstraint1609236552057'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + await queryRunner.query( + `CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"` + ); + await queryRunner.query(`DROP TABLE "media"`); + await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`); + await queryRunner.query( + `CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`); + await queryRunner.query( + `CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"))` + ); + await queryRunner.query( + `INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "temporary_media"` + ); + await queryRunner.query(`DROP TABLE "temporary_media"`); + await queryRunner.query( + `CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") ` + ); + } +} diff --git a/server/models/Collection.ts b/server/models/Collection.ts index f80a1ad79..64709502c 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -1,4 +1,5 @@ import { TmdbCollection } from '../api/themoviedb'; +import { MediaType } from '../constants/media'; import Media from '../entity/Media'; import { mapMovieResult, MovieResult } from './Search'; @@ -23,7 +24,9 @@ export const mapCollection = ( parts: collection.parts.map((part) => mapMovieResult( part, - media?.find((req) => req.tmdbId === part.id) + media?.find( + (req) => req.tmdbId === part.id && req.mediaType === MediaType.MOVIE + ) ) ), }); diff --git a/server/models/Search.ts b/server/models/Search.ts index 68d8dab89..7d347207d 100644 --- a/server/models/Search.ts +++ b/server/models/Search.ts @@ -3,6 +3,7 @@ import type { TmdbPersonResult, TmdbTvResult, } from '../api/themoviedb'; +import { MediaType as MainMediaType } from '../constants/media'; import Media from '../entity/Media'; export type MediaType = 'tv' | 'movie' | 'person'; @@ -122,12 +123,18 @@ export const mapSearchResults = ( case 'movie': return mapMovieResult( result, - media?.find((req) => req.tmdbId === result.id) + media?.find( + (req) => + req.tmdbId === result.id && req.mediaType === MainMediaType.MOVIE + ) ); case 'tv': return mapTvResult( result, - media?.find((req) => req.tmdbId === result.id) + media?.find( + (req) => + req.tmdbId === result.id && req.mediaType === MainMediaType.TV + ) ); default: return mapPersonResult(result); diff --git a/server/routes/discover.ts b/server/routes/discover.ts index 1193354ce..ec2116aa0 100644 --- a/server/routes/discover.ts +++ b/server/routes/discover.ts @@ -26,7 +26,9 @@ discoverRoutes.get('/movies', async (req, res) => { results: data.results.map((result) => mapMovieResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + ) ) ), }); diff --git a/server/routes/movie.ts b/server/routes/movie.ts index af4992ed3..cadaf5a7e 100644 --- a/server/routes/movie.ts +++ b/server/routes/movie.ts @@ -5,6 +5,7 @@ import { mapMovieResult } from '../models/Search'; import Media from '../entity/Media'; import RottenTomatoes from '../api/rottentomatoes'; import logger from '../logger'; +import { MediaType } from '../constants/media'; const movieRoutes = Router(); @@ -17,7 +18,7 @@ movieRoutes.get('/:id', async (req, res, next) => { language: req.query.language as string, }); - const media = await Media.getMedia(tmdbMovie.id); + const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE); return res.status(200).json(mapMovieDetails(tmdbMovie, media)); } catch (e) { @@ -49,7 +50,9 @@ movieRoutes.get('/:id/recommendations', async (req, res) => { results: results.results.map((result) => mapMovieResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + ) ) ), }); @@ -75,7 +78,9 @@ movieRoutes.get('/:id/similar', async (req, res) => { results: results.results.map((result) => mapMovieResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE + ) ) ), }); diff --git a/server/routes/person.ts b/server/routes/person.ts index dfdd4ce24..add0b0f1c 100644 --- a/server/routes/person.ts +++ b/server/routes/person.ts @@ -45,13 +45,19 @@ personRoutes.get('/:id/combined_credits', async (req, res) => { cast: combinedCredits.cast.map((result) => mapCastCredits( result, - castMedia.find((med) => med.tmdbId === result.id) + castMedia.find( + (med) => + med.tmdbId === result.id && med.mediaType === result.media_type + ) ) ), crew: combinedCredits.crew.map((result) => mapCrewCredits( result, - crewMedia.find((med) => med.tmdbId === result.id) + crewMedia.find( + (med) => + med.tmdbId === result.id && med.mediaType === result.media_type + ) ) ), id: combinedCredits.id, diff --git a/server/routes/request.ts b/server/routes/request.ts index 857d11f74..dfafdc606 100644 --- a/server/routes/request.ts +++ b/server/routes/request.ts @@ -102,7 +102,7 @@ requestRoutes.post( : await tmdb.getTvShow({ tvId: req.body.mediaId }); let media = await mediaRepository.findOne({ - where: { tmdbId: req.body.mediaId }, + where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType }, relations: ['requests'], }); diff --git a/server/routes/tv.ts b/server/routes/tv.ts index be3915b03..1ddf1f80c 100644 --- a/server/routes/tv.ts +++ b/server/routes/tv.ts @@ -5,6 +5,7 @@ import { mapTvResult } from '../models/Search'; import Media from '../entity/Media'; import RottenTomatoes from '../api/rottentomatoes'; import logger from '../logger'; +import { MediaType } from '../constants/media'; const tvRoutes = Router(); @@ -16,7 +17,7 @@ tvRoutes.get('/:id', async (req, res, next) => { language: req.query.language as string, }); - const media = await Media.getMedia(tv.id); + const media = await Media.getMedia(tv.id, MediaType.TV); return res.status(200).json(mapTvDetails(tv, media)); } catch (e) { @@ -60,7 +61,9 @@ tvRoutes.get('/:id/recommendations', async (req, res) => { results: results.results.map((result) => mapTvResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.TV + ) ) ), }); @@ -86,7 +89,9 @@ tvRoutes.get('/:id/similar', async (req, res) => { results: results.results.map((result) => mapTvResult( result, - media.find((req) => req.tmdbId === result.id) + media.find( + (req) => req.tmdbId === result.id && req.mediaType === MediaType.TV + ) ) ), });