fix: disambiguate tmdb ids by media type across lookups (#2577)

This commit is contained in:
fallenbagel
2026-03-14 23:47:21 +05:00
committed by GitHub
parent d25d0ca570
commit 0be18968b4
22 changed files with 479 additions and 70 deletions

View File

@@ -18,7 +18,7 @@ import {
import type { ZodNumber, ZodOptional, ZodString } from 'zod';
@Entity()
@Unique(['tmdbId'])
@Unique(['tmdbId', 'mediaType'])
export class Blocklist implements BlocklistItem {
@PrimaryGeneratedColumn()
public id: number;
@@ -77,6 +77,7 @@ export class Blocklist implements BlocklistItem {
let media = await mediaRepository.findOne({
where: {
tmdbId: blocklistRequest.tmdbId,
mediaType: blocklistRequest.mediaType,
},
});

View File

@@ -30,22 +30,17 @@ import Season from './Season';
class Media {
public static async getRelatedMedia(
user: User | undefined,
tmdbIds: number | number[]
items: { tmdbId: number; mediaType: string }[]
): Promise<Media[]> {
const mediaRepository = getRepository(Media);
try {
let finalIds: number[];
if (!Array.isArray(tmdbIds)) {
finalIds = [tmdbIds];
} else {
finalIds = tmdbIds;
}
if (finalIds.length === 0) {
if (items.length === 0) {
return [];
}
const finalIds = [...new Set(items.map((i) => i.tmdbId))];
const media = await mediaRepository
.createQueryBuilder('media')
.leftJoinAndSelect(
@@ -57,7 +52,9 @@ class Media {
.where(' media.tmdbId in (:...finalIds)', { finalIds })
.getMany();
return media;
return media.filter((m) =>
items.some((i) => i.tmdbId === m.tmdbId && i.mediaType === m.mediaType)
);
} catch (e) {
logger.error(e.message);
return [];

View File

@@ -25,7 +25,7 @@ export class NotFoundError extends Error {
}
@Entity()
@Unique('UNIQUE_USER_DB', ['tmdbId', 'requestedBy'])
@Unique('UNIQUE_USER_DB', ['tmdbId', 'mediaType', 'requestedBy'])
export class Watchlist implements WatchlistItem {
@PrimaryGeneratedColumn()
id: number;
@@ -142,11 +142,13 @@ export class Watchlist implements WatchlistItem {
public static async deleteWatchlist(
tmdbId: Watchlist['tmdbId'],
mediaType: MediaType,
user: User
): Promise<Watchlist | null> {
const watchlistRepository = getRepository(this);
const watchlist = await watchlistRepository.findOneBy({
tmdbId,
mediaType,
requestedBy: { id: user.id },
});
if (!watchlist) {

View File

@@ -173,7 +173,7 @@ class BlocklistedTagProcessor implements RunnableScanner<StatusBase> {
for (const entry of response.results) {
const blocklistEntry = await blocklistRepository.findOne({
where: { tmdbId: entry.id },
where: { tmdbId: entry.id, mediaType },
});
if (blocklistEntry) {
@@ -209,7 +209,11 @@ class BlocklistedTagProcessor implements RunnableScanner<StatusBase> {
const mediaRepository = em.getRepository(Media);
const mediaToRemove = await mediaRepository
.createQueryBuilder('media')
.innerJoinAndSelect(Blocklist, 'blist', 'blist.tmdbId = media.tmdbId')
.innerJoinAndSelect(
Blocklist,
'blist',
'blist.tmdbId = media.tmdbId AND blist.mediaType = media.mediaType'
)
.where(`blist.blocklistedTags IS NOT NULL`)
.getMany();

View File

@@ -367,18 +367,16 @@ class PlexScanner
}
}
if (mediaIds.tvdbId) {
await this.processShow(
mediaIds.tmdbId,
mediaIds.tvdbId ?? tvShow.external_ids.tvdb_id,
processableSeasons,
{
mediaAddedAt: new Date(metadata.addedAt * 1000),
ratingKey: ratingKey,
title: metadata.title,
}
);
}
await this.processShow(
mediaIds.tmdbId,
mediaIds.tvdbId ?? tvShow.external_ids.tvdb_id,
processableSeasons,
{
mediaAddedAt: new Date(metadata.addedAt * 1000),
ratingKey: ratingKey,
title: metadata.title,
}
);
}
private async getMediaIds(plexitem: PlexLibraryItem): Promise<MediaIds> {

View File

@@ -67,7 +67,10 @@ class WatchlistSync {
const mediaItems = await Media.getRelatedMedia(
user,
response.items.map((i) => i.tmdbId)
response.items.map((i) => ({
tmdbId: i.tmdbId,
mediaType: i.type === 'show' ? MediaType.TV : MediaType.MOVIE,
}))
);
const watchlistTmdbIds = response.items.map((i) => i.tmdbId);
@@ -87,19 +90,23 @@ class WatchlistSync {
.map((r) => `${r.media.mediaType}:${r.media.tmdbId}`)
);
const unavailableItems = response.items.filter(
(i) =>
!autoRequestedTmdbIds.has(
`${i.type === 'show' ? MediaType.TV : MediaType.MOVIE}:${i.tmdbId}`
) &&
const unavailableItems = response.items.filter((i) => {
const itemMediaType = i.type === 'show' ? MediaType.TV : MediaType.MOVIE;
return (
!autoRequestedTmdbIds.has(`${itemMediaType}:${i.tmdbId}`) &&
!mediaItems.find(
(m) =>
m.tmdbId === i.tmdbId &&
m.mediaType === itemMediaType &&
(m.status === MediaStatus.BLOCKLISTED ||
(m.status !== MediaStatus.UNKNOWN && m.mediaType === 'movie') ||
(m.mediaType === 'tv' && m.status === MediaStatus.AVAILABLE))
(itemMediaType === MediaType.MOVIE &&
m.status !== MediaStatus.UNKNOWN) ||
(itemMediaType === MediaType.TV &&
m.status === MediaStatus.AVAILABLE))
)
);
);
});
for (const mediaItem of unavailableItems) {
try {

View File

@@ -0,0 +1,51 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddMediaTypeToUniqueConstraints1772048000333 implements MigrationInterface {
name = 'AddMediaTypeToUniqueConstraints1772048000333';
public async up(queryRunner: QueryRunner): Promise<void> {
// Manually added: TypeORM migration:generate does not detect changes to named unique constraints.
await queryRunner.query(
`ALTER TABLE "watchlist" DROP CONSTRAINT "UNIQUE_USER_DB"`
);
await queryRunner.query(
`ALTER TABLE "watchlist" ADD CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "mediaType", "requestedById")`
);
// Auto-generated by TypeORM
await queryRunner.query(
`CREATE SEQUENCE IF NOT EXISTS "blocklist_id_seq" OWNED BY "blocklist"."id"`
);
await queryRunner.query(
`ALTER TABLE "blocklist" ALTER COLUMN "id" SET DEFAULT nextval('"blocklist_id_seq"')`
);
await queryRunner.query(
`ALTER TABLE "blocklist" DROP CONSTRAINT "UQ_6bbafa28411e6046421991ea21c"`
);
await queryRunner.query(
`ALTER TABLE "blocklist" ADD CONSTRAINT "UQ_81504e02db89b4c1e3152729fa6" UNIQUE ("tmdbId", "mediaType")`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Manually added: TypeORM migration:generate does not detect changes to named unique constraints.
await queryRunner.query(
`ALTER TABLE "watchlist" DROP CONSTRAINT "UNIQUE_USER_DB"`
);
await queryRunner.query(
`ALTER TABLE "watchlist" ADD CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById")`
);
// Auto-generated by TypeORM
await queryRunner.query(
`ALTER TABLE "blocklist" DROP CONSTRAINT "UQ_81504e02db89b4c1e3152729fa6"`
);
await queryRunner.query(
`ALTER TABLE "blocklist" ADD CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId")`
);
await queryRunner.query(
`ALTER TABLE "blocklist" ALTER COLUMN "id" DROP DEFAULT`
);
await queryRunner.query(`DROP SEQUENCE "blocklist_id_seq"`);
}
}

View File

@@ -0,0 +1,225 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddMediaTypeToUniqueConstraints1772047972752 implements MigrationInterface {
name = 'AddMediaTypeToUniqueConstraints1772047972752';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
await queryRunner.query(
`CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") `
);
await queryRunner.query(`DROP INDEX "IDX_356721a49f145aa439c16e6b99"`);
await queryRunner.query(`DROP INDEX "IDX_09b94c932e84635c5461f3c0a9"`);
await queryRunner.query(
`CREATE TABLE "temporary_blocklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "blocklistedTags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "FK_5c8af2d0e83b3be6d250eccc19d" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_356721a49f145aa439c16e6b999" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_blocklist"("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "blocklist"`
);
await queryRunner.query(`DROP TABLE "blocklist"`);
await queryRunner.query(
`ALTER TABLE "temporary_blocklist" RENAME TO "blocklist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_356721a49f145aa439c16e6b99" ON "blocklist" ("userId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_09b94c932e84635c5461f3c0a9" ON "blocklist" ("tmdbId") `
);
await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
await queryRunner.query(
`CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") `
);
await queryRunner.query(`DROP INDEX "IDX_356721a49f145aa439c16e6b99"`);
await queryRunner.query(`DROP INDEX "IDX_09b94c932e84635c5461f3c0a9"`);
await queryRunner.query(
`CREATE TABLE "temporary_blocklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "blocklistedTags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "FK_5c8af2d0e83b3be6d250eccc19d" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_356721a49f145aa439c16e6b999" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_blocklist"("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "blocklist"`
);
await queryRunner.query(`DROP TABLE "blocklist"`);
await queryRunner.query(
`ALTER TABLE "temporary_blocklist" RENAME TO "blocklist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_356721a49f145aa439c16e6b99" ON "blocklist" ("userId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_09b94c932e84635c5461f3c0a9" ON "blocklist" ("tmdbId") `
);
await queryRunner.query(`DROP INDEX "IDX_356721a49f145aa439c16e6b99"`);
await queryRunner.query(`DROP INDEX "IDX_09b94c932e84635c5461f3c0a9"`);
await queryRunner.query(
`CREATE TABLE "temporary_blocklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "blocklistedTags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "UQ_81504e02db89b4c1e3152729fa6" UNIQUE ("tmdbId", "mediaType"), CONSTRAINT "FK_5c8af2d0e83b3be6d250eccc19d" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_356721a49f145aa439c16e6b999" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_blocklist"("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "blocklist"`
);
await queryRunner.query(`DROP TABLE "blocklist"`);
await queryRunner.query(
`ALTER TABLE "temporary_blocklist" RENAME TO "blocklist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_356721a49f145aa439c16e6b99" ON "blocklist" ("userId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_09b94c932e84635c5461f3c0a9" ON "blocklist" ("tmdbId") `
);
// Manually added as TypeORM migration:generate does not detect changes to named unique constraints.
await queryRunner.query(`DROP INDEX "IDX_939f205946256cc0d2a1ac51a8"`);
await queryRunner.query(`DROP INDEX "IDX_ae34e6b153a90672eb9dc4857d"`);
await queryRunner.query(`DROP INDEX "IDX_6641da8d831b93dfcb429f8b8b"`);
await queryRunner.query(
`CREATE TABLE "temporary_watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "mediaType", "requestedById"), CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "watchlist"`
);
await queryRunner.query(`DROP TABLE "watchlist"`);
await queryRunner.query(
`ALTER TABLE "temporary_watchlist" RENAME TO "watchlist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId")`
);
await queryRunner.query(
`CREATE INDEX "IDX_ae34e6b153a90672eb9dc4857d" ON "watchlist" ("requestedById")`
);
await queryRunner.query(
`CREATE INDEX "IDX_6641da8d831b93dfcb429f8b8b" ON "watchlist" ("mediaId")`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Manually added as TypeORM migration:generate does not detect changes to named unique constraints.
await queryRunner.query(`DROP INDEX "IDX_939f205946256cc0d2a1ac51a8"`);
await queryRunner.query(`DROP INDEX "IDX_ae34e6b153a90672eb9dc4857d"`);
await queryRunner.query(`DROP INDEX "IDX_6641da8d831b93dfcb429f8b8b"`);
await queryRunner.query(
`CREATE TABLE "temporary_watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "watchlist"`
);
await queryRunner.query(`DROP TABLE "watchlist"`);
await queryRunner.query(
`ALTER TABLE "temporary_watchlist" RENAME TO "watchlist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId")`
);
await queryRunner.query(
`CREATE INDEX "IDX_ae34e6b153a90672eb9dc4857d" ON "watchlist" ("requestedById")`
);
await queryRunner.query(
`CREATE INDEX "IDX_6641da8d831b93dfcb429f8b8b" ON "watchlist" ("mediaId")`
);
// Blocklist: revert to original
await queryRunner.query(`DROP INDEX "IDX_09b94c932e84635c5461f3c0a9"`);
await queryRunner.query(`DROP INDEX "IDX_356721a49f145aa439c16e6b99"`);
await queryRunner.query(
`ALTER TABLE "blocklist" RENAME TO "temporary_blocklist"`
);
await queryRunner.query(
`CREATE TABLE "blocklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "blocklistedTags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "FK_5c8af2d0e83b3be6d250eccc19d" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_356721a49f145aa439c16e6b999" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "blocklist"("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "temporary_blocklist"`
);
await queryRunner.query(`DROP TABLE "temporary_blocklist"`);
await queryRunner.query(
`CREATE INDEX "IDX_09b94c932e84635c5461f3c0a9" ON "blocklist" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_356721a49f145aa439c16e6b99" ON "blocklist" ("userId") `
);
await queryRunner.query(`DROP INDEX "IDX_09b94c932e84635c5461f3c0a9"`);
await queryRunner.query(`DROP INDEX "IDX_356721a49f145aa439c16e6b99"`);
await queryRunner.query(
`ALTER TABLE "blocklist" RENAME TO "temporary_blocklist"`
);
await queryRunner.query(
`CREATE TABLE "blocklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "blocklistedTags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "FK_5c8af2d0e83b3be6d250eccc19d" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_356721a49f145aa439c16e6b999" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "blocklist"("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "temporary_blocklist"`
);
await queryRunner.query(`DROP TABLE "temporary_blocklist"`);
await queryRunner.query(
`CREATE INDEX "IDX_09b94c932e84635c5461f3c0a9" ON "blocklist" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_356721a49f145aa439c16e6b99" ON "blocklist" ("userId") `
);
await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`);
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(
`CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") `
);
await queryRunner.query(`DROP INDEX "IDX_09b94c932e84635c5461f3c0a9"`);
await queryRunner.query(`DROP INDEX "IDX_356721a49f145aa439c16e6b99"`);
await queryRunner.query(
`ALTER TABLE "blocklist" RENAME TO "temporary_blocklist"`
);
await queryRunner.query(
`CREATE TABLE "blocklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "blocklistedTags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "FK_5c8af2d0e83b3be6d250eccc19d" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_356721a49f145aa439c16e6b999" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "blocklist"("id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "blocklistedTags", "createdAt", "userId", "mediaId" FROM "temporary_blocklist"`
);
await queryRunner.query(`DROP TABLE "temporary_blocklist"`);
await queryRunner.query(
`CREATE INDEX "IDX_09b94c932e84635c5461f3c0a9" ON "blocklist" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_356721a49f145aa439c16e6b99" ON "blocklist" ("userId") `
);
await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`);
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(
`CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") `
);
}
}

View File

@@ -89,18 +89,29 @@ blocklistRoutes.get(
type: 'or',
}),
async (req, res, next) => {
const mediaType = req.query.mediaType;
if (mediaType !== MediaType.MOVIE && mediaType !== MediaType.TV) {
return next({
status: 400,
message: 'Invalid or missing mediaType query parameter.',
});
}
try {
const blocklisteRepository = getRepository(Blocklist);
const blocklistItem = await blocklisteRepository.findOneOrFail({
where: { tmdbId: Number(req.params.id) },
where: {
tmdbId: Number(req.params.id),
mediaType,
},
});
return res.status(200).send(blocklistItem);
} catch (e) {
if (e instanceof EntityNotFoundError) {
return next({
status: 401,
status: 404,
message: e.message,
});
}
@@ -153,11 +164,22 @@ blocklistRoutes.delete(
type: 'or',
}),
async (req, res, next) => {
const mediaType = req.query.mediaType;
if (mediaType !== MediaType.MOVIE && mediaType !== MediaType.TV) {
return next({
status: 400,
message: 'Invalid or missing mediaType query parameter.',
});
}
try {
const blocklisteRepository = getRepository(Blocklist);
const blocklistItem = await blocklisteRepository.findOneOrFail({
where: { tmdbId: Number(req.params.id) },
where: {
tmdbId: Number(req.params.id),
mediaType,
},
});
await blocklisteRepository.remove(blocklistItem);
@@ -165,7 +187,10 @@ blocklistRoutes.delete(
const mediaRepository = getRepository(Media);
const mediaItem = await mediaRepository.findOneOrFail({
where: { tmdbId: Number(req.params.id) },
where: {
tmdbId: Number(req.params.id),
mediaType: req.query.mediaType as MediaType,
},
});
await mediaRepository.remove(mediaItem);
@@ -174,7 +199,7 @@ blocklistRoutes.delete(
} catch (e) {
if (e instanceof EntityNotFoundError) {
return next({
status: 401,
status: 404,
message: e.message,
});
}

View File

@@ -1,4 +1,5 @@
import TheMovieDb from '@server/api/themoviedb';
import { MediaType } from '@server/constants/media';
import Media from '@server/entity/Media';
import logger from '@server/logger';
import { mapCollection } from '@server/models/Collection';
@@ -17,7 +18,10 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
collection.parts.map((part) => part.id)
collection.parts.map((part) => ({
tmdbId: part.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json(mapCollection(collection, media));

View File

@@ -124,7 +124,10 @@ discoverRoutes.get('/movies', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
let keywordData: TmdbKeyword[] = [];
@@ -193,7 +196,10 @@ discoverRoutes.get<{ language: string }>(
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json({
@@ -251,7 +257,10 @@ discoverRoutes.get<{ genreId: string }>(
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json({
@@ -299,7 +308,10 @@ discoverRoutes.get<{ studioId: string }>(
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json({
@@ -349,7 +361,10 @@ discoverRoutes.get('/movies/upcoming', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json({
@@ -417,7 +432,10 @@ discoverRoutes.get('/tv', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.TV,
}))
);
let keywordData: TmdbKeyword[] = [];
@@ -485,7 +503,10 @@ discoverRoutes.get<{ language: string }>(
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.TV,
}))
);
return res.status(200).json({
@@ -543,7 +564,10 @@ discoverRoutes.get<{ genreId: string }>(
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.TV,
}))
);
return res.status(200).json({
@@ -591,7 +615,10 @@ discoverRoutes.get<{ networkId: string }>(
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.TV,
}))
);
return res.status(200).json({
@@ -641,7 +668,10 @@ discoverRoutes.get('/tv/upcoming', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.TV,
}))
);
return res.status(200).json({
@@ -711,7 +741,10 @@ discoverRoutes.get('/trending', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: isMovie(result) ? MediaType.MOVIE : MediaType.TV,
}))
);
return res.status(200).json({
@@ -755,7 +788,10 @@ discoverRoutes.get<{ keywordId: string }>(
const media = await Media.getRelatedMedia(
req.user,
data.results.map((result) => result.id)
data.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json({

View File

@@ -27,6 +27,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
const onUserWatchlist = await getRepository(Watchlist).exist({
where: {
tmdbId: Number(req.params.id),
mediaType: MediaType.MOVIE,
requestedBy: {
id: req.user?.id,
},
@@ -67,7 +68,10 @@ movieRoutes.get('/:id/recommendations', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
results.results.map((result) => result.id)
results.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json({
@@ -109,7 +113,10 @@ movieRoutes.get('/:id/similar', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
results.results.map((result) => result.id)
results.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.MOVIE,
}))
);
return res.status(200).json({

View File

@@ -43,12 +43,22 @@ personRoutes.get('/:id/combined_credits', async (req, res, next) => {
const castMedia = await Media.getRelatedMedia(
req.user,
combinedCredits.cast.map((result) => result.id)
combinedCredits.cast
.filter((result) => result.media_type)
.map((result) => ({
tmdbId: result.id,
mediaType: result.media_type!,
}))
);
const crewMedia = await Media.getRelatedMedia(
req.user,
combinedCredits.crew.map((result) => result.id)
combinedCredits.crew
.filter((result) => result.media_type)
.map((result) => ({
tmdbId: result.id,
mediaType: result.media_type!,
}))
);
return res.status(200).json({

View File

@@ -35,7 +35,10 @@ searchRoutes.get('/', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
results.results.map((result) => result.id)
results.results.map((result) => ({
tmdbId: result.id,
mediaType: result.media_type,
}))
);
return res.status(200).json({

View File

@@ -35,6 +35,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
const onUserWatchlist = await getRepository(Watchlist).exist({
where: {
tmdbId: Number(req.params.id),
mediaType: MediaType.TV,
requestedBy: {
id: req.user?.id,
},
@@ -110,7 +111,10 @@ tvRoutes.get('/:id/recommendations', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
results.results.map((result) => result.id)
results.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.TV,
}))
);
return res.status(200).json({
@@ -151,7 +155,10 @@ tvRoutes.get('/:id/similar', async (req, res, next) => {
const media = await Media.getRelatedMedia(
req.user,
results.results.map((result) => result.id)
results.results.map((result) => ({
tmdbId: result.id,
mediaType: MediaType.TV,
}))
);
return res.status(200).json({

View File

@@ -7,6 +7,7 @@ import logger from '@server/logger';
import { Router } from 'express';
import { QueryFailedError } from 'typeorm';
import { MediaType } from '@server/constants/media';
import { watchlistCreate } from '@server/interfaces/api/watchlistCreate';
const watchlistRoutes = Router();
@@ -57,12 +58,24 @@ watchlistRoutes.delete('/:tmdbId', async (req, res, next) => {
});
}
try {
await Watchlist.deleteWatchlist(Number(req.params.tmdbId), req.user);
const mediaType = req.query.mediaType;
if (mediaType !== MediaType.MOVIE && mediaType !== MediaType.TV) {
return next({
status: 400,
message: 'Invalid mediaType query parameter.',
});
}
await Watchlist.deleteWatchlist(
Number(req.params.tmdbId),
mediaType,
req.user
);
return res.status(204).send();
} catch (e) {
if (e instanceof NotFoundError) {
return next({
status: 401,
status: 404,
message: e.message,
});
}