fix: allow only one auto request per media item per-user

feature/watchlist-sync
Ryan Cohen 3 years ago
parent 0bb6c65c28
commit fa50e3ccb7

@ -34,11 +34,16 @@ export class QuotaRestrictedError extends Error {}
export class DuplicateMediaRequestError extends Error {} export class DuplicateMediaRequestError extends Error {}
export class NoSeasonsAvailableError extends Error {} export class NoSeasonsAvailableError extends Error {}
type MediaRequestOptions = {
isAutoRequest?: boolean;
};
@Entity() @Entity()
export class MediaRequest { export class MediaRequest {
public static async request( public static async request(
requestBody: MediaRequestBody, requestBody: MediaRequestBody,
user: User user: User,
options: MediaRequestOptions = {}
): Promise<MediaRequest> { ): Promise<MediaRequest> {
const tmdb = new TheMovieDb(); const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media); const mediaRepository = getRepository(Media);
@ -140,21 +145,23 @@ export class MediaRequest {
} }
} }
if (requestBody.mediaType === MediaType.MOVIE) { const existing = await requestRepository
const existing = await requestRepository .createQueryBuilder('request')
.createQueryBuilder('request') .leftJoin('request.media', 'media')
.leftJoin('request.media', 'media') .leftJoinAndSelect('request.requestedBy', 'user')
.where('request.is4k = :is4k', { is4k: requestBody.is4k }) .where('request.is4k = :is4k', { is4k: requestBody.is4k })
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id }) .andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
.andWhere('media.mediaType = :mediaType', { .andWhere('media.mediaType = :mediaType', {
mediaType: MediaType.MOVIE, mediaType: requestBody.mediaType,
}) })
.andWhere('request.status != :requestStatus', { .getMany();
requestStatus: MediaRequestStatus.DECLINED,
}) if (existing && existing.length > 0) {
.getOne(); // If there is an existing movie request that isn't declined, don't allow a new one.
if (
if (existing) { requestBody.mediaType === MediaType.MOVIE &&
existing[0].status !== MediaRequestStatus.DECLINED
) {
logger.warn('Duplicate request for media blocked', { logger.warn('Duplicate request for media blocked', {
tmdbId: tmdbMedia.id, tmdbId: tmdbMedia.id,
mediaType: requestBody.mediaType, mediaType: requestBody.mediaType,
@ -167,6 +174,20 @@ export class MediaRequest {
); );
} }
// If an existing auto-request for this media exists from the same user,
// don't allow a new one.
if (
existing.find(
(r) => r.requestedBy.id === requestUser.id && r.isAutoRequest
)
) {
throw new DuplicateMediaRequestError(
'Auto-request for this media and user already exists.'
);
}
}
if (requestBody.mediaType === MediaType.MOVIE) {
await mediaRepository.save(media); await mediaRepository.save(media);
const request = new MediaRequest({ const request = new MediaRequest({
@ -207,6 +228,7 @@ export class MediaRequest {
profileId: requestBody.profileId, profileId: requestBody.profileId,
rootFolder: requestBody.rootFolder, rootFolder: requestBody.rootFolder,
tags: requestBody.tags, tags: requestBody.tags,
isAutoRequest: options.isAutoRequest ?? false,
}); });
await requestRepository.save(request); await requestRepository.save(request);
@ -330,6 +352,7 @@ export class MediaRequest {
: MediaRequestStatus.PENDING, : MediaRequestStatus.PENDING,
}) })
), ),
isAutoRequest: options.isAutoRequest ?? false,
}); });
await requestRepository.save(request); await requestRepository.save(request);
@ -427,6 +450,9 @@ export class MediaRequest {
}) })
public tags?: number[]; public tags?: number[];
@Column({ default: false })
public isAutoRequest: boolean;
constructor(init?: Partial<MediaRequest>) { constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init); Object.assign(this, init);
} }

@ -121,7 +121,8 @@ class WatchlistSync {
tvdbId: mediaItem.tvdbId, tvdbId: mediaItem.tvdbId,
is4k: false, is4k: false,
}, },
user user,
{ isAutoRequest: true }
); );
} catch (e) { } catch (e) {
if (!(e instanceof Error)) { if (!(e instanceof Error)) {

@ -0,0 +1,33 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddMediaRequestIsAutoRequestedField1660714479373
implements MigrationInterface
{
name = 'AddMediaRequestIsAutoRequestedField1660714479373';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
}
}
Loading…
Cancel
Save