diff --git a/server/api/plexapi.ts b/server/api/plexapi.ts
index 617203447f..d49a92e031 100644
--- a/server/api/plexapi.ts
+++ b/server/api/plexapi.ts
@@ -18,6 +18,7 @@ export interface PlexLibraryItem {
guid: string;
parentGuid?: string;
grandparentGuid?: string;
+ editionTitle?: string;
addedAt: number;
updatedAt: number;
Guid?: {
@@ -53,6 +54,7 @@ export interface PlexMetadata {
guid: string;
type: 'movie' | 'show' | 'season';
title: string;
+ editionTitle?: string;
Guid: {
id: string;
}[];
diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts
index c6c09cb553..f8ab5e159a 100644
--- a/server/lib/availabilitySync.ts
+++ b/server/lib/availabilitySync.ts
@@ -16,6 +16,7 @@ import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { getHostname } from '@server/utils/getHostname';
+import { isIgnoredEdition } from '@server/utils/plexFilter';
class AvailabilitySync {
public running = false;
@@ -878,6 +879,12 @@ class AvailabilitySync {
}
}
+ if (plexMedia && media.mediaType === 'movie') {
+ if (isIgnoredEdition(plexMedia.editionTitle)) {
+ plexMedia = undefined;
+ }
+ }
+
if (plexMedia) {
existsInPlex = true;
}
diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts
index b464008e89..530421e1d5 100644
--- a/server/lib/scanners/plex/index.ts
+++ b/server/lib/scanners/plex/index.ts
@@ -20,6 +20,7 @@ import type {
import BaseScanner from '@server/lib/scanners/baseScanner';
import type { Library } from '@server/lib/settings';
import { getSettings } from '@server/lib/settings';
+import { isIgnoredEdition, isIgnoredEpisode } from '@server/utils/plexFilter';
import { uniqWith } from 'lodash';
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
@@ -225,6 +226,13 @@ class PlexScanner
}
private async processPlexMovie(plexitem: PlexLibraryItem) {
+ if (isIgnoredEdition(plexitem.editionTitle)) {
+ this.log(
+ `Skipping movie ${plexitem.title} with ignored edition: ${plexitem.editionTitle}`
+ );
+ return;
+ }
+
const mediaIds = await this.getMediaIds(plexitem);
const has4k = plexitem.Media.some(
@@ -243,6 +251,13 @@ class PlexScanner
plexitem: PlexMetadata,
tmdbId: number
) {
+ if (isIgnoredEdition(plexitem.editionTitle)) {
+ this.log(
+ `Skipping movie ${plexitem.title} with ignored edition: ${plexitem.editionTitle}`
+ );
+ return;
+ }
+
const has4k = plexitem.Media.some(
(media) => media.videoResolution === '4k'
);
@@ -334,9 +349,19 @@ class PlexScanner
if (matchedPlexSeason) {
// If we have a matched Plex season, get its children metadata so we can check details
- const episodes = await this.plexClient.getChildrenMetadata(
+ const allEpisodes = await this.plexClient.getChildrenMetadata(
matchedPlexSeason.ratingKey
);
+
+ const episodes = allEpisodes.filter(
+ (episode) =>
+ !isIgnoredEpisode(
+ episode.title,
+ season.season_number,
+ episode.index
+ )
+ );
+
// Total episodes that are in standard definition (not 4k)
const totalStandard = episodes.filter((episode) =>
!this.enable4kShow
@@ -367,17 +392,18 @@ class PlexScanner
}
}
- if (mediaIds.tvdbId) {
- await this.processShow(
- mediaIds.tmdbId,
- mediaIds.tvdbId ?? tvShow.external_ids.tvdb_id,
- processableSeasons,
- {
- mediaAddedAt: new Date(metadata.addedAt * 1000),
- ratingKey: ratingKey,
- title: metadata.title,
- }
- );
+ const hasAnyEpisodes = processableSeasons.some(
+ (s) => s.episodes > 0 || s.episodes4k > 0
+ );
+
+ const tvdbId = mediaIds.tvdbId ?? tvShow.external_ids?.tvdb_id;
+
+ if (tvdbId && hasAnyEpisodes) {
+ await this.processShow(mediaIds.tmdbId, tvdbId, processableSeasons, {
+ mediaAddedAt: new Date(metadata.addedAt * 1000),
+ ratingKey: ratingKey,
+ title: metadata.title,
+ });
}
}
@@ -577,6 +603,9 @@ class PlexScanner
);
if (episodes) {
for (const episode of episodes) {
+ if (isIgnoredEpisode(episode.title, 0, episode.index)) {
+ continue;
+ }
const special = animeList.getSpecialEpisode(tvdbId, episode.index);
if (special) {
if (special.tmdbId) {
diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts
index 7057cf2b01..00e5df4707 100644
--- a/server/lib/settings/index.ts
+++ b/server/lib/settings/index.ts
@@ -35,6 +35,9 @@ export interface PlexSettings {
useSsl?: boolean;
libraries: Library[];
webAppUrl?: string;
+ ignoredEditions: string[];
+ ignoredEpisodeTitles: string[];
+ ignoredEpisodeFilterMode: 'season' | 'seasonAndEpisode' | 'any';
}
export interface JellyfinSettings {
@@ -414,6 +417,9 @@ class Settings {
port: 32400,
useSsl: false,
libraries: [],
+ ignoredEditions: [],
+ ignoredEpisodeTitles: [],
+ ignoredEpisodeFilterMode: 'season',
},
jellyfin: {
name: '',
diff --git a/server/utils/plexFilter.ts b/server/utils/plexFilter.ts
new file mode 100644
index 0000000000..4e14c4da06
--- /dev/null
+++ b/server/utils/plexFilter.ts
@@ -0,0 +1,34 @@
+import { getSettings } from '@server/lib/settings';
+
+export function isIgnoredEdition(editionTitle?: string): boolean {
+ if (!editionTitle) return false;
+
+ const ignoredEditions = getSettings().plex.ignoredEditions ?? [];
+ return ignoredEditions.some(
+ (e) => editionTitle.toLowerCase().trim() === e.toLowerCase().trim()
+ );
+}
+
+export function isIgnoredEpisode(
+ episodeTitle: string | undefined,
+ seasonNumber: number,
+ episodeNumber: number
+): boolean {
+ const settings = getSettings();
+ const ignoredTitles = settings.plex.ignoredEpisodeTitles ?? [];
+ const filterMode = settings.plex.ignoredEpisodeFilterMode ?? 'season';
+
+ if (ignoredTitles.length === 0) return false;
+
+ const titleMatch = ignoredTitles.some(
+ (t) => episodeTitle?.toLowerCase().trim() === t.toLowerCase().trim()
+ );
+ if (!titleMatch) return false;
+
+ if (filterMode === 'any') return true;
+ if (filterMode === 'season') return seasonNumber === 0;
+ if (filterMode === 'seasonAndEpisode')
+ return seasonNumber === 0 && episodeNumber === 0;
+
+ return false;
+}
diff --git a/src/components/Settings/SettingsPlex.tsx b/src/components/Settings/SettingsPlex.tsx
index 0cb1c11987..e6e6d1d76b 100644
--- a/src/components/Settings/SettingsPlex.tsx
+++ b/src/components/Settings/SettingsPlex.tsx
@@ -22,10 +22,16 @@ import { Field, Formik } from 'formik';
import { orderBy } from 'lodash';
import { useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
+import CreatableSelect from 'react-select/creatable';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
+type TagOption = {
+ label: string;
+ value: string;
+};
+
const messages = defineMessages('components.Settings', {
plex: 'Plex',
plexsettings: 'Plex Settings',
@@ -67,6 +73,23 @@ const messages = defineMessages('components.Settings', {
webAppUrl: '
+ {intl.formatMessage(messages.contentFilteringDescription)} +
+