feat: 4K Requests (#559)

This commit is contained in:
sct
2021-01-11 23:42:33 +09:00
committed by GitHub
parent 79629645aa
commit 6b2df24a2e
30 changed files with 1384 additions and 467 deletions

View File

@@ -48,6 +48,23 @@ export interface PlexMetadata {
parentIndex?: number;
leafCount: number;
viewedLeafCount: number;
Media: Media[];
}
interface Media {
id: number;
duration: number;
bitrate: number;
width: number;
height: number;
aspectRatio: number;
audioChannels: number;
audioCodec: string;
videoCodec: string;
videoResolution: string;
container: string;
videoFrameRate: string;
videoProfile: string;
}
interface PlexMetadataResponse {

View File

@@ -80,6 +80,9 @@ class Media {
@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[];

View File

@@ -65,6 +65,18 @@ export class MediaRequest {
})
public seasons: SeasonRequest[];
@Column({ default: false })
public is4k: boolean;
@Column({ nullable: true })
public serverId: number;
@Column({ nullable: true })
public profileId: number;
@Column({ nullable: true })
public rootFolder: string;
constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}
@@ -181,7 +193,11 @@ export class MediaRequest {
}
const seasonRequestRepository = getRepository(SeasonRequest);
if (this.status === MediaRequestStatus.APPROVED) {
media.status = MediaStatus.PROCESSING;
if (this.is4k) {
media.status4k = MediaStatus.PROCESSING;
} else {
media.status = MediaStatus.PROCESSING;
}
mediaRepository.save(media);
}
@@ -189,7 +205,11 @@ export class MediaRequest {
this.media.mediaType === MediaType.MOVIE &&
this.status === MediaRequestStatus.DECLINED
) {
media.status = MediaStatus.UNKNOWN;
if (this.is4k) {
media.status4k = MediaStatus.UNKNOWN;
} else {
media.status = MediaStatus.UNKNOWN;
}
mediaRepository.save(media);
}
@@ -224,15 +244,28 @@ export class MediaRequest {
}
@AfterRemove()
private async _handleRemoveParentUpdate() {
public async handleRemoveParentUpdate(): Promise<void> {
const mediaRepository = getRepository(Media);
const fullMedia = await mediaRepository.findOneOrFail({
where: { id: this.media.id },
relations: ['requests'],
});
if (!fullMedia.requests || fullMedia.requests.length === 0) {
if (
!fullMedia.requests.some((request) => !request.is4k) &&
fullMedia.status !== MediaStatus.AVAILABLE
) {
fullMedia.status = MediaStatus.UNKNOWN;
mediaRepository.save(fullMedia);
}
if (
!fullMedia.requests.some((request) => request.is4k) &&
fullMedia.status4k !== MediaStatus.AVAILABLE
) {
fullMedia.status4k = MediaStatus.UNKNOWN;
}
mediaRepository.save(fullMedia);
}
private async _sendToRadarr() {
@@ -252,12 +285,14 @@ export class MediaRequest {
}
const radarrSettings = settings.radarr.find(
(radarr) => radarr.isDefault && !radarr.is4k
(radarr) => radarr.isDefault && this.is4k
);
if (!radarrSettings) {
logger.info(
'There is no default radarr configured. Did you set any of your Radarr servers as default?',
`There is no default ${
this.is4k ? '4K ' : ''
}radarr configured. Did you set any of your Radarr servers as default?`,
{ label: 'Media Request' }
);
return;
@@ -342,12 +377,14 @@ export class MediaRequest {
}
const sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && !sonarr.is4k
(sonarr) => sonarr.isDefault && this.is4k
);
if (!sonarrSettings) {
logger.info(
'There is no default sonarr configured. Did you set any of your Sonarr servers as default?',
`There is no default ${
this.is4k ? '4K ' : ''
}sonarr configured. Did you set any of your Sonarr servers as default?`,
{ label: 'Media Request' }
);
return;

View File

@@ -20,6 +20,9 @@ class Season {
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status: MediaStatus;
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status4k: MediaStatus;
@ManyToOne(() => Media, (media) => media.seasons, { onDelete: 'CASCADE' })
public media: Promise<Media>;

View File

@@ -98,6 +98,7 @@ app
};
next();
});
server.use('/api/v1', routes);
server.get('*', (req, res) => handle(req, res));
server.use(

View File

@@ -4,3 +4,9 @@ export interface SettingsAboutResponse {
totalMediaItems: number;
tz?: string;
}
export interface PublicSettingsResponse {
initialized: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
}

View File

@@ -45,6 +45,8 @@ class JobPlexSync {
private currentLibrary: Library;
private running = false;
private isRecentOnly = false;
private enable4kMovie = false;
private enable4kShow = false;
private asyncLock = new AsyncLock();
constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) {
@@ -86,23 +88,59 @@ class JobPlexSync {
}
});
const has4k = metadata.Media.some(
(media) => media.videoResolution === '4k'
);
const hasOtherResolution = metadata.Media.some(
(media) => media.videoResolution !== '4k'
);
await this.asyncLock.dispatch(newMedia.tmdbId, async () => {
const existing = await this.getExisting(
newMedia.tmdbId,
MediaType.MOVIE
);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${metadata.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
mediaRepository.save(existing);
this.log(
`Request for ${metadata.title} exists. Setting status AVAILABLE`,
'info'
);
if (existing) {
let changedExisting = false;
if (
(hasOtherResolution || (!this.enable4kMovie && has4k)) &&
existing.status !== MediaStatus.AVAILABLE
) {
existing.status = MediaStatus.AVAILABLE;
changedExisting = true;
}
if (
has4k &&
this.enable4kMovie &&
existing.status4k !== MediaStatus.AVAILABLE
) {
existing.status4k = MediaStatus.AVAILABLE;
changedExisting = true;
}
if (changedExisting) {
await mediaRepository.save(existing);
this.log(
`Request for ${metadata.title} exists. New media types set to AVAILABLE`,
'info'
);
} else {
this.log(
`Title already exists and no new media types found ${metadata.title}`
);
}
} else {
newMedia.status = MediaStatus.AVAILABLE;
newMedia.status =
hasOtherResolution || (!this.enable4kMovie && has4k)
? MediaStatus.AVAILABLE
: MediaStatus.UNKNOWN;
newMedia.status4k =
has4k && this.enable4kMovie
? MediaStatus.AVAILABLE
: MediaStatus.UNKNOWN;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${plexitem.title}`);
@@ -150,16 +188,47 @@ class JobPlexSync {
const mediaRepository = getRepository(Media);
await this.asyncLock.dispatch(tmdbMovieId, async () => {
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
const existing = await this.getExisting(tmdbMovieId, MediaType.MOVIE);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${plexitem.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
await mediaRepository.save(existing);
this.log(
`Request for ${plexitem.title} exists. Setting status AVAILABLE`,
'info'
);
const has4k = metadata.Media.some(
(media) => media.videoResolution === '4k'
);
const hasOtherResolution = metadata.Media.some(
(media) => media.videoResolution !== '4k'
);
if (existing) {
let changedExisting = false;
if (
(hasOtherResolution || (!this.enable4kMovie && has4k)) &&
existing.status !== MediaStatus.AVAILABLE
) {
existing.status = MediaStatus.AVAILABLE;
changedExisting = true;
}
if (
has4k &&
this.enable4kMovie &&
existing.status4k !== MediaStatus.AVAILABLE
) {
existing.status4k = MediaStatus.AVAILABLE;
changedExisting = true;
}
if (changedExisting) {
await mediaRepository.save(existing);
this.log(
`Request for ${metadata.title} exists. New media types set to AVAILABLE`,
'info'
);
} else {
this.log(
`Title already exists and no new media types found ${metadata.title}`
);
}
} else {
// If we have a tmdb movie guid but it didn't already exist, only then
// do we request the movie from tmdb (to reduce api requests)
@@ -169,7 +238,14 @@ class JobPlexSync {
const newMedia = new Media();
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
newMedia.tmdbId = tmdbMovie.id;
newMedia.status = MediaStatus.AVAILABLE;
newMedia.status =
hasOtherResolution || (!this.enable4kMovie && has4k)
? MediaStatus.AVAILABLE
: MediaStatus.UNKNOWN;
newMedia.status4k =
has4k && this.enable4kMovie
? MediaStatus.AVAILABLE
: MediaStatus.UNKNOWN;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${tmdbMovie.title}`);
@@ -316,13 +392,18 @@ class JobPlexSync {
const newSeasons: Season[] = [];
const currentSeasonAvailable = (
const currentStandardSeasonAvailable = (
media?.seasons.filter(
(season) => season.status === MediaStatus.AVAILABLE
) ?? []
).length;
const current4kSeasonAvailable = (
media?.seasons.filter(
(season) => season.status4k === MediaStatus.AVAILABLE
) ?? []
).length;
seasons.forEach((season) => {
for (const season of seasons) {
const matchedPlexSeason = metadata.Children?.Metadata.find(
(md) => Number(md.index) === season.season_number
);
@@ -332,68 +413,136 @@ class JobPlexSync {
);
// Check if we found the matching season and it has all the available episodes
if (
matchedPlexSeason &&
Number(matchedPlexSeason.leafCount) === season.episode_count
) {
if (matchedPlexSeason) {
// If we have a matched plex season, get its children metadata so we can check details
const episodes = await this.plexClient.getChildrenMetadata(
matchedPlexSeason.ratingKey
);
// Total episodes that are in standard definition (not 4k)
const totalStandard = episodes.filter((episode) =>
episode.Media.some((media) => media.videoResolution !== '4k')
).length;
// Total episodes that are in 4k
const total4k = episodes.filter((episode) =>
episode.Media.some((media) => media.videoResolution === '4k')
).length;
if (existingSeason) {
existingSeason.status = MediaStatus.AVAILABLE;
// These ternary statements look super confusing, but they are simply
// setting the status to AVAILABLE if all of a type is there, partially if some,
// and then not modifying the status if there are 0 items
existingSeason.status =
totalStandard === season.episode_count
? MediaStatus.AVAILABLE
: totalStandard > 0
? MediaStatus.PARTIALLY_AVAILABLE
: existingSeason.status;
existingSeason.status4k =
total4k === season.episode_count
? MediaStatus.AVAILABLE
: total4k > 0
? MediaStatus.PARTIALLY_AVAILABLE
: existingSeason.status4k;
} else {
newSeasons.push(
new Season({
seasonNumber: season.season_number,
status: MediaStatus.AVAILABLE,
})
);
}
} else if (matchedPlexSeason) {
if (existingSeason) {
existingSeason.status = MediaStatus.PARTIALLY_AVAILABLE;
} else {
newSeasons.push(
new Season({
seasonNumber: season.season_number,
status: MediaStatus.PARTIALLY_AVAILABLE,
// This ternary is the same as the ones above, but it just falls back to "UNKNOWN"
// if we dont have any items for the season
status:
totalStandard === season.episode_count
? MediaStatus.AVAILABLE
: totalStandard > 0
? MediaStatus.PARTIALLY_AVAILABLE
: MediaStatus.UNKNOWN,
status4k:
total4k === season.episode_count
? MediaStatus.AVAILABLE
: total4k > 0
? MediaStatus.PARTIALLY_AVAILABLE
: MediaStatus.UNKNOWN,
})
);
}
}
});
}
// Remove extras season. We dont count it for determining availability
const filteredSeasons = tvShow.seasons.filter(
(season) => season.season_number !== 0
);
const isAllSeasons =
newSeasons.length + (media?.seasons.length ?? 0) >=
const isAllStandardSeasons =
newSeasons.filter(
(season) => season.status === MediaStatus.AVAILABLE
).length +
(media?.seasons.filter(
(season) => season.status === MediaStatus.AVAILABLE
).length ?? 0) >=
filteredSeasons.length;
const isAll4kSeasons =
newSeasons.filter(
(season) => season.status4k === MediaStatus.AVAILABLE
).length +
(media?.seasons.filter(
(season) => season.status4k === MediaStatus.AVAILABLE
).length ?? 0) >=
filteredSeasons.length;
if (media) {
// Update existing
media.seasons = [...media.seasons, ...newSeasons];
const newSeasonAvailable = (
const newStandardSeasonAvailable = (
media.seasons.filter(
(season) => season.status === MediaStatus.AVAILABLE
) ?? []
).length;
const new4kSeasonAvailable = (
media.seasons.filter(
(season) => season.status4k === MediaStatus.AVAILABLE
) ?? []
).length;
// If at least one new season has become available, update
// the lastSeasonChange field so we can trigger notifications
if (newSeasonAvailable > currentSeasonAvailable) {
if (newStandardSeasonAvailable > currentStandardSeasonAvailable) {
this.log(
`Detected ${
newSeasonAvailable - currentSeasonAvailable
} new season(s) for ${tvShow.name}`,
newStandardSeasonAvailable - currentStandardSeasonAvailable
} new standard season(s) for ${tvShow.name}`,
'debug'
);
media.lastSeasonChange = new Date();
}
media.status = isAllSeasons
if (new4kSeasonAvailable > current4kSeasonAvailable) {
this.log(
`Detected ${
new4kSeasonAvailable - current4kSeasonAvailable
} new 4K season(s) for ${tvShow.name}`,
'debug'
);
media.lastSeasonChange = new Date();
}
media.status = isAllStandardSeasons
? MediaStatus.AVAILABLE
: MediaStatus.PARTIALLY_AVAILABLE;
: media.seasons.some(
(season) => season.status !== MediaStatus.UNKNOWN
)
? MediaStatus.PARTIALLY_AVAILABLE
: MediaStatus.UNKNOWN;
media.status4k = isAll4kSeasons
? MediaStatus.AVAILABLE
: media.seasons.some(
(season) => season.status4k !== MediaStatus.UNKNOWN
)
? MediaStatus.PARTIALLY_AVAILABLE
: MediaStatus.UNKNOWN;
await mediaRepository.save(media);
this.log(`Updating existing title: ${tvShow.name}`);
} else {
@@ -402,9 +551,20 @@ class JobPlexSync {
seasons: newSeasons,
tmdbId: tvShow.id,
tvdbId: tvShow.external_ids.tvdb_id,
status: isAllSeasons
status: isAllStandardSeasons
? MediaStatus.AVAILABLE
: MediaStatus.PARTIALLY_AVAILABLE,
: newSeasons.some(
(season) => season.status !== MediaStatus.UNKNOWN
)
? MediaStatus.PARTIALLY_AVAILABLE
: MediaStatus.UNKNOWN,
status4k: isAll4kSeasons
? MediaStatus.AVAILABLE
: newSeasons.some(
(season) => season.status4k !== MediaStatus.UNKNOWN
)
? MediaStatus.PARTIALLY_AVAILABLE
: MediaStatus.UNKNOWN,
});
await mediaRepository.save(newMedia);
this.log(`Saved ${tvShow.name}`);
@@ -508,6 +668,22 @@ class JobPlexSync {
(library) => library.enabled
);
this.enable4kMovie = settings.radarr.some((radarr) => radarr.is4k);
if (this.enable4kMovie) {
this.log(
'At least one 4K Radarr server was detected, so 4K movie detection is now enabled',
'info'
);
}
this.enable4kShow = settings.sonarr.some((sonarr) => sonarr.is4k);
if (this.enable4kShow) {
this.log(
'At least one 4K Sonarr server was detected, so 4K series detection is now enabled',
'info'
);
}
const hasHama = await this.hasHamaAgent();
if (hasHama) {
await animeList.sync();

View File

@@ -9,6 +9,9 @@ export enum Permission {
AUTO_APPROVE = 128,
AUTO_APPROVE_MOVIE = 256,
AUTO_APPROVE_TV = 512,
REQUEST_4K = 1024,
REQUEST_4K_MOVIE = 2048,
REQUEST_4K_TV = 4096,
}
/**

View File

@@ -55,6 +55,11 @@ interface PublicSettings {
initialized: boolean;
}
interface FullPublicSettings extends PublicSettings {
movie4kEnabled: boolean;
series4kEnabled: boolean;
}
export interface NotificationAgentConfig {
enabled: boolean;
types: number;
@@ -246,6 +251,18 @@ class Settings {
this.data.public = data;
}
get fullPublicSettings(): FullPublicSettings {
return {
...this.data.public,
movie4kEnabled: this.data.radarr.some(
(radarr) => radarr.is4k && radarr.isDefault
),
series4kEnabled: this.data.sonarr.some(
(sonarr) => sonarr.is4k && sonarr.isDefault
),
};
}
get notifications(): NotificationSettings {
return this.data.notifications;
}

View File

@@ -0,0 +1,91 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Add4kStatusFields1610370640747 implements MigrationInterface {
name = 'Add4kStatusFields1610370640747';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId" FROM "season"`
);
await queryRunner.query(`DROP TABLE "season"`);
await queryRunner.query(
`ALTER TABLE "temporary_season" RENAME TO "season"`
);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
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, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" 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, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`ALTER TABLE "season" RENAME TO "temporary_season"`
);
await queryRunner.query(
`CREATE TABLE "season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId" FROM "temporary_season"`
);
await queryRunner.query(`DROP TABLE "temporary_season"`);
}
}

View File

@@ -30,7 +30,7 @@ router.use('/user', isAuthenticated(Permission.MANAGE_USERS), user);
router.get('/settings/public', (_req, res) => {
const settings = getSettings();
return res.status(200).json(settings.public);
return res.status(200).json(settings.fullPublicSettings);
});
router.use(
'/settings',

View File

@@ -110,15 +110,21 @@ requestRoutes.post(
media = new Media({
tmdbId: tmdbMedia.id,
tvdbId: tmdbMedia.external_ids.tvdb_id,
status: MediaStatus.PENDING,
status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
mediaType: req.body.mediaType,
});
await mediaRepository.save(media);
} else {
if (media.status === MediaStatus.UNKNOWN) {
if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
media.status = MediaStatus.PENDING;
await mediaRepository.save(media);
}
if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
media.status4k = MediaStatus.PENDING;
await mediaRepository.save(media);
}
}
if (req.body.mediaType === 'movie') {
@@ -137,6 +143,10 @@ requestRoutes.post(
req.user?.hasPermission(Permission.AUTO_APPROVE_MOVIE)
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
});
await requestRepository.save(request);
@@ -149,13 +159,15 @@ requestRoutes.post(
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
// (Unless there are no seasons, in which case we abort)
if (media.requests) {
existingSeasons = media.requests.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
existingSeasons = media.requests
.filter((request) => request.is4k === req.body.is4k)
.reduce((seasons, request) => {
const combinedSeasons = request.seasons.map(
(season) => season.seasonNumber
);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
}
const finalSeasons = requestedSeasons.filter(
@@ -186,6 +198,7 @@ requestRoutes.post(
req.user?.hasPermission(Permission.AUTO_APPROVE_TV)
? req.user
: undefined,
is4k: req.body.is4k,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({