mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
fix: correctly deal with tmdb id duplicates between movies/series
fixes #526
This commit is contained in:
@@ -40,12 +40,15 @@ class Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getMedia(id: number): Promise<Media | undefined> {
|
public static async getMedia(
|
||||||
|
id: number,
|
||||||
|
mediaType: MediaType
|
||||||
|
): Promise<Media | undefined> {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { tmdbId: id },
|
where: { tmdbId: id, mediaType },
|
||||||
relations: ['requests'],
|
relations: ['requests'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ class Media {
|
|||||||
@Column({ type: 'varchar' })
|
@Column({ type: 'varchar' })
|
||||||
public mediaType: MediaType;
|
public mediaType: MediaType;
|
||||||
|
|
||||||
@Column({ unique: true })
|
@Column()
|
||||||
@Index()
|
@Index()
|
||||||
public tmdbId: number;
|
public tmdbId: number;
|
||||||
|
|
||||||
@@ -70,7 +73,7 @@ class Media {
|
|||||||
@Index()
|
@Index()
|
||||||
public tvdbId?: number;
|
public tvdbId?: number;
|
||||||
|
|
||||||
@Column({ unique: true, nullable: true })
|
@Column({ nullable: true })
|
||||||
@Index()
|
@Index()
|
||||||
public imdbId?: string;
|
public imdbId?: string;
|
||||||
|
|
||||||
|
@@ -44,11 +44,11 @@ class JobPlexSync {
|
|||||||
this.isRecentOnly = isRecentOnly ?? false;
|
this.isRecentOnly = isRecentOnly ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getExisting(tmdbId: number) {
|
private async getExisting(tmdbId: number, mediaType: MediaType) {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
|
|
||||||
const existing = await mediaRepository.findOne({
|
const existing = await mediaRepository.findOne({
|
||||||
where: { tmdbId: tmdbId },
|
where: { tmdbId: tmdbId, mediaType },
|
||||||
});
|
});
|
||||||
|
|
||||||
return existing;
|
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) {
|
if (existing && existing.status === MediaStatus.AVAILABLE) {
|
||||||
this.log(`Title exists and is already available ${metadata.title}`);
|
this.log(`Title exists and is already available ${metadata.title}`);
|
||||||
@@ -115,7 +118,7 @@ class JobPlexSync {
|
|||||||
throw new Error('Unable to find TMDB ID');
|
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) {
|
if (existing && existing.status === MediaStatus.AVAILABLE) {
|
||||||
this.log(`Title exists and is already available ${plexitem.title}`);
|
this.log(`Title exists and is already available ${plexitem.title}`);
|
||||||
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
|
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
|
||||||
@@ -184,9 +187,7 @@ class JobPlexSync {
|
|||||||
if (tvShow && metadata) {
|
if (tvShow && metadata) {
|
||||||
// Lets get the available seasons from plex
|
// Lets get the available seasons from plex
|
||||||
const seasons = tvShow.seasons;
|
const seasons = tvShow.seasons;
|
||||||
const media = await mediaRepository.findOne({
|
const media = await this.getExisting(tvShow.id, MediaType.TV);
|
||||||
where: { tmdbId: tvShow.id, mediaType: MediaType.TV },
|
|
||||||
});
|
|
||||||
|
|
||||||
const newSeasons: Season[] = [];
|
const newSeasons: Season[] = [];
|
||||||
|
|
||||||
|
@@ -0,0 +1,52 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class RemoveTmdbIdUniqueConstraint1609236552057
|
||||||
|
implements MigrationInterface {
|
||||||
|
name = 'RemoveTmdbIdUniqueConstraint1609236552057';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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") `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import { TmdbCollection } from '../api/themoviedb';
|
import { TmdbCollection } from '../api/themoviedb';
|
||||||
|
import { MediaType } from '../constants/media';
|
||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
import { mapMovieResult, MovieResult } from './Search';
|
import { mapMovieResult, MovieResult } from './Search';
|
||||||
|
|
||||||
@@ -23,7 +24,9 @@ export const mapCollection = (
|
|||||||
parts: collection.parts.map((part) =>
|
parts: collection.parts.map((part) =>
|
||||||
mapMovieResult(
|
mapMovieResult(
|
||||||
part,
|
part,
|
||||||
media?.find((req) => req.tmdbId === part.id)
|
media?.find(
|
||||||
|
(req) => req.tmdbId === part.id && req.mediaType === MediaType.MOVIE
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@@ -3,6 +3,7 @@ import type {
|
|||||||
TmdbPersonResult,
|
TmdbPersonResult,
|
||||||
TmdbTvResult,
|
TmdbTvResult,
|
||||||
} from '../api/themoviedb';
|
} from '../api/themoviedb';
|
||||||
|
import { MediaType as MainMediaType } from '../constants/media';
|
||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
|
|
||||||
export type MediaType = 'tv' | 'movie' | 'person';
|
export type MediaType = 'tv' | 'movie' | 'person';
|
||||||
@@ -122,12 +123,18 @@ export const mapSearchResults = (
|
|||||||
case 'movie':
|
case 'movie':
|
||||||
return mapMovieResult(
|
return mapMovieResult(
|
||||||
result,
|
result,
|
||||||
media?.find((req) => req.tmdbId === result.id)
|
media?.find(
|
||||||
|
(req) =>
|
||||||
|
req.tmdbId === result.id && req.mediaType === MainMediaType.MOVIE
|
||||||
|
)
|
||||||
);
|
);
|
||||||
case 'tv':
|
case 'tv':
|
||||||
return mapTvResult(
|
return mapTvResult(
|
||||||
result,
|
result,
|
||||||
media?.find((req) => req.tmdbId === result.id)
|
media?.find(
|
||||||
|
(req) =>
|
||||||
|
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
|
||||||
|
)
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return mapPersonResult(result);
|
return mapPersonResult(result);
|
||||||
|
@@ -26,7 +26,9 @@ discoverRoutes.get('/movies', async (req, res) => {
|
|||||||
results: data.results.map((result) =>
|
results: data.results.map((result) =>
|
||||||
mapMovieResult(
|
mapMovieResult(
|
||||||
result,
|
result,
|
||||||
media.find((req) => req.tmdbId === result.id)
|
media.find(
|
||||||
|
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@@ -5,6 +5,7 @@ import { mapMovieResult } from '../models/Search';
|
|||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
import RottenTomatoes from '../api/rottentomatoes';
|
import RottenTomatoes from '../api/rottentomatoes';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import { MediaType } from '../constants/media';
|
||||||
|
|
||||||
const movieRoutes = Router();
|
const movieRoutes = Router();
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
|
|||||||
language: req.query.language as string,
|
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));
|
return res.status(200).json(mapMovieDetails(tmdbMovie, media));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -49,7 +50,9 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
|
|||||||
results: results.results.map((result) =>
|
results: results.results.map((result) =>
|
||||||
mapMovieResult(
|
mapMovieResult(
|
||||||
result,
|
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) =>
|
results: results.results.map((result) =>
|
||||||
mapMovieResult(
|
mapMovieResult(
|
||||||
result,
|
result,
|
||||||
media.find((req) => req.tmdbId === result.id)
|
media.find(
|
||||||
|
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@@ -45,13 +45,19 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
|
|||||||
cast: combinedCredits.cast.map((result) =>
|
cast: combinedCredits.cast.map((result) =>
|
||||||
mapCastCredits(
|
mapCastCredits(
|
||||||
result,
|
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) =>
|
crew: combinedCredits.crew.map((result) =>
|
||||||
mapCrewCredits(
|
mapCrewCredits(
|
||||||
result,
|
result,
|
||||||
crewMedia.find((med) => med.tmdbId === result.id)
|
crewMedia.find(
|
||||||
|
(med) =>
|
||||||
|
med.tmdbId === result.id && med.mediaType === result.media_type
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
id: combinedCredits.id,
|
id: combinedCredits.id,
|
||||||
|
@@ -102,7 +102,7 @@ requestRoutes.post(
|
|||||||
: await tmdb.getTvShow({ tvId: req.body.mediaId });
|
: await tmdb.getTvShow({ tvId: req.body.mediaId });
|
||||||
|
|
||||||
let media = await mediaRepository.findOne({
|
let media = await mediaRepository.findOne({
|
||||||
where: { tmdbId: req.body.mediaId },
|
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
|
||||||
relations: ['requests'],
|
relations: ['requests'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ import { mapTvResult } from '../models/Search';
|
|||||||
import Media from '../entity/Media';
|
import Media from '../entity/Media';
|
||||||
import RottenTomatoes from '../api/rottentomatoes';
|
import RottenTomatoes from '../api/rottentomatoes';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import { MediaType } from '../constants/media';
|
||||||
|
|
||||||
const tvRoutes = Router();
|
const tvRoutes = Router();
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
|
|||||||
language: req.query.language as string,
|
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));
|
return res.status(200).json(mapTvDetails(tv, media));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -60,7 +61,9 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
|
|||||||
results: results.results.map((result) =>
|
results: results.results.map((result) =>
|
||||||
mapTvResult(
|
mapTvResult(
|
||||||
result,
|
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) =>
|
results: results.results.map((result) =>
|
||||||
mapTvResult(
|
mapTvResult(
|
||||||
result,
|
result,
|
||||||
media.find((req) => req.tmdbId === result.id)
|
media.find(
|
||||||
|
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user