mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
* fix(entity): use TIMESTAMPTZ in Postgres and sort issue comments oldest-first Switch key datetime columns to TIMESTAMPTZ for proper UTC handling (“just now”) and sort issue comments on load so the original description stays first and in proper sorted order in fix-pgsql-timezone fixes #1569, fixes #1568 * refactor(migration): manually rewrite pgsql migration to preserve existing data Typeorm generated migration was dropping the entire column thus leading to data loss so this is an attempt at manually rewriting the migration to preserve the data * refactor(migrations): rename to be consistent with other migration files * fix: use id to order instead of createdAt to avoid non-existant createdAt --------- Co-authored-by: Gauthier <mail@gauthierth.fr>
161 lines
4.1 KiB
TypeScript
161 lines
4.1 KiB
TypeScript
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 { DbAwareColumn } from '@server/utils/DbColumnHelper';
|
|
import {
|
|
Column,
|
|
Entity,
|
|
Index,
|
|
ManyToOne,
|
|
PrimaryGeneratedColumn,
|
|
Unique,
|
|
} 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;
|
|
|
|
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
|
public createdAt: Date;
|
|
|
|
@DbAwareColumn({
|
|
type: 'datetime',
|
|
default: () => 'CURRENT_TIMESTAMP',
|
|
onUpdate: 'CURRENT_TIMESTAMP',
|
|
})
|
|
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;
|
|
}
|
|
}
|