Skip to content

feat: Add option to exclude providers from request options#2533

Open
jensdufour wants to merge 17 commits intoseerr-team:developfrom
jensdufour:develop
Open

feat: Add option to exclude providers from request options#2533
jensdufour wants to merge 17 commits intoseerr-team:developfrom
jensdufour:develop

Conversation

@jensdufour
Copy link

@jensdufour jensdufour commented Feb 20, 2026

Description

Allow users and admins to configure a list of excluded streaming providers.
When content is available on one of these providers, the title card and detail pages visually indicate it (dimmed poster) and replace the request button with a direct link to the provider's page on TMDB, using the streaming region configured in general settings.

The excluded providers list can be set globally (server settings) and overridden per-user (user/admin general settings).

This PR was developed with significant GitHub Copilot assistance (Claude Sonnet 4.6 via VS Code).
All code was reviewed, tested, and understood by the human contributor.
PR responses are also written with AI assistance.

Closes #206

How Has This Been Tested?

  • Configured excluded providers in both server settings and per-user settings
  • Verified WatchProviderSelector loads correctly and persists selections
  • Verified dimmed poster + streaming link appears on TitleCard, MovieDetails and TvDetails for content available on excluded providers
  • Verified normal request button shows for non-excluded providers
  • Verified DB migration runs cleanly on fresh install (SQLite)
  • Successful pnpm build on Node 22

Screenshots / Logs (if applicable)

2026-02-20 16_44_39-Greenshot 2026-02-20 16_44_43-Greenshot 2026-02-20 16_44_46-Greenshot

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • 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

    • New "Excluded Streaming Providers" setting in main and user settings; selections persist and are included in saved settings.
    • Added a selector control to pick providers to exclude.
  • UI / Behavior

    • Posters and title cards dim and overlay the excluded provider logo when content is available on an excluded provider.
    • When applicable, a provider-specific watch button/link replaces the standard request action.

Copilot AI and others added 12 commits February 19, 2026 19:20
Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
…r request gate

Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
…r loops

Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
… and sync API spec

Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
…n for excluded providers

Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
…rch pages

Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
… button click

Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
Co-authored-by: jensdufour <25767022+jensdufour@users.noreply.github.com>
…luded-providers

Add excluded streaming providers: dim posters, provider link button, per-user settings
@jensdufour jensdufour requested a review from a team as a code owner February 20, 2026 16:02
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an excludedWatchProviders string setting across API schema, DB, backend services, migrations, and frontend UI; persists pipe-separated provider IDs per user and surfaces the value to media/detail components to enable exclusion-aware UI and provider-specific watch links.

Changes

Cohort / File(s) Summary
API Schema
seerr-api.yml
Added excludedWatchProviders (string, example `'8
Database Migrations
server/migration/postgres/1771722789735-AddUserSettingsExcludedWatchProviders.ts, server/migration/sqlite/1771722789735-AddUserSettingsExcludedWatchProviders.ts
New migrations adding/removing excludedWatchProviders column on user_settings (Postgres and SQLite flows).
Backend Entity & Settings
server/entity/UserSettings.ts, server/lib/settings/index.ts, server/interfaces/api/settingsInterfaces.ts, server/interfaces/api/userSettingsInterfaces.ts
Added excludedWatchProviders field to UserSettings entity, MainSettings, FullPublicSettings, and public response interfaces; default main setting initialized to empty string and exposed in public settings.
Backend Routes
server/routes/user/usersettings.ts
GET/POST handlers updated to accept, persist, and return excludedWatchProviders on user settings endpoints.
Frontend Context & Types
src/context/SettingsContext.tsx, src/hooks/useUser.ts, src/pages/_app.tsx
Included excludedWatchProviders in default/current settings and user settings types/initialization.
New Hook
src/hooks/useExcludedProviders.ts
Added hook that parses pipe-delimited exclusions, derives streamingProviders, watchProviderLink, firstExcludedProvider, and isAvailableOnExcludedProvider.
Selector Component
src/components/Selector/index.tsx
Added hideRegionSelector prop, mount-guard to avoid initial onChange, sync effect for activeProviders, and refactored provider rendering.
Settings UIs
src/components/Settings/SettingsMain/index.tsx, src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx
Added WatchProviderSelector to configure excluded providers; selections serialized as pipe-delimited string and included in save payload; region-aware behavior and remount on region change.
Media Detail & Cards UI
src/components/MovieDetails/index.tsx, src/components/TvDetails/index.tsx, src/components/TitleCard/index.tsx
Integrated exclusion-aware logic: parse excluded IDs, compute firstExcludedProvider and watchProviderLink, dim posters, overlay provider logo, and conditionally show provider-specific watch links; added watchonprovider translation key.
Other
server/interfaces/..., src/hooks/useUser.ts
Small type/interface additions to surface new field across codebase.

Sequence Diagram

sequenceDiagram
    actor User
    participant SettingsUI as Settings UI
    participant Backend as Backend API
    participant DB as Database
    participant MediaUI as Media Detail UI
    participant ProviderAPI as External Provider Data

    User->>SettingsUI: Select excluded providers
    SettingsUI->>SettingsUI: Serialize to pipe-delimited string
    SettingsUI->>Backend: POST /main with excludedWatchProviders
    Backend->>DB: Persist excludedWatchProviders
    DB-->>Backend: Ack
    Backend-->>SettingsUI: Return updated settings

    Note over User,MediaUI: Later when viewing media
    User->>MediaUI: Open media detail
    MediaUI->>Backend: GET user settings
    Backend->>DB: Read excludedWatchProviders
    DB-->>Backend: Return value
    Backend-->>MediaUI: Settings with excludedWatchProviders
    MediaUI->>ProviderAPI: Fetch provider details for item
    ProviderAPI-->>MediaUI: Provider data
    MediaUI->>MediaUI: Match excluded provider, compute watch link
    MediaUI->>User: Render dimmed poster / overlay / provider watch button
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

🐰 I hop through pipes of IDs with a twitch and little cheer,
Posters dim and logos gleam when excluded providers appear.
Selections saved, links revealed, a tiny rabbit’s clap—
Hop, click, and watch—excluded paths lead users to the map.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: adding an option to exclude streaming providers from request options. It directly relates to the primary change implemented across the codebase.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

@jensdufour jensdufour changed the title Add option to exclude providers from request options feat: Add option to exclude providers from request options Feb 20, 2026
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: 3

Caution

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

⚠️ Outside diff range comments (1)
src/components/TitleCard/index.tsx (1)

569-618: ⚠️ Potential issue | 🟡 Minor

Same fallback issue when watchProviderLink is unavailable.

Consistent with MovieDetails and TvDetails, when isAvailableOnExcludedProvider is true but watchProviderLink is undefined, no button renders. The fix should be applied here as well to fall back to the request button.

Proposed fix
                 {showRequestButton &&
                   (!currentStatus ||
                     currentStatus === MediaStatus.UNKNOWN ||
-                    currentStatus === MediaStatus.DELETED) &&
-                  (isAvailableOnExcludedProvider &&
-                  firstExcludedProvider &&
-                  watchProviderLink ? (
+                    currentStatus === MediaStatus.DELETED) ? (
+                  isAvailableOnExcludedProvider &&
+                  firstExcludedProvider &&
+                  watchProviderLink ? (
                     <Tooltip ... >
                       ...
                     </Tooltip>
                   ) : (
                     <Button ... >
                       ...
                     </Button>
-                  ))}
+                  )
+                ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TitleCard/index.tsx` around lines 569 - 618, The conditional
rendering block for the provider/request button skips rendering the request
button when isAvailableOnExcludedProvider is true but watchProviderLink is
falsy; update the logic in the JSX that uses showRequestButton,
currentStatus/MediaStatus checks and the isAvailableOnExcludedProvider &&
firstExcludedProvider && watchProviderLink condition so that when
isAvailableOnExcludedProvider && firstExcludedProvider is true but
watchProviderLink is undefined it falls back to rendering the request Button
(the branch that calls setShowRequestModal) instead of rendering nothing; keep
the existing Tooltip/Button for the external link when watchProviderLink is
present and preserve onClick handlers and props.
🧹 Nitpick comments (1)
src/components/TitleCard/index.tsx (1)

104-124: Watch provider filtering fetches are reasonable given the constraints, but consider optimization strategies for scale.

The implementation correctly gates API calls behind the shouldFetchDetails guard, so no extra overhead when the feature is unused. SWR's URL-based caching prevents duplicate fetches for the same movie ID.

However, with excluded providers configured, a page loading 40+ cards will trigger that many individual API requests initially. While SWR mitigates repeated fetches, the first-page load cost remains notable.

Worth revisiting if this becomes a bottleneck in production, but current optimizations (lazy-fetch, guard, caching) are sound. If needed later, consider:

  • Backend enrichment: Include a simplified provider matching flag in the search/discover API response
  • Lazy loading: Defer details fetching until hover or card visibility (IntersectionObserver)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TitleCard/index.tsx` around lines 104 - 124, Large-scale page
loads can trigger many per-card detail fetches when excluded providers are set;
keep the existing guard (shouldFetchDetails) and SWR usage but mitigate
initial-load bursts by moving provider checks off the client: add a server-side
flag or minimal field to the primary search/discover response (e.g., include a
boolean like hasExcludedProvider or an array of provider IDs) so TitleCard no
longer needs to compute streamingProviders for every card; update the TitleCard
logic (where shouldFetchDetails, detailsUrl, useSWR, streamingProviders, and
excludedWatchProviderIds are referenced) to prefer the enriched search result
field and only fall back to fetching details via useSWR when that server-side
flag is absent or inconclusive.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 651-686: When isAvailableOnExcludedProvider is true but
watchProviderLink is undefined the UI renders nothing; update the conditional in
MovieDetails (around RequestButton / Tooltip/Button rendering) to fall back to
rendering the RequestButton (same props: mediaType="movie",
media={data.mediaInfo}, tmdbId={data.id}, onUpdate={() => revalidate()}) when
watchProviderLink is missing—i.e., render the Tooltip/Button only when
watchProviderLink exists, otherwise render RequestButton, referencing
RequestButton, isAvailableOnExcludedProvider, firstExcludedProvider, and
watchProviderLink to locate the change.

In `@src/components/Settings/SettingsMain/index.tsx`:
- Around line 425-441: WatchProviderSelector is only reading its region prop at
mount, so when values.streamingRegion changes while hideRegionSelector is true
the internal watchRegion stays stale; to fix, force a remount by adding a key
tied to the region (e.g., key={values.streamingRegion}) on the
<WatchProviderSelector> element so React recreates WatchProviderSelector when
values.streamingRegion changes, ensuring it re-initializes its internal
watchRegion from the updated region prop; leave the existing props (type,
region, hideRegionSelector, activeProviders, onChange) unchanged.

In `@src/components/TvDetails/index.tsx`:
- Around line 693-730: If isAvailableOnExcludedProvider is true but
watchProviderLink is undefined, render the RequestButton as a fallback so users
always have an action; update the conditional that currently requires both
isAvailableOnExcludedProvider and watchProviderLink (and firstExcludedProvider)
to instead branch: if watchProviderLink and firstExcludedProvider render the
Tooltip/Button block (as now), otherwise render the existing RequestButton with
the same props (mediaType="tv", onUpdate={() => revalidate()},
tmdbId={data?.id}, media={data?.mediaInfo}, isShowComplete={isComplete},
is4kShowComplete={is4kComplete}); ensure you reference RequestButton,
isAvailableOnExcludedProvider, watchProviderLink, and firstExcludedProvider in
the updated logic so the fallback displays when the TMDB link is missing.

---

Outside diff comments:
In `@src/components/TitleCard/index.tsx`:
- Around line 569-618: The conditional rendering block for the provider/request
button skips rendering the request button when isAvailableOnExcludedProvider is
true but watchProviderLink is falsy; update the logic in the JSX that uses
showRequestButton, currentStatus/MediaStatus checks and the
isAvailableOnExcludedProvider && firstExcludedProvider && watchProviderLink
condition so that when isAvailableOnExcludedProvider && firstExcludedProvider is
true but watchProviderLink is undefined it falls back to rendering the request
Button (the branch that calls setShowRequestModal) instead of rendering nothing;
keep the existing Tooltip/Button for the external link when watchProviderLink is
present and preserve onClick handlers and props.

---

Duplicate comments:
In `@src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx`:
- Around line 480-498: The WatchProviderSelector here can get stuck on the old
region when values.streamingRegion changes; to fix it either force a remount by
adding a key tied to the resolved region (e.g., key={`${values.streamingRegion
|| currentSettings.streamingRegion || 'US'}`) on the WatchProviderSelector, or
update WatchProviderSelector to respond to prop changes for region (implement
componentDidUpdate/useEffect to sync internal state when the region prop
changes). Update references around WatchProviderSelector, region prop, and
onChange/setFieldValue to ensure the selector resets when values.streamingRegion
or currentSettings.streamingRegion changes.

---

Nitpick comments:
In `@src/components/TitleCard/index.tsx`:
- Around line 104-124: Large-scale page loads can trigger many per-card detail
fetches when excluded providers are set; keep the existing guard
(shouldFetchDetails) and SWR usage but mitigate initial-load bursts by moving
provider checks off the client: add a server-side flag or minimal field to the
primary search/discover response (e.g., include a boolean like
hasExcludedProvider or an array of provider IDs) so TitleCard no longer needs to
compute streamingProviders for every card; update the TitleCard logic (where
shouldFetchDetails, detailsUrl, useSWR, streamingProviders, and
excludedWatchProviderIds are referenced) to prefer the enriched search result
field and only fall back to fetching details via useSWR when that server-side
flag is absent or inconclusive.

@jensdufour jensdufour marked this pull request as draft February 21, 2026 07:37
@jensdufour jensdufour marked this pull request as ready for review February 21, 2026 08:27
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.

Caution

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

⚠️ Outside diff range comments (2)
src/components/TvDetails/index.tsx (1)

1349-1375: ⚠️ Potential issue | 🟡 Minor

Move key prop to the outermost element in the map callback.

The key prop is on the inner <span> (line 1359) instead of the <Tooltip> wrapper. React requires the key on the outermost element returned from a map callback for proper reconciliation.

🔧 Proposed fix
                   {streamingProviders.map((p) => {
                     const isExcluded = excludedWatchProviders.includes(p.id);
                     return (
-                      <Tooltip content={p.name}>
+                      <Tooltip content={p.name} key={`provider-${p.id}`}>
                         <span
                           className={`relative transition duration-300 ${
                             isExcluded
                               ? 'opacity-100'
                               : 'opacity-50 hover:opacity-100'
                           }`}
-                          key={`provider-${p.id}`}
                         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TvDetails/index.tsx` around lines 1349 - 1375, The mapped list
puts the key on the inner <span> instead of the outermost element returned from
streamingProviders.map; move the key prop from the inner span
(key={`provider-${p.id}`}) to the <Tooltip> wrapper so the Tooltip component is
the root element for each iteration (keep the same key value), ensuring proper
reconciliation for the map that renders Tooltip -> span -> CachedImage.
src/components/MovieDetails/index.tsx (1)

1129-1155: ⚠️ Potential issue | 🟡 Minor

Move key prop to the outermost element in the map callback.

Same issue as in TvDetails: the key prop is on the inner <span> instead of the <Tooltip> wrapper.

🔧 Proposed fix
                   {streamingProviders.map((p) => {
                     const isExcluded = excludedWatchProviders.includes(p.id);
                     return (
-                      <Tooltip content={p.name}>
+                      <Tooltip content={p.name} key={`provider-${p.id}`}>
                         <span
                           className={`relative transition duration-300 ${
                             isExcluded
                               ? 'opacity-100'
                               : 'opacity-50 hover:opacity-100'
                           }`}
-                          key={`provider-${p.id}`}
                         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MovieDetails/index.tsx` around lines 1129 - 1155, The map
callback over streamingProviders places the React key on the inner <span>
instead of the outer <Tooltip>; move the key prop from the inner element to the
outer Tooltip component in the streamingProviders.map callback (remove it from
the inner span) so the Tooltip is the keyed root for each mapped item.
🧹 Nitpick comments (1)
src/components/MovieDetails/index.tsx (1)

298-316: Consider extracting shared exclusion logic.

The exclusion computation (lines 298-316) is duplicated verbatim in TvDetails. Consider extracting this to a shared utility or custom hook to reduce duplication and ensure consistent behavior across components.

♻️ Example extraction
// hooks/useExcludedProviders.ts
export function useExcludedProviders(
  watchProviders: WatchProviderRegion[] | undefined,
  streamingRegion: string,
  userExcluded: string | undefined,
  globalExcluded: string | undefined
) {
  const streamingProviderData = watchProviders?.find(
    (provider) => provider.iso_3166_1 === streamingRegion
  );
  const streamingProviders = streamingProviderData?.flatrate ?? [];
  const watchProviderLink = streamingProviderData?.link;
  
  const excludedWatchProviders = (userExcluded ?? globalExcluded ?? '')
    .split('|')
    .filter(Boolean)
    .map(Number);
    
  const firstExcludedProvider = streamingProviders.find((p) =>
    excludedWatchProviders.includes(p.id)
  );
  
  const isAvailableOnExcludedProvider =
    excludedWatchProviders.length > 0 &&
    streamingProviders.some((p) => excludedWatchProviders.includes(p.id));

  return {
    streamingProviders,
    watchProviderLink,
    excludedWatchProviders,
    firstExcludedProvider,
    isAvailableOnExcludedProvider,
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MovieDetails/index.tsx` around lines 298 - 316, Extract the
duplicated exclusion logic (the block computing streamingProviderData,
streamingProviders, watchProviderLink, excludedWatchProviders,
firstExcludedProvider, isAvailableOnExcludedProvider) into a shared helper or
custom hook (e.g., useExcludedProviders) and replace the inline code in
MovieDetails (index.tsx) and TvDetails with a call to that helper; ensure the
helper accepts watchProviders, streamingRegion,
user?.settings?.excludedWatchProviders and
settings.currentSettings.excludedWatchProviders (or their string values) and
returns the same named values so existing references to streamingProviders,
watchProviderLink, excludedWatchProviders, firstExcludedProvider, and
isAvailableOnExcludedProvider keep working.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 1129-1155: The map callback over streamingProviders places the
React key on the inner <span> instead of the outer <Tooltip>; move the key prop
from the inner element to the outer Tooltip component in the
streamingProviders.map callback (remove it from the inner span) so the Tooltip
is the keyed root for each mapped item.

In `@src/components/TvDetails/index.tsx`:
- Around line 1349-1375: The mapped list puts the key on the inner <span>
instead of the outermost element returned from streamingProviders.map; move the
key prop from the inner span (key={`provider-${p.id}`}) to the <Tooltip> wrapper
so the Tooltip component is the root element for each iteration (keep the same
key value), ensuring proper reconciliation for the map that renders Tooltip ->
span -> CachedImage.

---

Duplicate comments:
In `@src/components/Settings/SettingsMain/index.tsx`:
- Around line 411-446: The component correctly forces a remount when the
streaming region changes by adding key={values.streamingRegion} to
WatchProviderSelector; ensure this key remains on the WatchProviderSelector
component (alongside props type="movie", region={values.streamingRegion},
hideRegionSelector, activeProviders derived from
values.excludedWatchProviders.split('|').map(Number), and the onChange that sets
'excludedWatchProviders' via setFieldValue) so the selector fully resets when
streamingRegion updates.

---

Nitpick comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 298-316: Extract the duplicated exclusion logic (the block
computing streamingProviderData, streamingProviders, watchProviderLink,
excludedWatchProviders, firstExcludedProvider, isAvailableOnExcludedProvider)
into a shared helper or custom hook (e.g., useExcludedProviders) and replace the
inline code in MovieDetails (index.tsx) and TvDetails with a call to that
helper; ensure the helper accepts watchProviders, streamingRegion,
user?.settings?.excludedWatchProviders and
settings.currentSettings.excludedWatchProviders (or their string values) and
returns the same named values so existing references to streamingProviders,
watchProviderLink, excludedWatchProviders, firstExcludedProvider, and
isAvailableOnExcludedProvider keep working.

@jensdufour jensdufour marked this pull request as draft February 21, 2026 08:32
@jensdufour jensdufour marked this pull request as ready for review February 21, 2026 08:45
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.

Possibility to ignore videos from streaming services

2 participants