Skip to content

feat(plex): add content filtering for ignored editions and episode titles#2603

Open
Rubeanie wants to merge 5 commits intoseerr-team:developfrom
Rubeanie:feature/plex-ignored-editions
Open

feat(plex): add content filtering for ignored editions and episode titles#2603
Rubeanie wants to merge 5 commits intoseerr-team:developfrom
Rubeanie:feature/plex-ignored-editions

Conversation

@Rubeanie
Copy link

@Rubeanie Rubeanie commented Feb 28, 2026

Description

Adds configurable content filtering to Plex settings, allowing users to exclude specific media from being detected during library scans. This prevents placeholder files (e.g., trailers) from incorrectly marking content as available.

Movie Edition Filtering:

  • New "Ignored Editions" multi-tag input in Plex settings
  • Movies with matching Plex edition tags (e.g., Trailer) are skipped during library scans and availability sync
  • Exact, case-insensitive matching

TV Episode Title Filtering:

  • New "Ignored Episode Titles" multi-tag input
  • Configurable "Episode Filter Mode" dropdown with three modes:
    • Season 0: only filters matching titles in specials/Season 0
    • Season 0 + Episode 0: only filters S00E00 specifically
    • Any Season or Episode: filters matching titles everywhere
  • Also applies to Hama (anime) special episode processing
  • Includes a guard to prevent processShow from running when no episodes remain after filtering

UI:

  • New "Content Filtering" section heading in Plex settings
  • Filter mode dropdown is disabled when no ignored episode titles are configured
  • Tooltips cross-reference each other for clarity

Related

How Has This Been Tested?

Built and published to DockerHub rubeanie/seerr:ignored-editions. Self tested, with more users actively testing.

Screenshots / Logs (if applicable)

Default:
image

Example:
image

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
    • AI Assistance: Claude Opus 4.6 was used for troubleshooting and code review
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

Summary by CodeRabbit

  • New Features
    • Content-filtering for Plex: ignore specific movie editions and episode titles, with a selectable episode filter mode (season / seasonAndEpisode / any). Ignored items are skipped during scans and processing so they won't be imported or updated.
  • Documentation
    • Added UI labels, tips, and placeholders to configure Plex content-filtering (ignored editions, ignored episode titles, and filter mode).

@Rubeanie Rubeanie requested a review from a team as a code owner February 28, 2026 11:04
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Adds configurable Plex content filtering: new settings and UI to ignore specific movie editions and episode titles, utilities to evaluate ignores, and integrations that skip ignored media in Plex scanners and availability sync.

Changes

Cohort / File(s) Summary
Type Definitions
server/api/plexapi.ts
Added optional editionTitle?: string to PlexLibraryItem and PlexMetadata.
Filtering Utilities
server/utils/plexFilter.ts
New exports isIgnoredEdition(editionTitle?: string) and isIgnoredEpisode(episodeTitle, seasonNumber, episodeNumber) that read plex settings and apply mode-based logic.
Scan & Sync Integration
server/lib/scanners/plex/index.ts, server/lib/availabilitySync.ts
Integrates ignore checks: skip movies with ignored editions (with log), filter out ignored episodes when collecting seasons/episodes, avoid processing shows with no non-ignored episodes; availability sync treats ignored-edition media as absent.
Settings Schema
server/lib/settings/index.ts
Added Plex settings: ignoredEditions: string[], ignoredEpisodeTitles: string[], `ignoredEpisodeFilterMode: 'season'
Settings UI
src/components/Settings/SettingsPlex.tsx
Added content filtering UI (creatable multi-selects and mode dropdown), form handling, normalization/deduplication on submit, and sends new fields to server.
Localization
src/i18n/locale/en.json
Added 13 i18n keys for content filtering, ignored editions/episode titles, placeholders, tips, and filter mode labels.
sequenceDiagram
  participant User
  participant UI as Settings UI
  participant Server
  participant SettingsStore as Settings DB
  participant Scanner as Plex Scanner
  participant Plex as Plex Media Server

  User->>UI: configure ignored editions / episode titles & mode
  UI->>Server: POST /api/v1/settings/plex (includes ignored lists + mode)
  Server->>SettingsStore: save plex.ignoredEditions, plex.ignoredEpisodeTitles, plex.ignoredEpisodeFilterMode
  Scanner->>SettingsStore: getSettings()
  Scanner->>Plex: fetch library items / seasons / episodes
  Scanner->>Server: call isIgnoredEdition / isIgnoredEpisode
  Server->>SettingsStore: read plex ignore settings
  Server-->>Scanner: return ignore decision (true/false)
  alt not ignored
    Scanner->>Server: process item (match, add, update)
  else ignored
    Scanner->>Scanner: skip item (log and do not process)
  end
  Scanner->>Server: availability sync uses same ignore checks before marking available/missing
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibble titles, trim and tidy bright,
Hopping past editions that don't feel right.
Seasons pruned with a careful twitch,
Ignored or kept — I know which!
A rabbit's filter, quick and spry — hop on by!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding content filtering for ignored editions and episode titles in Plex settings.
Linked Issues check ✅ Passed The PR implements content filtering for ignored editions and episodes to prevent placeholder/trailer content from being marked as available, directly addressing issue #2235's core requirement.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing content filtering: API types, filter utilities, settings storage, UI components, and scanner logic modifications are all aligned with the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/lib/scanners/plex/index.ts (1)

431-435: ⚠️ Potential issue | 🟠 Major

Fix unreachable TVDB fallback before calling processShow.

Line 431 currently requires mediaIds.tvdbId, which prevents processing when TVDB is only available via tvShow.external_ids.tvdb_id. That makes the fallback on Line 434 effectively dead.

💡 Proposed fix
-    if (mediaIds.tvdbId && hasAnyEpisodes) {
+    const resolvedTvdbId = mediaIds.tvdbId ?? tvShow.external_ids.tvdb_id;
+    if (resolvedTvdbId && hasAnyEpisodes) {
       await this.processShow(
         mediaIds.tmdbId,
-        mediaIds.tvdbId ?? tvShow.external_ids.tvdb_id,
+        resolvedTvdbId,
         processableSeasons,
         {
           mediaAddedAt: new Date(metadata.addedAt * 1000),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/scanners/plex/index.ts` around lines 431 - 435, The condition
currently gates processShow on mediaIds.tvdbId, making the
tvShow.external_ids.tvdb_id fallback dead; change the if to check for either
TVDB id (e.g., if ((mediaIds.tvdbId || tvShow.external_ids?.tvdb_id) &&
hasAnyEpisodes)) and call processShow with the chosen id (mediaIds.tvdbId ??
tvShow.external_ids?.tvdb_id) so processShow receives the correct TVDB id even
when mediaIds.tvdbId is absent; ensure optional chaining on tvShow.external_ids
to avoid runtime errors.
🧹 Nitpick comments (1)
src/components/Settings/SettingsPlex.tsx (1)

425-427: Normalize tag arrays before persisting settings.

Consider trimming, filtering empty strings, and case-insensitive deduplication before POST to avoid redundant config entries.

♻️ Suggested hardening
+            const normalizeTags = (tags: string[]) =>
+              [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
+
             await axios.post('/api/v1/settings/plex', {
               ip: values.hostname,
               port: Number(values.port),
               useSsl: values.useSsl,
               webAppUrl: values.webAppUrl,
-              ignoredEditions: values.ignoredEditions,
-              ignoredEpisodeTitles: values.ignoredEpisodeTitles,
+              ignoredEditions: normalizeTags(values.ignoredEditions),
+              ignoredEpisodeTitles: normalizeTags(values.ignoredEpisodeTitles),
               ignoredEpisodeFilterMode: values.ignoredEpisodeFilterMode,
             } as PlexSettings);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/SettingsPlex.tsx` around lines 425 - 427, The
ignoredEditions and ignoredEpisodeTitles arrays are assigned directly from
values and should be normalized before the POST: trim each string, remove any
empty strings, lowercase for case-insensitive comparisons, and deduplicate
(preserve order) so the payload has no redundant entries; modify the code that
builds the request body where ignoredEditions and ignoredEpisodeTitles are set
(the block assigning ignoredEditions: values.ignoredEditions and
ignoredEpisodeTitles: values.ignoredEpisodeTitles) to
map/trim/filter/toLowerCase and run a dedupe step before sending.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/lib/availabilitySync.ts`:
- Line 15: The availability sync file imports isIgnoredEdition directly from the
scanner module which causes scanner singletons to initialize as a side effect;
extract the pure helper isIgnoredEdition into a new small shared module (e.g.,
export a function named isIgnoredEdition from a new plex filtering utility) and
update both the scanner module and availabilitySync to import that helper
instead of importing from the scanner module; remove the import of the scanner
module from availabilitySync so availability sync no longer triggers scanner
initialization and ensure both places import the same new utility function name
isIgnoredEdition.

---

Outside diff comments:
In `@server/lib/scanners/plex/index.ts`:
- Around line 431-435: The condition currently gates processShow on
mediaIds.tvdbId, making the tvShow.external_ids.tvdb_id fallback dead; change
the if to check for either TVDB id (e.g., if ((mediaIds.tvdbId ||
tvShow.external_ids?.tvdb_id) && hasAnyEpisodes)) and call processShow with the
chosen id (mediaIds.tvdbId ?? tvShow.external_ids?.tvdb_id) so processShow
receives the correct TVDB id even when mediaIds.tvdbId is absent; ensure
optional chaining on tvShow.external_ids to avoid runtime errors.

---

Nitpick comments:
In `@src/components/Settings/SettingsPlex.tsx`:
- Around line 425-427: The ignoredEditions and ignoredEpisodeTitles arrays are
assigned directly from values and should be normalized before the POST: trim
each string, remove any empty strings, lowercase for case-insensitive
comparisons, and deduplicate (preserve order) so the payload has no redundant
entries; modify the code that builds the request body where ignoredEditions and
ignoredEpisodeTitles are set (the block assigning ignoredEditions:
values.ignoredEditions and ignoredEpisodeTitles: values.ignoredEpisodeTitles) to
map/trim/filter/toLowerCase and run a dedupe step before sending.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f74306e and f11631d.

📒 Files selected for processing (6)
  • server/api/plexapi.ts
  • server/lib/availabilitySync.ts
  • server/lib/scanners/plex/index.ts
  • server/lib/settings/index.ts
  • src/components/Settings/SettingsPlex.tsx
  • src/i18n/locale/en.json

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/components/Settings/SettingsPlex.tsx (1)

420-428: Consider using Set for deduplication.

The Map-based approach works but Set is more idiomatic for simple deduplication:

♻️ Suggested simplification
-            const dedupe = (arr: string[]) => [
-              ...new Map(arr.map((s) => [s, s])).values(),
-            ];
+            const dedupe = (arr: string[]) => [...new Set(arr)];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/SettingsPlex.tsx` around lines 420 - 428, The
deduplication is implemented with a Map in the dedupe function; replace that
with an idiomatic Set-based approach by changing dedupe(arr: string[]) to return
[...new Set(arr)] and ensure normalize (which calls dedupe) still trims,
lowercases and filters entries before deduplication; update references to dedupe
and normalize accordingly so types (arr?: string[]) and behavior remain the
same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/Settings/SettingsPlex.tsx`:
- Around line 420-428: The deduplication is implemented with a Map in the dedupe
function; replace that with an idiomatic Set-based approach by changing
dedupe(arr: string[]) to return [...new Set(arr)] and ensure normalize (which
calls dedupe) still trims, lowercases and filters entries before deduplication;
update references to dedupe and normalize accordingly so types (arr?: string[])
and behavior remain the same.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f11631d and ee7cc5a.

📒 Files selected for processing (4)
  • server/lib/availabilitySync.ts
  • server/lib/scanners/plex/index.ts
  • server/utils/plexFilter.ts
  • src/components/Settings/SettingsPlex.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/lib/availabilitySync.ts

@Rubeanie
Copy link
Author

One alternative approach to this problem is filtering ignored media based on file paths using regular expressions

@Rubeanie
Copy link
Author

Leaving this PR open just in case this implementation is preferred.

@Rubeanie Rubeanie force-pushed the feature/plex-ignored-editions branch from ee7cc5a to 62697d6 Compare March 5, 2026 03:34
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/components/Settings/SettingsPlex.tsx (2)

420-438: Prefer Set-based dedupe (and consider coercing mode when titles are empty).

  1. new Map(arr.map((s) => [s, s])) is a common TS footgun because map() often infers string[] rather than [string, string] tuples, depending on TS/version/config. Using Set is simpler and avoids tuple inference issues.

  2. Minor UX consistency: if ignoredEpisodeTitles is empty, you may want to force ignoredEpisodeFilterMode back to 'season' before persisting, otherwise the dropdown can appear “stuck” on a non-default mode (while disabled) after reload.

Proposed refactor
-            const dedupe = (arr: string[]) => [
-              ...new Map(arr.map((s) => [s, s])).values(),
-            ];
-            const normalize = (arr?: string[]) =>
-              dedupe(
-                (arr ?? [])
-                  .map((s) => s.trim().toLowerCase())
-                  .filter((s) => s.length > 0)
-              );
+            const normalize = (arr?: string[]) => {
+              const cleaned = (arr ?? [])
+                .map((s) => s.trim().toLowerCase())
+                .filter((s) => s.length > 0);
+              return [...new Set(cleaned)];
+            };
 
             await axios.post('/api/v1/settings/plex', {
               ip: values.hostname,
               port: Number(values.port),
               useSsl: values.useSsl,
               webAppUrl: values.webAppUrl,
-              ignoredEditions: normalize(values.ignoredEditions),
-              ignoredEpisodeTitles: normalize(values.ignoredEpisodeTitles),
-              ignoredEpisodeFilterMode: values.ignoredEpisodeFilterMode,
+              ignoredEditions: normalize(values.ignoredEditions),
+              ignoredEpisodeTitles: normalize(values.ignoredEpisodeTitles),
+              ignoredEpisodeFilterMode: normalize(values.ignoredEpisodeTitles)
+                .length
+                ? values.ignoredEpisodeFilterMode
+                : 'season',
             } as PlexSettings);
  • Please confirm TS compilation for the current Map-based dedupe under this repo’s TS config (or adopt the Set version).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/SettingsPlex.tsx` around lines 420 - 438, The dedupe
implementation using new Map(...) can cause tuple inference issues; replace the
dedupe function with a Set-based approach (e.g., Array.from(new Set(arr))) and
update normalize to use that dedupe; additionally, when preparing the payload
for axios.post in the SettingsPlex component, ensure ignoredEpisodeFilterMode is
coerced to 'season' if normalized ignoredEpisodeTitles is empty (so the
persisted mode can't remain a non-default when titles are blank); adjust
references to dedupe, normalize, ignoredEpisodeTitles, and
ignoredEpisodeFilterMode in that submission block accordingly.

643-766: Add inputId/instanceId for react-select fields so the <label htmlFor> actually works (a11y).

Right now the labels’ htmlFor="ignoredEditions" / htmlFor="ignoredEpisodeTitles" won’t reliably focus/associate with the underlying react-select input unless you wire inputId (and typically instanceId). This improves keyboard UX and screen-reader association.

Proposed patch
                     <CreatableSelect<TagOption, true>
+                      inputId="ignoredEditions"
+                      instanceId="ignoredEditions"
                       components={{
                         DropdownIndicator: null,
                       }}
                       isClearable
                       isMulti
@@
                     <CreatableSelect<TagOption, true>
+                      inputId="ignoredEpisodeTitles"
+                      instanceId="ignoredEpisodeTitles"
                       components={{
                         DropdownIndicator: null,
                       }}
                       isClearable
                       isMulti
  • Please confirm inputId/instanceId are the preferred props for react-select v5.10.2 in this repo’s usage (some codebases standardize on aria-labelledby instead).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/SettingsPlex.tsx` around lines 643 - 766, The
react-select controls (CreatableSelect components used for ignoredEditions and
ignoredEpisodeTitles) need inputId and instanceId props so the existing <label
htmlFor="ignoredEditions"> and <label htmlFor="ignoredEpisodeTitles"> correctly
associate and focus the underlying input; update the CreatableSelect for
ignoredEditions and ignoredEpisodeTitles to include inputId="ignoredEditions"
(and inputId="ignoredEpisodeTitles") and matching unique instanceId values
(e.g., "ignoredEditions" / "ignoredEpisodeTitles"), leaving existing value
handlers (values.ignoredEditions, values.ignoredEpisodeTitles) and
onChange/setFieldValue logic untouched; use inputId and instanceId as
react-select v5.10.2 standard props rather than aria-labelledby.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/lib/scanners/plex/index.ts`:
- Around line 395-407: The current guard skips calling processShow when
hasAnyEpisodes is false, which leaves existing media availability stale; always
invoke processShow for the show when it already exists so season availability
and media.status are recalculated even if all episodes were filtered.
Specifically, remove or bypass the hasAnyEpisodes-only condition around the call
to this.processShow (referencing mediaIds.tmdbId, tvdbId, hasAnyEpisodes) and
ensure you check for an existing show via the BaseScanner.getExisting pattern
(or call getExisting first) and then call this.processShow with the same payload
({ mediaAddedAt: new Date(metadata.addedAt * 1000), ratingKey, title:
metadata.title }) so the DB seasons/status are updated even when no episodes
survive filtering. Ensure you preserve creating new shows only when appropriate
and follow existing scanner semantics used by Sonarr/Jellyfin.

---

Nitpick comments:
In `@src/components/Settings/SettingsPlex.tsx`:
- Around line 420-438: The dedupe implementation using new Map(...) can cause
tuple inference issues; replace the dedupe function with a Set-based approach
(e.g., Array.from(new Set(arr))) and update normalize to use that dedupe;
additionally, when preparing the payload for axios.post in the SettingsPlex
component, ensure ignoredEpisodeFilterMode is coerced to 'season' if normalized
ignoredEpisodeTitles is empty (so the persisted mode can't remain a non-default
when titles are blank); adjust references to dedupe, normalize,
ignoredEpisodeTitles, and ignoredEpisodeFilterMode in that submission block
accordingly.
- Around line 643-766: The react-select controls (CreatableSelect components
used for ignoredEditions and ignoredEpisodeTitles) need inputId and instanceId
props so the existing <label htmlFor="ignoredEditions"> and <label
htmlFor="ignoredEpisodeTitles"> correctly associate and focus the underlying
input; update the CreatableSelect for ignoredEditions and ignoredEpisodeTitles
to include inputId="ignoredEditions" (and inputId="ignoredEpisodeTitles") and
matching unique instanceId values (e.g., "ignoredEditions" /
"ignoredEpisodeTitles"), leaving existing value handlers
(values.ignoredEditions, values.ignoredEpisodeTitles) and onChange/setFieldValue
logic untouched; use inputId and instanceId as react-select v5.10.2 standard
props rather than aria-labelledby.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5e257b0-f204-4652-b4d3-f02a9f70b463

📥 Commits

Reviewing files that changed from the base of the PR and between ee7cc5a and 62697d6.

📒 Files selected for processing (7)
  • server/api/plexapi.ts
  • server/lib/availabilitySync.ts
  • server/lib/scanners/plex/index.ts
  • server/lib/settings/index.ts
  • server/utils/plexFilter.ts
  • src/components/Settings/SettingsPlex.tsx
  • src/i18n/locale/en.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/lib/availabilitySync.ts
  • server/api/plexapi.ts
  • server/lib/settings/index.ts
  • src/i18n/locale/en.json
  • server/utils/plexFilter.ts

Comment on lines +395 to 407
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,
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "index.ts" -path "*/scanners/plex/*" | head -1

Repository: seerr-team/seerr

Length of output: 95


🏁 Script executed:

find . -name "baseScanner.ts" -path "*/scanners/*" | head -1

Repository: seerr-team/seerr

Length of output: 96


🏁 Script executed:

wc -l server/lib/scanners/plex/index.ts

Repository: seerr-team/seerr

Length of output: 97


🏁 Script executed:

sed -n '390,415p' server/lib/scanners/plex/index.ts

Repository: seerr-team/seerr

Length of output: 835


🏁 Script executed:

wc -l server/lib/scanners/baseScanner.ts

Repository: seerr-team/seerr

Length of output: 98


🏁 Script executed:

sed -n '240,310p' server/lib/scanners/baseScanner.ts

Repository: seerr-team/seerr

Length of output: 2265


🏁 Script executed:

sed -n '1,50p' server/lib/scanners/baseScanner.ts

Repository: seerr-team/seerr

Length of output: 1274


🏁 Script executed:

sed -n '310,390p' server/lib/scanners/baseScanner.ts

Repository: seerr-team/seerr

Length of output: 3360


🏁 Script executed:

rg "getExisting" server/lib/scanners/baseScanner.ts -A 5 -B 2

Repository: seerr-team/seerr

Length of output: 807


🏁 Script executed:

rg "availability" server/lib/scanners/ -i --max-count=20

Repository: seerr-team/seerr

Length of output: 239


🏁 Script executed:

sed -n '376,420p' server/lib/scanners/baseScanner.ts

Repository: seerr-team/seerr

Length of output: 1544


🏁 Script executed:

sed -n '370,390p' server/lib/scanners/plex/index.ts

Repository: seerr-team/seerr

Length of output: 710


🏁 Script executed:

rg "processShow|processMedia" server/lib/scanners/plex/index.ts -B 5 -A 2

Repository: seerr-team/seerr

Length of output: 335


🏁 Script executed:

sed -n '410,460p' server/lib/scanners/baseScanner.ts

Repository: seerr-team/seerr

Length of output: 1652


🏁 Script executed:

rg "mediaRepository.save|mediaRepository.update" server/lib/scanners/baseScanner.ts -B 2 -A 2

Repository: seerr-team/seerr

Length of output: 712


🏁 Script executed:

sed -n '350,410p' server/lib/scanners/plex/index.ts

Repository: seerr-team/seerr

Length of output: 1956


🏁 Script executed:

rg "ignored|filter|delete.*season|availability.*sync" server/lib/scanners/ -i -B 2 -A 2 | head -50

Repository: seerr-team/seerr

Length of output: 3072


🏁 Script executed:

grep -rn "MediaStatus.UNKNOWN\|MediaStatus.DELETED" server/lib/scanners/baseScanner.ts | head -20

Repository: seerr-team/seerr

Length of output: 1136


🏁 Script executed:

sed -n '455,480p' server/lib/scanners/baseScanner.ts

Repository: seerr-team/seerr

Length of output: 1159


🏁 Script executed:

rg "MediaStatus.UNKNOWN|refresh|cleanup|sync.*availability" server/lib -i -B 2 -A 2 | head -60

Repository: seerr-team/seerr

Length of output: 3025


🏁 Script executed:

rg "this\.getExisting" server/lib/scanners/ -B 2 -A 2

Repository: seerr-team/seerr

Length of output: 727


🏁 Script executed:

sed -n '1,100p' server/lib/scanners/plex/index.ts | head -50

Repository: seerr-team/seerr

Length of output: 1946


🏁 Script executed:

grep -A 20 "class PlexScanner" server/lib/scanners/plex/index.ts

Repository: seerr-team/seerr

Length of output: 637


🏁 Script executed:

rg "availability-sync" server/lib -B 5 -A 10

Repository: seerr-team/seerr

Length of output: 1807


🏁 Script executed:

grep -n "private\|protected\|public" server/lib/scanners/baseScanner.ts | grep -i "getexisting\|processshow"

Repository: seerr-team/seerr

Length of output: 165


🏁 Script executed:

find server -name "*availability*" -o -name "*sync*" | grep -i availability

Repository: seerr-team/seerr

Length of output: 90


🏁 Script executed:

rg "availability.sync|availabilitySync" server/ -B 5 -A 15

Repository: seerr-team/seerr

Length of output: 10499


🏁 Script executed:

grep -rn "MediaStatus.UNKNOWN" server/lib/scanners/baseScanner.ts | grep -i "season\|status ="

Repository: seerr-team/seerr

Length of output: 42


🏁 Script executed:

wc -l server/lib/availabilitySync.ts

Repository: seerr-team/seerr

Length of output: 95


🏁 Script executed:

sed -n '1,100p' server/lib/availabilitySync.ts

Repository: seerr-team/seerr

Length of output: 3779


🏁 Script executed:

rg "MediaStatus\." server/lib/availabilitySync.ts -B 2 -A 2

Repository: seerr-team/seerr

Length of output: 4020


🏁 Script executed:

sed -n '200,350p' server/lib/availabilitySync.ts | grep -A 30 "showExists"

Repository: seerr-team/seerr

Length of output: 3798


🏁 Script executed:

sed -n '300,450p' server/lib/availabilitySync.ts

Repository: seerr-team/seerr

Length of output: 4859


🏁 Script executed:

sed -n '600,700p' server/lib/availabilitySync.ts | head -50

Repository: seerr-team/seerr

Length of output: 1423


🏁 Script executed:

rg "mediaExistsInPlex" server/lib/availabilitySync.ts -A 20 | head -40

Repository: seerr-team/seerr

Length of output: 1546


🏁 Script executed:

git log --oneline -p server/lib/scanners/plex/index.ts | grep -B 20 -A 5 "hasAnyEpisodes" | head -50

Repository: seerr-team/seerr

Length of output: 1043


🏁 Script executed:

rg "processShow" server/lib/scanners/ -B 3 -A 1 | head -60

Repository: seerr-team/seerr

Length of output: 1798


Risk: Skipping processShow when all episodes are filtered leaves stale TV availability in the database.

When a show already exists in the database marked as AVAILABLE and a subsequent scan finds hasAnyEpisodes === false (all episodes filtered by the ignored titles setting), processShow is skipped entirely. This means the show's season statuses and overall availability status are never updated in the database. Since processShow is where season availability is recalculated and media.status is refreshed (lines 455–495 in baseScanner.ts), skipping it allows stale AVAILABLE/PARTIALLY_AVAILABLE records to persist indefinitely.

The availability-sync job (scheduled daily) does not address this scenario—it only marks shows as DELETED if they're genuinely missing from the external source, not if all episodes are filtered by user settings.

Note: The Sonarr and Jellyfin scanners do not have this guard and always call processShow, creating inconsistent behavior across scanners.

Recommended action: Consider updating the show if it already exists in the database, even when hasAnyEpisodes === false, so that season and media statuses reflect the current filtered state. The getExisting method is protected in BaseScanner; confirm the preferred approach within this codebase's patterns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/scanners/plex/index.ts` around lines 395 - 407, The current guard
skips calling processShow when hasAnyEpisodes is false, which leaves existing
media availability stale; always invoke processShow for the show when it already
exists so season availability and media.status are recalculated even if all
episodes were filtered. Specifically, remove or bypass the hasAnyEpisodes-only
condition around the call to this.processShow (referencing mediaIds.tmdbId,
tvdbId, hasAnyEpisodes) and ensure you check for an existing show via the
BaseScanner.getExisting pattern (or call getExisting first) and then call
this.processShow with the same payload ({ mediaAddedAt: new
Date(metadata.addedAt * 1000), ratingKey, title: metadata.title }) so the DB
seasons/status are updated even when no episodes survive filtering. Ensure you
preserve creating new shows only when appropriate and follow existing scanner
semantics used by Sonarr/Jellyfin.

@Rubeanie Rubeanie force-pushed the feature/plex-ignored-editions branch from 62697d6 to 0d494e6 Compare March 7, 2026 06:02
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/components/Settings/SettingsPlex.tsx (1)

420-428: Normalization logic is correct but dedupe implementation could be simplified.

The dedupe function using Map works but is unnecessarily complex since after toLowerCase().trim(), simple [...new Set(arr)] would suffice for deduplication.

♻️ Optional simplification
-            const dedupe = (arr: string[]) => [
-              ...new Map(arr.map((s) => [s, s])).values(),
-            ];
-            const normalize = (arr?: string[]) =>
-              dedupe(
-                (arr ?? [])
-                  .map((s) => s.trim().toLowerCase())
-                  .filter((s) => s.length > 0)
-              );
+            const normalize = (arr?: string[]) =>
+              [...new Set(
+                (arr ?? [])
+                  .map((s) => s.trim().toLowerCase())
+                  .filter((s) => s.length > 0)
+              )];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/SettingsPlex.tsx` around lines 420 - 428, The dedupe
implementation is more complex than necessary; replace the Map-based logic in
the dedupe function with a Set-based deduplication (e.g., using [...new
Set(arr)]) and keep normalize as-is (it already trims, lowercases, filters empty
strings, and calls dedupe). Update the dedupe function referenced in this block
(and any callers like normalize) to return an array of unique strings using Set
to simplify and clarify the intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/Settings/SettingsPlex.tsx`:
- Around line 420-428: The dedupe implementation is more complex than necessary;
replace the Map-based logic in the dedupe function with a Set-based
deduplication (e.g., using [...new Set(arr)]) and keep normalize as-is (it
already trims, lowercases, filters empty strings, and calls dedupe). Update the
dedupe function referenced in this block (and any callers like normalize) to
return an array of unique strings using Set to simplify and clarify the intent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 434e28f3-7c9f-415d-a461-848379da4d15

📥 Commits

Reviewing files that changed from the base of the PR and between 62697d6 and 0d494e6.

📒 Files selected for processing (7)
  • server/api/plexapi.ts
  • server/lib/availabilitySync.ts
  • server/lib/scanners/plex/index.ts
  • server/lib/settings/index.ts
  • server/utils/plexFilter.ts
  • src/components/Settings/SettingsPlex.tsx
  • src/i18n/locale/en.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • server/api/plexapi.ts
  • server/lib/availabilitySync.ts
  • src/i18n/locale/en.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add media detection Priority for Radarr/Sonarr and ignore media server Availability

1 participant