diff --git a/overseerr-api.yml b/overseerr-api.yml index e5cd443b..a4a01aec 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -2976,7 +2976,7 @@ paths: name: sort schema: type: string - enum: [added, modified] + enum: [added, modified, mediaAdded] default: added responses: '200': diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts index d460fe77..43487c21 100644 --- a/server/api/plexapi.ts +++ b/server/api/plexapi.ts @@ -9,6 +9,8 @@ export interface PlexLibraryItem { guid: string; parentGuid?: string; grandparentGuid?: string; + addedAt: number; + updatedAt: number; type: 'movie' | 'show' | 'season' | 'episode'; } @@ -48,6 +50,8 @@ export interface PlexMetadata { parentIndex?: number; leafCount: number; viewedLeafCount: number; + addedAt: number; + updatedAt: number; Media: Media[]; } diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 2a918575..dc269f5a 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -101,6 +101,9 @@ class Media { @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) public lastSeasonChange: Date; + @Column({ type: 'datetime', nullable: true }) + public mediaAddedAt: Date; + constructor(init?: Partial) { Object.assign(this, init); } diff --git a/server/job/plexsync/index.ts b/server/job/plexsync/index.ts index 1e8470fb..2c3330ca 100644 --- a/server/job/plexsync/index.ts +++ b/server/job/plexsync/index.ts @@ -111,6 +111,7 @@ class JobPlexSync { existing.status !== MediaStatus.AVAILABLE ) { existing.status = MediaStatus.AVAILABLE; + existing.mediaAddedAt = new Date(plexitem.addedAt * 1000); changedExisting = true; } @@ -123,6 +124,11 @@ class JobPlexSync { changedExisting = true; } + if (!existing.mediaAddedAt && !changedExisting) { + existing.mediaAddedAt = new Date(plexitem.addedAt * 1000); + changedExisting = true; + } + if (changedExisting) { await mediaRepository.save(existing); this.log( @@ -144,6 +150,7 @@ class JobPlexSync { ? MediaStatus.AVAILABLE : MediaStatus.UNKNOWN; newMedia.mediaType = MediaType.MOVIE; + newMedia.mediaAddedAt = new Date(plexitem.addedAt * 1000); await mediaRepository.save(newMedia); this.log(`Saved ${plexitem.title}`); } @@ -208,6 +215,7 @@ class JobPlexSync { existing.status !== MediaStatus.AVAILABLE ) { existing.status = MediaStatus.AVAILABLE; + existing.mediaAddedAt = new Date(plexitem.addedAt * 1000); changedExisting = true; } @@ -220,6 +228,11 @@ class JobPlexSync { changedExisting = true; } + if (!existing.mediaAddedAt && !changedExisting) { + existing.mediaAddedAt = new Date(plexitem.addedAt * 1000); + changedExisting = true; + } + if (changedExisting) { await mediaRepository.save(existing); this.log( @@ -240,6 +253,7 @@ class JobPlexSync { const newMedia = new Media(); newMedia.imdbId = tmdbMovie.external_ids.imdb_id; newMedia.tmdbId = tmdbMovie.id; + newMedia.mediaAddedAt = new Date(plexitem.addedAt * 1000); newMedia.status = hasOtherResolution || (!this.enable4kMovie && has4k) ? MediaStatus.AVAILABLE @@ -266,10 +280,7 @@ class JobPlexSync { ); if (episodes) { for (const episode of episodes) { - const special = await animeList.getSpecialEpisode( - tvdbId, - episode.index - ); + const special = animeList.getSpecialEpisode(tvdbId, episode.index); if (special) { if (special.tmdbId) { await this.processMovieWithId(episode, undefined, special.tmdbId); @@ -519,6 +530,7 @@ class JobPlexSync { 'debug' ); media.lastSeasonChange = new Date(); + media.mediaAddedAt = new Date(plexitem.addedAt * 1000); } if (new4kSeasonAvailable > current4kSeasonAvailable) { @@ -531,6 +543,10 @@ class JobPlexSync { media.lastSeasonChange = new Date(); } + if (!media.mediaAddedAt) { + media.mediaAddedAt = new Date(plexitem.addedAt * 1000); + } + media.status = isAllStandardSeasons ? MediaStatus.AVAILABLE : media.seasons.some( @@ -553,6 +569,7 @@ class JobPlexSync { seasons: newSeasons, tmdbId: tvShow.id, tvdbId: tvShow.external_ids.tvdb_id, + mediaAddedAt: new Date(plexitem.addedAt * 1000), status: isAllStandardSeasons ? MediaStatus.AVAILABLE : newSeasons.some( diff --git a/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts b/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts new file mode 100644 index 00000000..78dbc06e --- /dev/null +++ b/server/migration/1610522845513-AddMediaAddedFieldToMedia.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMediaAddedFieldToMedia1610522845513 + implements MigrationInterface { + name = 'AddMediaAddedFieldToMedia1610522845513'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + 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), "mediaAddedAt" datetime, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k" FROM "media"` + ); + await queryRunner.query(`DROP TABLE "media"`); + await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "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") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + 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), "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))` + ); + await queryRunner.query( + `INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k" FROM "temporary_media"` + ); + await queryRunner.query(`DROP TABLE "temporary_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") ` + ); + } +} diff --git a/server/routes/media.ts b/server/routes/media.ts index 36e64e6e..f7d67d5c 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -47,6 +47,10 @@ mediaRoutes.get('/', async (req, res, next) => { updatedAt: 'DESC', }; break; + case 'mediaAdded': + sortFilter = { + mediaAddedAt: 'DESC', + }; } try { diff --git a/src/components/Discover/index.tsx b/src/components/Discover/index.tsx index 5377970d..bca0ca47 100644 --- a/src/components/Discover/index.tsx +++ b/src/components/Discover/index.tsx @@ -69,7 +69,7 @@ const Discover: React.FC = () => { ); const { data: media, error: mediaError } = useSWR( - '/api/v1/media?filter=available&take=20&sort=modified' + '/api/v1/media?filter=available&take=20&sort=mediaAdded' ); const {