diff --git a/overseerr-api.yml b/overseerr-api.yml index e5cd443ba..a4a01aece 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 d460fe77b..43487c21e 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 2a9185756..dc269f5aa 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 1e8470fbd..2c3330ca3 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 000000000..78dbc06e2 --- /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 36e64e6eb..f7d67d5c3 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 5377970d0..bca0ca477 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 {