Skip to content

Commit fc6add5

Browse files
committed
feat: track episode availability from media servers scanners
1 parent 0321ea6 commit fc6add5

10 files changed

Lines changed: 1692 additions & 125 deletions

File tree

server/lib/availabilitySync.test.ts

Lines changed: 403 additions & 4 deletions
Large diffs are not rendered by default.

server/lib/availabilitySync.ts

Lines changed: 339 additions & 100 deletions
Large diffs are not rendered by default.

server/lib/scanners/baseScanner.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ interface ProcessOptions {
5454
export interface ProcessableEpisode {
5555
episodeNumber: number;
5656
hasFile: boolean;
57+
hasFile4k?: boolean;
5758
}
5859

5960
export interface ProcessableSeason {
@@ -711,16 +712,56 @@ class BaseScanner<T> {
711712
const existingEpisode = existingBySeasonAndNumber.get(key);
712713

713714
if (existingEpisode) {
715+
let changed = false;
716+
const statusField = is4k ? 'status4k' : 'status';
717+
const currentStatus = existingEpisode[statusField];
718+
714719
if (episodeDetail.hasFile) {
715-
existingEpisode[is4k ? 'status4k' : 'status'] =
716-
MediaStatus.AVAILABLE;
720+
existingEpisode[statusField] = MediaStatus.AVAILABLE;
721+
changed = true;
722+
} else if (
723+
currentStatus !== MediaStatus.DELETED &&
724+
currentStatus !== MediaStatus.UNKNOWN
725+
) {
726+
existingEpisode[statusField] = MediaStatus.DELETED;
727+
changed = true;
728+
}
729+
730+
if (this.enable4kShow && episodeDetail.hasFile4k != null && !is4k) {
731+
const currentStatus4k = existingEpisode.status4k;
732+
if (episodeDetail.hasFile4k) {
733+
existingEpisode.status4k = MediaStatus.AVAILABLE;
734+
changed = true;
735+
} else if (
736+
currentStatus4k !== MediaStatus.DELETED &&
737+
currentStatus4k !== MediaStatus.UNKNOWN
738+
) {
739+
existingEpisode.status4k = MediaStatus.DELETED;
740+
changed = true;
741+
}
742+
}
743+
744+
if (changed) {
717745
toSave.push(existingEpisode);
718746
}
719-
} else if (episodeDetail.hasFile) {
747+
} else if (
748+
episodeDetail.hasFile ||
749+
(this.enable4kShow && episodeDetail.hasFile4k)
750+
) {
751+
// Only persist episodes that have a file on at least one tier.
720752
const newEpisode = new Episode({
721753
episodeNumber: episodeDetail.episodeNumber,
722-
status: is4k ? MediaStatus.UNKNOWN : MediaStatus.AVAILABLE,
723-
status4k: is4k ? MediaStatus.AVAILABLE : MediaStatus.UNKNOWN,
754+
status: episodeDetail.hasFile
755+
? is4k
756+
? MediaStatus.UNKNOWN
757+
: MediaStatus.AVAILABLE
758+
: MediaStatus.UNKNOWN,
759+
status4k:
760+
this.enable4kShow && episodeDetail.hasFile4k
761+
? MediaStatus.AVAILABLE
762+
: is4k && episodeDetail.hasFile
763+
? MediaStatus.AVAILABLE
764+
: MediaStatus.UNKNOWN,
724765
season: Promise.resolve(dbSeason),
725766
});
726767
toSave.push(newEpisode);

server/lib/scanners/jellyfin/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { MediaServerType } from '@server/constants/server';
1616
import { getRepository } from '@server/datasource';
1717
import { User } from '@server/entity/User';
1818
import type {
19+
ProcessableEpisode,
1920
ProcessableSeason,
2021
RunnableScanner,
2122
StatusBase,
@@ -310,6 +311,8 @@ class JellyfinScanner
310311
let totalStandard = 0;
311312
let total4k = 0;
312313

314+
let episodeDetails: ProcessableEpisode[] | undefined;
315+
313316
if (!this.enable4kShow) {
314317
const episodes = await this.jfClient.getEpisodes(
315318
Id,
@@ -330,6 +333,10 @@ class JellyfinScanner
330333

331334
totalStandard += episodeCount;
332335
}
336+
337+
if (settings.main.enableEpisodeAvailability) {
338+
episodeDetails = this.toProcessableEpisodes(episodes);
339+
}
333340
} else {
334341
// 4K detection enabled - request media info to check resolution
335342
const episodes = await this.jfClient.getEpisodes(
@@ -376,6 +383,19 @@ class JellyfinScanner
376383
// See issue https://github.com/seerr-team/seerr/issues/1744 for details.
377384
if (hasStandard) totalStandard += episodeCount;
378385
if (has4k) total4k += episodeCount;
386+
387+
if (
388+
settings.main.enableEpisodeAvailability &&
389+
(hasStandard || has4k)
390+
) {
391+
episodeDetails ??= [];
392+
episodeDetails.push(
393+
...this.toProcessableEpisodes([episode], {
394+
hasFile: !!hasStandard,
395+
hasFile4k: !!has4k,
396+
})
397+
);
398+
}
379399
}
380400
}
381401

@@ -400,6 +420,7 @@ class JellyfinScanner
400420
totalEpisodes: season.episode_count,
401421
episodes: totalStandard,
402422
episodes4k: total4k,
423+
episodeDetails: tvdbSeasonFromAnidb ? undefined : episodeDetails,
403424
});
404425
} else {
405426
processableSeasons.push({
@@ -443,6 +464,32 @@ class JellyfinScanner
443464
}
444465
}
445466

467+
private toProcessableEpisodes(
468+
episodes: JellyfinLibraryItem[],
469+
availability: { hasFile: boolean; hasFile4k?: boolean } = { hasFile: true }
470+
): ProcessableEpisode[] {
471+
return episodes.flatMap((episode) => {
472+
if (episode.IndexNumber == null) {
473+
return [];
474+
}
475+
476+
const lastEpisodeNumber = episode.IndexNumberEnd ?? episode.IndexNumber;
477+
const details: ProcessableEpisode[] = [];
478+
for (
479+
let episodeNumber = episode.IndexNumber;
480+
episodeNumber <= lastEpisodeNumber;
481+
episodeNumber++
482+
) {
483+
details.push({
484+
episodeNumber,
485+
hasFile: availability.hasFile,
486+
hasFile4k: availability.hasFile4k,
487+
});
488+
}
489+
return details;
490+
});
491+
}
492+
446493
private async processItem(item: JellyfinLibraryItem): Promise<void> {
447494
if (item.Type === 'Movie') {
448495
await this.processJellyfinMovie(item);

0 commit comments

Comments
 (0)