mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
* chore(release): 1.4.0 * chore(release): 1.4.1 * chore(release): 1.5.0 * chore(release): 1.6.0 * chore(release): 1.7.0 * feat: support for postgresql * test(pgsql): disable root certificate verification * test(ci): temporarily change CI for local repo * fix: don't use SQLite idiom when using PgSQL * feat(db): add flag to toggle TLS for Postgres * feat(postgres and migrations): added migrations for postgres & imporved ssl for postgres config #186 * fix: restored workflow actions * fix: access order * fix: added pushover sound migration tto initial migration * fix: added option to log queries * fix: issue with session migration * chore: relocate pushover sound migration * feat: added logging option to other datasources * chore: small tweaks for the datasource. Added docs for db setup * chore: cleanup logs * fix: added default dates to postgres migration * fix: removed psql specific relation checks * chore: added some debug sanity checks * chore: added some more debug sanity checks * chore: added some more additional debug sanity checks * chore: added some more+ additional debug sanity checks * chore: mild log cleanup * chore: more log cleanup * chore: finish log cleanup * fix: added not null to migration so typeorm doesn't delete ids * chore: cleanup extra psql code * fix: remove eager load * docs: added documentation for migration to postgres * docs: added database option to bug template * feat: created docker-compose postgres file * fix: updated ts schema to align with change to migration * fix: switch timestamp to include timezone * fix: fixed indentation in psql docker-compose * fix: changed version to 0.1.0 to remove ui notification * style: fixed prettier in docker-compose.pastgres.yaml * chore: restored CHANGELOG.md * chore: revverted ts commit * fix: update pnpm lock with pg package * chore(pnpm-lock.yaml): updated pnpm-lock * docs: update docs to add psql set up info * refactor: clean up code from cr comments * feat: migrate blacklist * fix: fix issue with cypress tests * docs: update psql docs * fix: fix psql issue in user page; fix tiny psql error when selecting by empty list * fix: incorrect current date function * fix: null contraint with mediaAddedAt; fix psql col type * refactor: removed unnecessary import * feat: add postgres migration for streaming region --------- Co-authored-by: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net> Co-authored-by: zackhow <zackhow@gmail.com> Co-authored-by: Ryan Algar <me@ralgar.dev> Co-authored-by: Ryan Algar <59636191+ralgar@users.noreply.github.com>
377 lines
11 KiB
TypeScript
377 lines
11 KiB
TypeScript
import RadarrAPI from '@server/api/servarr/radarr';
|
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
|
import { MediaStatus, MediaType } from '@server/constants/media';
|
|
import { MediaServerType } from '@server/constants/server';
|
|
import { getRepository } from '@server/datasource';
|
|
import { Blacklist } from '@server/entity/Blacklist';
|
|
import type { User } from '@server/entity/User';
|
|
import { Watchlist } from '@server/entity/Watchlist';
|
|
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
|
import downloadTracker from '@server/lib/downloadtracker';
|
|
import { getSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import { DbAwareColumn } from '@server/utils/DbColumnHelper';
|
|
import { getHostname } from '@server/utils/getHostname';
|
|
import {
|
|
AfterLoad,
|
|
Column,
|
|
CreateDateColumn,
|
|
Entity,
|
|
Index,
|
|
OneToMany,
|
|
OneToOne,
|
|
PrimaryGeneratedColumn,
|
|
UpdateDateColumn,
|
|
} from 'typeorm';
|
|
import Issue from './Issue';
|
|
import { MediaRequest } from './MediaRequest';
|
|
import Season from './Season';
|
|
|
|
@Entity()
|
|
class Media {
|
|
public static async getRelatedMedia(
|
|
user: User | undefined,
|
|
tmdbIds: number | number[]
|
|
): Promise<Media[]> {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
try {
|
|
let finalIds: number[];
|
|
if (!Array.isArray(tmdbIds)) {
|
|
finalIds = [tmdbIds];
|
|
} else {
|
|
finalIds = tmdbIds;
|
|
}
|
|
|
|
if (finalIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const media = await mediaRepository
|
|
.createQueryBuilder('media')
|
|
.leftJoinAndSelect(
|
|
'media.watchlists',
|
|
'watchlist',
|
|
'media.id= watchlist.media and watchlist.requestedBy = :userId',
|
|
{ userId: user?.id }
|
|
) //,
|
|
.where(' media.tmdbId in (:...finalIds)', { finalIds })
|
|
.getMany();
|
|
|
|
return media;
|
|
} catch (e) {
|
|
logger.error(e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public static async getMedia(
|
|
id: number,
|
|
mediaType: MediaType
|
|
): Promise<Media | undefined> {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
try {
|
|
const media = await mediaRepository.findOne({
|
|
where: { tmdbId: id, mediaType: mediaType },
|
|
relations: { requests: true, issues: true },
|
|
});
|
|
|
|
return media ?? undefined;
|
|
} catch (e) {
|
|
logger.error(e.message);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
@PrimaryGeneratedColumn()
|
|
public id: number;
|
|
|
|
@Column({ type: 'varchar' })
|
|
public mediaType: MediaType;
|
|
|
|
@Column()
|
|
@Index()
|
|
public tmdbId: number;
|
|
|
|
@Column({ unique: true, nullable: true })
|
|
@Index()
|
|
public tvdbId?: number;
|
|
|
|
@Column({ nullable: true })
|
|
@Index()
|
|
public imdbId?: string;
|
|
|
|
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
|
public status: MediaStatus;
|
|
|
|
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
|
public status4k: MediaStatus;
|
|
|
|
@OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
|
|
public requests: MediaRequest[];
|
|
|
|
@OneToMany(() => Watchlist, (watchlist) => watchlist.media)
|
|
public watchlists: null | Watchlist[];
|
|
|
|
@OneToMany(() => Season, (season) => season.media, {
|
|
cascade: true,
|
|
eager: true,
|
|
})
|
|
public seasons: Season[];
|
|
|
|
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
|
|
public issues: Issue[];
|
|
|
|
@OneToOne(() => Blacklist, (blacklist) => blacklist.media)
|
|
public blacklist: Promise<Blacklist>;
|
|
|
|
@CreateDateColumn()
|
|
public createdAt: Date;
|
|
|
|
@UpdateDateColumn()
|
|
public updatedAt: Date;
|
|
|
|
/**
|
|
* The `lastSeasonChange` column stores the date and time when the media was added to the library.
|
|
* It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`.
|
|
*/
|
|
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
|
public lastSeasonChange: Date;
|
|
|
|
/**
|
|
* The `mediaAddedAt` column stores the date and time when the media was added to the library.
|
|
* It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`.
|
|
* This column is nullable because it can be null when the media is not yet synced to the library.
|
|
*/
|
|
@DbAwareColumn({
|
|
type: 'datetime',
|
|
default: () => 'CURRENT_TIMESTAMP',
|
|
nullable: true,
|
|
})
|
|
public mediaAddedAt: Date;
|
|
|
|
@Column({ nullable: true, type: 'int' })
|
|
public serviceId?: number | null;
|
|
|
|
@Column({ nullable: true, type: 'int' })
|
|
public serviceId4k?: number | null;
|
|
|
|
@Column({ nullable: true, type: 'int' })
|
|
public externalServiceId?: number | null;
|
|
|
|
@Column({ nullable: true, type: 'int' })
|
|
public externalServiceId4k?: number | null;
|
|
|
|
@Column({ nullable: true, type: 'varchar' })
|
|
public externalServiceSlug?: string | null;
|
|
|
|
@Column({ nullable: true, type: 'varchar' })
|
|
public externalServiceSlug4k?: string | null;
|
|
|
|
@Column({ nullable: true, type: 'varchar' })
|
|
public ratingKey?: string | null;
|
|
|
|
@Column({ nullable: true, type: 'varchar' })
|
|
public ratingKey4k?: string | null;
|
|
|
|
@Column({ nullable: true, type: 'varchar' })
|
|
public jellyfinMediaId?: string | null;
|
|
|
|
@Column({ nullable: true, type: 'varchar' })
|
|
public jellyfinMediaId4k?: string | null;
|
|
|
|
public serviceUrl?: string;
|
|
public serviceUrl4k?: string;
|
|
public downloadStatus?: DownloadingItem[] = [];
|
|
public downloadStatus4k?: DownloadingItem[] = [];
|
|
|
|
public mediaUrl?: string;
|
|
public mediaUrl4k?: string;
|
|
|
|
public iOSPlexUrl?: string;
|
|
public iOSPlexUrl4k?: string;
|
|
|
|
public tautulliUrl?: string;
|
|
public tautulliUrl4k?: string;
|
|
|
|
constructor(init?: Partial<Media>) {
|
|
Object.assign(this, init);
|
|
}
|
|
|
|
@AfterLoad()
|
|
public setPlexUrls(): void {
|
|
const { machineId, webAppUrl } = getSettings().plex;
|
|
const { externalUrl: tautulliUrl } = getSettings().tautulli;
|
|
|
|
if (getSettings().main.mediaServerType == MediaServerType.PLEX) {
|
|
if (this.ratingKey) {
|
|
this.mediaUrl = `${
|
|
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
|
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
|
this.ratingKey
|
|
}`;
|
|
|
|
this.iOSPlexUrl = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey}&server=${machineId}`;
|
|
|
|
if (tautulliUrl) {
|
|
this.tautulliUrl = `${tautulliUrl}/info?rating_key=${this.ratingKey}`;
|
|
}
|
|
|
|
if (this.ratingKey4k) {
|
|
this.mediaUrl4k = `${
|
|
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
|
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
|
this.ratingKey4k
|
|
}`;
|
|
|
|
this.iOSPlexUrl4k = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}&server=${machineId}`;
|
|
|
|
if (tautulliUrl) {
|
|
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const pageName =
|
|
getSettings().main.mediaServerType == MediaServerType.EMBY
|
|
? 'item'
|
|
: 'details';
|
|
const { serverId, externalHostname } = getSettings().jellyfin;
|
|
const jellyfinHost =
|
|
externalHostname && externalHostname.length > 0
|
|
? externalHostname
|
|
: getHostname();
|
|
|
|
if (this.jellyfinMediaId) {
|
|
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
|
|
}
|
|
if (this.jellyfinMediaId4k) {
|
|
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
@AfterLoad()
|
|
public setServiceUrl(): void {
|
|
if (this.mediaType === MediaType.MOVIE) {
|
|
if (this.serviceId !== null && this.externalServiceSlug !== null) {
|
|
const settings = getSettings();
|
|
const server = settings.radarr.find(
|
|
(radarr) => radarr.id === this.serviceId
|
|
);
|
|
|
|
if (server) {
|
|
this.serviceUrl = server.externalUrl
|
|
? `${server.externalUrl}/movie/${this.externalServiceSlug}`
|
|
: RadarrAPI.buildUrl(server, `/movie/${this.externalServiceSlug}`);
|
|
}
|
|
}
|
|
|
|
if (this.serviceId4k !== null && this.externalServiceSlug4k !== null) {
|
|
const settings = getSettings();
|
|
const server = settings.radarr.find(
|
|
(radarr) => radarr.id === this.serviceId4k
|
|
);
|
|
|
|
if (server) {
|
|
this.serviceUrl4k = server.externalUrl
|
|
? `${server.externalUrl}/movie/${this.externalServiceSlug4k}`
|
|
: RadarrAPI.buildUrl(
|
|
server,
|
|
`/movie/${this.externalServiceSlug4k}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.mediaType === MediaType.TV) {
|
|
if (this.serviceId !== null && this.externalServiceSlug !== null) {
|
|
const settings = getSettings();
|
|
const server = settings.sonarr.find(
|
|
(sonarr) => sonarr.id === this.serviceId
|
|
);
|
|
|
|
if (server) {
|
|
this.serviceUrl = server.externalUrl
|
|
? `${server.externalUrl}/series/${this.externalServiceSlug}`
|
|
: SonarrAPI.buildUrl(server, `/series/${this.externalServiceSlug}`);
|
|
}
|
|
}
|
|
|
|
if (this.serviceId4k !== null && this.externalServiceSlug4k !== null) {
|
|
const settings = getSettings();
|
|
const server = settings.sonarr.find(
|
|
(sonarr) => sonarr.id === this.serviceId4k
|
|
);
|
|
|
|
if (server) {
|
|
this.serviceUrl4k = server.externalUrl
|
|
? `${server.externalUrl}/series/${this.externalServiceSlug4k}`
|
|
: SonarrAPI.buildUrl(
|
|
server,
|
|
`/series/${this.externalServiceSlug4k}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@AfterLoad()
|
|
public getDownloadingItem(): void {
|
|
if (this.mediaType === MediaType.MOVIE) {
|
|
if (
|
|
this.externalServiceId !== undefined &&
|
|
this.externalServiceId !== null &&
|
|
this.serviceId !== undefined &&
|
|
this.serviceId !== null
|
|
) {
|
|
this.downloadStatus = downloadTracker.getMovieProgress(
|
|
this.serviceId,
|
|
this.externalServiceId
|
|
);
|
|
}
|
|
|
|
if (
|
|
this.externalServiceId4k !== undefined &&
|
|
this.externalServiceId4k !== null &&
|
|
this.serviceId4k !== undefined &&
|
|
this.serviceId4k !== null
|
|
) {
|
|
this.downloadStatus4k = downloadTracker.getMovieProgress(
|
|
this.serviceId4k,
|
|
this.externalServiceId4k
|
|
);
|
|
}
|
|
}
|
|
|
|
if (this.mediaType === MediaType.TV) {
|
|
if (
|
|
this.externalServiceId !== undefined &&
|
|
this.externalServiceId !== null &&
|
|
this.serviceId !== undefined &&
|
|
this.serviceId !== null
|
|
) {
|
|
this.downloadStatus = downloadTracker.getSeriesProgress(
|
|
this.serviceId,
|
|
this.externalServiceId
|
|
);
|
|
}
|
|
|
|
if (
|
|
this.externalServiceId4k !== undefined &&
|
|
this.externalServiceId4k !== null &&
|
|
this.serviceId4k !== undefined &&
|
|
this.serviceId4k !== null
|
|
) {
|
|
this.downloadStatus4k = downloadTracker.getSeriesProgress(
|
|
this.serviceId4k,
|
|
this.externalServiceId4k
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Media;
|