-
-
Notifications
You must be signed in to change notification settings - Fork 297
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #374 from yalagin/add-watchlist
feat(watchlist): Add media to watchlist
- Loading branch information
Showing
31 changed files
with
566 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import TheMovieDb from '@server/api/themoviedb'; | ||
import { MediaType } from '@server/constants/media'; | ||
import { getRepository } from '@server/datasource'; | ||
import Media from '@server/entity/Media'; | ||
import { User } from '@server/entity/User'; | ||
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces'; | ||
import logger from '@server/logger'; | ||
import { | ||
Column, | ||
CreateDateColumn, | ||
Entity, | ||
Index, | ||
ManyToOne, | ||
PrimaryGeneratedColumn, | ||
Unique, | ||
UpdateDateColumn, | ||
} from 'typeorm'; | ||
import type { ZodNumber, ZodOptional, ZodString } from 'zod'; | ||
|
||
export class DuplicateWatchlistRequestError extends Error {} | ||
export class NotFoundError extends Error { | ||
constructor(message = 'Not found') { | ||
super(message); | ||
this.name = 'NotFoundError'; | ||
} | ||
} | ||
|
||
@Entity() | ||
@Unique('UNIQUE_USER_DB', ['tmdbId', 'requestedBy']) | ||
export class Watchlist implements WatchlistItem { | ||
@PrimaryGeneratedColumn() | ||
id: number; | ||
|
||
@Column({ type: 'varchar' }) | ||
public ratingKey = ''; | ||
|
||
@Column({ type: 'varchar' }) | ||
public mediaType: MediaType; | ||
|
||
@Column({ type: 'varchar' }) | ||
title = ''; | ||
|
||
@Column() | ||
@Index() | ||
public tmdbId: number; | ||
|
||
@ManyToOne(() => User, (user) => user.watchlists, { | ||
eager: true, | ||
onDelete: 'CASCADE', | ||
}) | ||
public requestedBy: User; | ||
|
||
@ManyToOne(() => Media, (media) => media.watchlists, { | ||
eager: true, | ||
onDelete: 'CASCADE', | ||
}) | ||
public media: Media; | ||
|
||
@CreateDateColumn() | ||
public createdAt: Date; | ||
|
||
@UpdateDateColumn() | ||
public updatedAt: Date; | ||
|
||
constructor(init?: Partial<Watchlist>) { | ||
Object.assign(this, init); | ||
} | ||
|
||
public static async createWatchlist({ | ||
watchlistRequest, | ||
user, | ||
}: { | ||
watchlistRequest: { | ||
mediaType: MediaType; | ||
ratingKey?: ZodOptional<ZodString>['_output']; | ||
title?: ZodOptional<ZodString>['_output']; | ||
tmdbId: ZodNumber['_output']; | ||
}; | ||
user: User; | ||
}): Promise<Watchlist> { | ||
const watchlistRepository = getRepository(this); | ||
const mediaRepository = getRepository(Media); | ||
const tmdb = new TheMovieDb(); | ||
|
||
const tmdbMedia = | ||
watchlistRequest.mediaType === MediaType.MOVIE | ||
? await tmdb.getMovie({ movieId: watchlistRequest.tmdbId }) | ||
: await tmdb.getTvShow({ tvId: watchlistRequest.tmdbId }); | ||
|
||
const existing = await watchlistRepository | ||
.createQueryBuilder('watchlist') | ||
.leftJoinAndSelect('watchlist.requestedBy', 'user') | ||
.where('user.id = :userId', { userId: user.id }) | ||
.andWhere('watchlist.tmdbId = :tmdbId', { | ||
tmdbId: watchlistRequest.tmdbId, | ||
}) | ||
.andWhere('watchlist.mediaType = :mediaType', { | ||
mediaType: watchlistRequest.mediaType, | ||
}) | ||
.getMany(); | ||
|
||
if (existing && existing.length > 0) { | ||
logger.warn('Duplicate request for watchlist blocked', { | ||
tmdbId: watchlistRequest.tmdbId, | ||
mediaType: watchlistRequest.mediaType, | ||
label: 'Watchlist', | ||
}); | ||
|
||
throw new DuplicateWatchlistRequestError(); | ||
} | ||
|
||
let media = await mediaRepository.findOne({ | ||
where: { | ||
tmdbId: watchlistRequest.tmdbId, | ||
mediaType: watchlistRequest.mediaType, | ||
}, | ||
}); | ||
|
||
if (!media) { | ||
media = new Media({ | ||
tmdbId: tmdbMedia.id, | ||
tvdbId: tmdbMedia.external_ids.tvdb_id, | ||
mediaType: watchlistRequest.mediaType, | ||
}); | ||
} | ||
|
||
const watchlist = new this({ | ||
...watchlistRequest, | ||
requestedBy: user, | ||
media, | ||
}); | ||
|
||
await mediaRepository.save(media); | ||
await watchlistRepository.save(watchlist); | ||
return watchlist; | ||
} | ||
|
||
public static async deleteWatchlist( | ||
tmdbId: Watchlist['tmdbId'], | ||
user: User | ||
): Promise<Watchlist | null> { | ||
const watchlistRepository = getRepository(this); | ||
const watchlist = await watchlistRepository.findOneBy({ | ||
tmdbId, | ||
requestedBy: { id: user.id }, | ||
}); | ||
if (!watchlist) { | ||
throw new NotFoundError('not Found'); | ||
} | ||
|
||
if (watchlist) { | ||
await watchlistRepository.delete(watchlist.id); | ||
} | ||
|
||
return watchlist; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { MediaType } from '@server/constants/media'; | ||
import { z } from 'zod'; | ||
|
||
export const watchlistCreate = z.object({ | ||
ratingKey: z.coerce.string().optional(), | ||
tmdbId: z.coerce.number(), | ||
mediaType: z.nativeEnum(MediaType), | ||
title: z.coerce.string().optional(), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { MigrationInterface, QueryRunner } from 'typeorm'; | ||
|
||
export class AddWatchlists1682608634546 implements MigrationInterface { | ||
name = 'AddWatchlists1682608634546'; | ||
|
||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query( | ||
`CREATE TABLE "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 (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"))` | ||
); | ||
await queryRunner.query( | ||
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") ` | ||
); | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(`DROP INDEX "IDX_939f205946256cc0d2a1ac51a8"`); | ||
await queryRunner.query(`DROP TABLE "watchlist"`); | ||
} | ||
} |
Oops, something went wrong.