Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
843 changes: 840 additions & 3 deletions server/lib/availabilitySync.test.ts

Large diffs are not rendered by default.

567 changes: 437 additions & 130 deletions server/lib/availabilitySync.ts

Large diffs are not rendered by default.

40 changes: 35 additions & 5 deletions server/lib/scanners/baseScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface ProcessOptions {
export interface ProcessableEpisode {
episodeNumber: number;
hasFile: boolean;
hasFile4k?: boolean;
}

export interface ProcessableSeason {
Expand Down Expand Up @@ -744,16 +745,45 @@ class BaseScanner<T> {
const existingEpisode = existingBySeasonAndNumber.get(key);

if (existingEpisode) {
let changed = false;
const statusField = is4k ? 'status4k' : 'status';
const currentStatus = existingEpisode[statusField];

if (episodeDetail.hasFile) {
existingEpisode[is4k ? 'status4k' : 'status'] =
MediaStatus.AVAILABLE;
if (currentStatus !== MediaStatus.AVAILABLE) {
existingEpisode[statusField] = MediaStatus.AVAILABLE;
changed = true;
}
}

if (this.enable4kShow && episodeDetail.hasFile4k && !is4k) {
if (existingEpisode.status4k !== MediaStatus.AVAILABLE) {
existingEpisode.status4k = MediaStatus.AVAILABLE;
changed = true;
}
}

if (changed) {
toSave.push(existingEpisode);
}
} else if (episodeDetail.hasFile) {
} else if (
episodeDetail.hasFile ||
(this.enable4kShow && episodeDetail.hasFile4k)
) {
// Only persist episodes that have a file on at least one tier.
const newEpisode = new Episode({
episodeNumber: episodeDetail.episodeNumber,
status: is4k ? MediaStatus.UNKNOWN : MediaStatus.AVAILABLE,
status4k: is4k ? MediaStatus.AVAILABLE : MediaStatus.UNKNOWN,
status: episodeDetail.hasFile
? is4k
? MediaStatus.UNKNOWN
: MediaStatus.AVAILABLE
: MediaStatus.UNKNOWN,
status4k:
this.enable4kShow && episodeDetail.hasFile4k
? MediaStatus.AVAILABLE
: is4k && episodeDetail.hasFile
? MediaStatus.AVAILABLE
: MediaStatus.UNKNOWN,
season: Promise.resolve(dbSeason),
});
toSave.push(newEpisode);
Expand Down
47 changes: 47 additions & 0 deletions server/lib/scanners/jellyfin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MediaServerType } from '@server/constants/server';
import { getRepository } from '@server/datasource';
import { User } from '@server/entity/User';
import type {
ProcessableEpisode,
ProcessableSeason,
RunnableScanner,
StatusBase,
Expand Down Expand Up @@ -310,6 +311,8 @@ class JellyfinScanner
let totalStandard = 0;
let total4k = 0;

let episodeDetails: ProcessableEpisode[] | undefined;

if (!this.enable4kShow) {
const episodes = await this.jfClient.getEpisodes(
Id,
Expand All @@ -330,6 +333,10 @@ class JellyfinScanner

totalStandard += episodeCount;
}

if (settings.main.enableEpisodeAvailability) {
episodeDetails = this.toProcessableEpisodes(episodes);
}
} else {
// 4K detection enabled - request media info to check resolution
const episodes = await this.jfClient.getEpisodes(
Expand Down Expand Up @@ -376,6 +383,19 @@ class JellyfinScanner
// See issue https://github.com/seerr-team/seerr/issues/1744 for details.
if (hasStandard) totalStandard += episodeCount;
if (has4k) total4k += episodeCount;

if (
settings.main.enableEpisodeAvailability &&
(hasStandard || has4k)
) {
episodeDetails ??= [];
episodeDetails.push(
...this.toProcessableEpisodes([episode], {
hasFile: !!hasStandard,
hasFile4k: !!has4k,
})
);
}
}
}

Expand All @@ -400,6 +420,7 @@ class JellyfinScanner
totalEpisodes: season.episode_count,
episodes: totalStandard,
episodes4k: total4k,
episodeDetails: tvdbSeasonFromAnidb ? undefined : episodeDetails,
});
} else {
processableSeasons.push({
Expand Down Expand Up @@ -443,6 +464,32 @@ class JellyfinScanner
}
}

private toProcessableEpisodes(
episodes: JellyfinLibraryItem[],
availability: { hasFile: boolean; hasFile4k?: boolean } = { hasFile: true }
): ProcessableEpisode[] {
return episodes.flatMap((episode) => {
if (episode.IndexNumber == null) {
return [];
}

const lastEpisodeNumber = episode.IndexNumberEnd ?? episode.IndexNumber;
const details: ProcessableEpisode[] = [];
for (
let episodeNumber = episode.IndexNumber;
episodeNumber <= lastEpisodeNumber;
episodeNumber++
) {
details.push({
episodeNumber,
hasFile: availability.hasFile,
hasFile4k: availability.hasFile4k,
});
}
return details;
});
}

private async processItem(item: JellyfinLibraryItem): Promise<void> {
if (item.Type === 'Movie') {
await this.processJellyfinMovie(item);
Expand Down
Loading
Loading