feat(ai): AI-powered recommendations and natural-language search#3258
feat(ai): AI-powered recommendations and natural-language search#3258rholmboe wants to merge 13 commits into
Conversation
Adds an AI recommendation engine that generates personalized movie/TV suggestions based on a user's request history, watchlist, and available library content, inspired by SuggestArr and Recomendarr. Backend: - Two-phase LLM pipeline: taste profile generation -> recommendations - Multi-provider support via OpenAI-compatible API (OpenAI, Ollama, OpenRouter, custom), with robust JSON repair + retry on parse failure - LLM titles resolved to real TMDb entries via search (small models hallucinate tmdbId values), so only valid titles are stored - TTL + upsert lifecycle: recommendations persist across runs, are refreshed when re-recommended, and age out via configurable TTL cleanup - Scheduled job (every 6h) generates recommendations for all users - API endpoints: /discover/ai-recommendations, /ai/settings, /ai/test, /ai/search, /ai/feedback, /ai/regenerate - New entities: AiRecommendation, UserFeedback (+ SQLite/Postgres migrations) - AI settings block (provider, recommendations, search) in global settings Frontend: - "Recommended for You" discover slider + dedicated /discover/ai-recommendations page - AI Settings tab (provider config, feature toggles, maxResults, minScore, TTL) - AI Recommendations Sync job registered in Jobs & Cache UI - All AI endpoints declared in the OpenAPI spec (seerr-api.yml) Composable dev setup via compose.ai.yaml (optional Ollama service). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an "AI Search" mode to the existing search page so users can describe
what they want to watch in natural language ("90s psychological thrillers"),
alongside the regular keyword search.
Backend:
- aiSearch() now returns { results, interpretation } so the UI can show how
the query was parsed
- POST /ai/search maps results through mapSearchResults + Media status
(same shape as the regular /search endpoint) so TitleCard/ListView render
unchanged in either mode
Frontend:
- useAiSearch hook (SWR, single non-paginated POST via closure-keyed fetcher)
- Search component gains an "AI Search" toggle; in AI mode it shows a
loading state, an interpretation badge (genres, years, language, min
rating, keywords), and the result grid
- Same search box as before — toggle is the only entry point, so the common
title-lookup case stays fast (no LLM call) while NL queries opt in
OpenAPI spec updated for the /ai/search response (incl. interpretation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Search quality: - Map LLM genre names to TMDb IDs per endpoint type (movie vs TV have different IDs), so /discover actually returns results instead of matching zero. A query like "dark dystopian sci-fi" now returns ~20 results (was ~5, all from suggested titles only). - Run movie/TV discover with their correct date params and genre IDs. - Sort merged search results by matchScore so LLM-suggested titles surface above discover results. Provider robustness (helps search + recommendations): - callLLM falls back to reasoning_content when content is empty, so reasoning models (GLM, o-series) work. - Connection test uses the OpenAI SDK, tolerates reasoning-model replies, and surfaces the provider's real error on failure. - GET /ai/settings returns hasApiKey (never the raw key); the test endpoint falls back to the stored key so users can re-test without re-entering it. Frontend shows a "key is saved" hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feedback loop (frontend + backend):
- AiRecommendationCard wraps TitleCard and overlays 👍/👁️/👎 buttons in
TitleCard's own visual language (ghost, sm icon buttons, hover-reveal),
placed just below the MOVIE/blocklist row.
- useAiFeedback hook submits/deletes feedback; dislike and seen remove the
card optimistically, like marks it; toggle removes; errors roll back.
- /discover/ai-recommendations renders a grid of these cards (replacing the
generic ListView) so feedback is isolated to the AI page.
- Like-biasing: generateRecommendations resolves liked titles and injects a
"TITLES THE USER EXPLICITLY LIKED" section into the recommendation prompt,
so likes now influence future generation (dislike/seen were already filtered).
UI polish:
- Fix "Unknown Slider" in the Discover edit view by mapping AI_RECOMMENDATIONS
and AI_SEARCH in DiscoverSliderEdit.
- Add a hover tooltip ("Add to Blocklist") to the shared TitleCard blocklist
button, matching the remove-from-blocklist button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-PR cleanup of the AI feature: - minScore -> minRating: the setting is wired to TMDb vote_average.gte, so relabel and rescale it as a minimum rating (0-10, default 7) instead of a 0-1 "confidence score" that did nothing at the default. Updated across the settings interface, route, UI, and docs. Also fixed the PUT handler to use `!== undefined` so a 0 floor actually saves (the old truthiness check was falsy for 0). - i18n: run `pnpm i18n:extract` (the step skipped when the feature landed) so all AI strings -- including the AI Recommendations Sync job label -- are in en.json and exposed to Weblate. 58 keys added, none removed. - Lint (52 -> 0 errors): convert relative imports to @server/@app aliases, add `import type` where type-only, remove unused imports/vars, drop the speculative userId scaffolding from the LLM client, restructure the three SettingsAi toggles to the htmlFor+id a11y pattern, and disable no-control-regex at the intentional control-char strip in JSON repair. Verified: pnpm lint (0 errors), tsc --noEmit (clean), pnpm format:check (clean). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add docs/using-seerr/settings/ai.md (sidebar_position 8) following Seerr's settings-doc voice, covering provider configuration, recommendations, AI search, feedback, and a privacy warning. Includes the Recommended for You screenshot under gen-docs/static/img/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client typecheck run by `next build` found three errors that the server-only `tsc --noEmit` didn't cover: - AiRecommendationCard: `item.title` / `item.releaseDate` only exist on MovieResult; TvResult uses `name` / `firstAirDate`. Switched to the `mediaType === 'movie'` ternary already used in PersonDetails. - SettingsAi: PageTitle only accepts `title` (the `message` prop was invalid -- the description is already rendered in the section header below). - SettingsAi: the max-results input had a duplicate `type="number"`. `pnpm build` now passes (next build + server tsc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dev container had flipped this auto-generated file to its dev-mode variant (`.next/dev/types/...`); develop uses the production variant (`.next/types/...`). Restored to match develop so the PR carries no spurious diff on this file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per CONTRIBUTING.md "UI Text Style": - "TMDb" -> "TMDB" in UI strings, docs, and comments (section 4: TMDB has a capital B; also the prevailing form across the codebase). - Form tips no longer end in punctuation (section 6): minRatingTip, ttlDaysTip, apiKeySet. - "..." -> "…" Unicode ellipsis (section 3): testing, testingConnection. User-facing strings regenerated via pnpm i18n:extract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This dev-setup note was committed by mistake in the initial AI feature commit. It is an AI-generated artifact and does not belong in the repo (CONTRIBUTING.md section 4: do not commit AI tool / non-project files). User-facing documentation lives under docs/using-seerr/settings/ai.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds opt-in AI recommendations and natural-language search using OpenAI-compatible providers, persisted feedback and recommendations, scheduled generation, new APIs, settings, Docker Compose support, and corresponding discovery and search interfaces. ChangesAI recommendation platform
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant Search
participant aiRoutes
participant aiRecommendations
participant OpenAICompatibleClient
participant TMDB
User->>Search: Enter natural-language query
Search->>aiRoutes: POST /ai/search
aiRoutes->>aiRecommendations: aiSearch(userId, query)
aiRecommendations->>OpenAICompatibleClient: interpretSearchQuery(query)
OpenAICompatibleClient-->>aiRecommendations: discover parameters and titles
aiRecommendations->>TMDB: discover and search media
TMDB-->>aiRecommendations: matching results
aiRecommendations-->>aiRoutes: results and interpretation
aiRoutes-->>Search: mapped search response
Search-->>User: AI results and interpretation
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
🧹 Nitpick comments (2)
server/entity/AiRecommendation.ts (1)
15-15: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winIndex
updatedAt, which is the actual TTL lookup column.Cleanup executes
WHERE updatedAt < cutoff; thecreatedAtindex cannot support that query and will cause table scans as recommendations accumulate.-@Index(['createdAt']) +@Index(['updatedAt'])Also applies to: 51-55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/entity/AiRecommendation.ts` at line 15, Update the entity index declaration in AiRecommendation to index updatedAt instead of createdAt, and ensure any corresponding index metadata near the referenced lines uses updatedAt so the cleanup query can use the correct lookup column.src/components/Discover/AiRecommendationCard/index.tsx (1)
49-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider removing or utilizing
SHOW_RATIONALE_BUTTON.
SHOW_RATIONALE_BUTTONis hardcoded tofalse, making the rationale tooltip code unreachable. If this is a feature flag for future use, consider adding a TODO comment; otherwise, it can be removed to reduce dead code.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Discover/AiRecommendationCard/index.tsx` around lines 49 - 50, Remove the unused SHOW_RATIONALE_BUTTON constant and the unreachable rationale tooltip code it gates, unless the feature is intended for future use; in that case, retain the flag and add a clear TODO describing when it should be enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@compose.ai.yaml`:
- Around line 29-30: Remove the Ollama port publication from the compose service
so its API remains accessible to Seerr through the internal ollama network
address. If host access is required, change the binding to localhost-only rather
than exposing port 11434 on all interfaces.
In `@docs/using-seerr/settings/ai.md`:
- Line 28: Update the Base URL documentation in the AI settings guide to include
the Docker Compose Ollama endpoint http://ollama:11434/v1, while retaining the
host-based http://localhost:11434/v1 example and clarifying when each URL
applies.
In `@server/api/ai/index.ts`:
- Around line 218-228: Validate the parsed result from extractJSON within
callForJSON before consuming it, using runtime schemas for each operation’s
expected shape. Reject or normalize invalid fields, including ensuring
recommendations is an array before slice() and discover parameters have valid
types before reaching TMDB calls; apply the same validation to the related
parsing paths around the referenced operations.
- Around line 116-122: Update callLLM’s response extraction and testConnection
to use a shared extractor that treats empty content as absent and falls back to
reasoning_content. Replace the current nullish-coalescing logic around
response.choices[0].message, including the corresponding extraction near
testConnection, while preserving the Empty response from LLM error when both
values are empty or unavailable.
In `@server/entity/AiRecommendation.ts`:
- Around line 13-21: Update AiRecommendation so userId is non-null and add a
unique constraint covering userId, tmdbId, and mediaType to prevent duplicate
recommendations. Change the timestamp index from createdAt to updatedAt, and
regenerate both migrations to reflect the schema changes.
In `@server/entity/UserFeedback.ts`:
- Around line 28-29: Update the UserFeedback entity’s feedbackType column
definition to declare an explicit database-level constraint permitting only
'like', 'dislike', and 'seen'. Keep the TypeScript union and ensure the column
metadata generates equivalent validation for both SQLite and PostgreSQL
migrations.
In `@server/job/schedule.ts`:
- Around line 278-347: Add a running guard around the AI Recommendations Sync
scheduled callback so a new invocation exits when a previous run is still
active. Set the guard before asynchronous work begins and reliably clear it
after completion, including failures, while preserving the existing
disabled-settings and per-user error handling; use the established
running-pattern symbol if available, or a distributed lock when multiple
instances can execute the job.
In `@server/lib/aiRecommendations.ts`:
- Around line 514-536: The discovery guard before buildParams must account for
every supported interpretation field, including year_to, original_language, and
sort_by, so queries containing only those values still run discovery. Update the
condition around buildParams, or use the normalized parameter object directly,
while preserving the existing parameter construction.
- Around line 565-579: Update the TMDB resolution logic around the title-search
loop and the positive-ID handling near the related resolution block to use one
shared resolver that validates title, year, and media type before accepting a
match. Validate model-generated IDs instead of trusting any positive ID, and
rank candidates by media type first, then year, so remakes and movie/TV
mismatches are rejected or deprioritized consistently.
- Around line 670-716: Normalize recommendation identity through a shared helper
that extracts tmdbId/id and mediaType/media_type, then keys identities as the
media type plus TMDB ID. Update the merged map in the recommendation merge flow
and the filtering logic in filterExistingContent to use this normalized
identity, preventing movie and TV collisions and ensuring AI search results are
excluded when watched or disliked. Remove the untyped excludedTmdbIds set and
apply the same helper across the additional recommendation paths.
In `@server/routes/ai.ts`:
- Around line 60-68: Update the provider settings logic in the request handler
so changing provider.type or provider.baseUrl clears the existing apiKey when no
replacement key is supplied. Preserve the current key only when the destination
remains unchanged, while continuing to apply a provided apiKey as the
replacement credential.
- Around line 134-143: Bind stored API keys to the provider destination they
were saved with: in server/routes/ai.ts lines 134-143, do not reuse the stored
key when the request provider type or base URL differs; in server/routes/ai.ts
lines 60-68, clear the saved key or require a replacement when saving a changed
destination; in src/components/Settings/SettingsAi/index.tsx lines 316-320, show
key-preservation guidance only when the destination remains unchanged, using the
existing provider and destination comparison symbols.
In `@server/routes/discover.ts`:
- Around line 1022-1055: Limit concurrency in the detailedResults flow around
the Promise.all over stored by processing recommendations in small sequential
batches, with each batch using Promise.all for parallel detail fetches. Preserve
the existing per-item try/catch behavior and result mapping, then combine all
batch results while retaining null entries for failed requests.
In `@server/routes/index.ts`:
- Line 152: Update the AI route mounting around aiRoutes so the global
administration endpoints /ai/settings and /ai/test require both authentication
and Permission.ADMIN authorization, while preserving regular authenticated
access to search, feedback, and regeneration routes. Apply the guard only to the
affected administrative endpoints rather than restricting the entire aiRoutes
mount.
In `@src/components/Discover/index.tsx`:
- Around line 412-423: The AI_SEARCH case in the Discover slider configuration
must use a GET-compatible endpoint and provide the required query parameter
through MediaSlider’s extraParams, or otherwise reuse the established
TMDB_SEARCH configuration if AI results cannot be fetched by MediaSlider’s
useSWRInfinite GET flow. Update the MediaSlider props in the AI_SEARCH branch
without routing it to the POST-only /api/v1/ai/search endpoint.
In `@src/components/Search/index.tsx`:
- Around line 125-135: Update the regular-search rendering branch in the Search
component to check regular.error before rendering ListView and display the
Next.js error page for failed requests. Import or alias that page as ErrorPage
to avoid shadowing the global Error constructor, while preserving the existing
ListView behavior when no error exists.
- Around line 42-48: Route all new user-facing text through react-intl: update
the interpretation field labels in src/components/Search/index.tsx lines 42-48,
the loading-state text in src/components/Settings/SettingsAi/index.tsx lines
143-147, and the provider section heading in
src/components/Settings/SettingsAi/index.tsx lines 216-220. Add or reuse
appropriate message identifiers and render each translated message through the
existing component internationalization pattern.
- Around line 110-120: Render only one empty-state message for AI search
results: update the ListView usage and surrounding conditional in the Search
component so an empty result set does not trigger both ListView’s global message
and the aiNoResults branch. Preserve the AI-specific message by disabling the
ListView empty state for this path or removing the redundant branch, using the
existing symbols ListView, isEmpty, and messages.aiNoResults.
In `@src/components/Settings/SettingsAi/index.tsx`:
- Around line 316-320: Update the provider API-key handling in the SettingsAi
component so changing the provider type or baseUrl clears the preserved
credential or requires a replacement key before submission. Do not display the
apiKeySet message or allow a blank field to reuse the old key when either
destination property changes; preserve the current behavior only when the
destination remains unchanged.
In `@src/hooks/useAiSearch.ts`:
- Around line 46-53: Update the useAiSearch SWR key so it includes the
authenticated user identity alongside the query, ensuring history-aware results
are not shared across accounts; preserve the null key behavior when no query is
provided and use the existing auth/user source available to the hook.
---
Nitpick comments:
In `@server/entity/AiRecommendation.ts`:
- Line 15: Update the entity index declaration in AiRecommendation to index
updatedAt instead of createdAt, and ensure any corresponding index metadata near
the referenced lines uses updatedAt so the cleanup query can use the correct
lookup column.
In `@src/components/Discover/AiRecommendationCard/index.tsx`:
- Around line 49-50: Remove the unused SHOW_RATIONALE_BUTTON constant and the
unreachable rationale tooltip code it gates, unless the feature is intended for
future use; in that case, retain the flag and add a clear TODO describing when
it should be enabled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d8d37c49-a223-401a-a6f0-d45a0fc6d4b9
⛔ Files ignored due to path filters (1)
gen-docs/static/img/seerr-ai-recommend.pngis excluded by!**/*.png
📒 Files selected for processing (31)
compose.ai.yamldocs/using-seerr/settings/ai.mdpackage.jsonseerr-api.ymlserver/api/ai/index.tsserver/constants/discover.tsserver/entity/AiRecommendation.tsserver/entity/UserFeedback.tsserver/job/schedule.tsserver/lib/aiRecommendations.tsserver/lib/settings/index.tsserver/migration/postgres/1784393737543-AddAiRecommendations.tsserver/migration/sqlite/1784393737543-AddAiRecommendations.tsserver/routes/ai.tsserver/routes/discover.tsserver/routes/index.tssrc/components/Discover/AiRecommendationCard/index.tsxsrc/components/Discover/AiRecommendations.tsxsrc/components/Discover/DiscoverSliderEdit/index.tsxsrc/components/Discover/constants.tssrc/components/Discover/index.tsxsrc/components/Search/index.tsxsrc/components/Settings/SettingsAi/index.tsxsrc/components/Settings/SettingsJobsCache/index.tsxsrc/components/Settings/SettingsLayout.tsxsrc/components/TitleCard/index.tsxsrc/hooks/useAiFeedback.tssrc/hooks/useAiSearch.tssrc/i18n/locale/en.jsonsrc/pages/discover/ai-recommendations.tsxsrc/pages/settings/ai.tsx
Security: - Require admin (Permission.ADMIN) on /api/v1/ai/settings and /ai/test. - Don't reuse a saved API key when the provider type or base URL changes: clear it on save, and in /test only fall back to the stored key for the same destination. SettingsAi shows the key-preservation hint only when the destination is unchanged. Correctness: - AI search discovery now runs for any interpretation field (year_to, original_language, sort_by), not just genres/year_from/min_rating. - Stop trusting model-supplied tmdbId values; always resolve by title+yr. - Key recommendation identity by mediaType+tmdbId so movie/TV no longer collide (merge, search-merge, and feedback exclusion). - Search: single empty state for AI results; render ErrorPage on regular-search failure; internationalize the interpretation labels. Robustness: - Guard the AI Recommendations Sync job against overlapping runs. - Batch TMDB detail fetches (chunks of 5) in /discover/ai-recommendations. - Validate parsed recommendations is an array before slicing. Schema: - AiRecommendation: userId NOT NULL, unique(userId,tmdbId,mediaType), and index on updatedAt (matches the TTL cleanup query). Both migrations updated. Misc: - i18n: SettingsAi loading/Provider Configuration strings; include the user id in the useAiSearch SWR key. - compose.ai.yaml: bind the Ollama port to 127.0.0.1. - docs: note the Docker-internal Ollama base URL. - Remove the unused/broken AI_SEARCH discover slider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/Search/index.tsx`:
- Around line 91-93: Remove the top-level regular search error return from the
component, and move the regular.error handling into the non-AI rendering branch
that displays ListView. In AI mode, continue rendering the AI results and toggle
even when the background useDiscover request fails; only replace the regular
search view with ErrorPage when that branch is active.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3f70e8d0-b435-49fa-899a-d3b16e33dfd2
📒 Files selected for processing (18)
compose.ai.yamldocs/using-seerr/settings/ai.mdserver/api/ai/index.tsserver/constants/discover.tsserver/entity/AiRecommendation.tsserver/job/schedule.tsserver/lib/aiRecommendations.tsserver/migration/postgres/1784393737543-AddAiRecommendations.tsserver/migration/sqlite/1784393737543-AddAiRecommendations.tsserver/routes/ai.tsserver/routes/discover.tssrc/components/Discover/DiscoverSliderEdit/index.tsxsrc/components/Discover/constants.tssrc/components/Discover/index.tsxsrc/components/Search/index.tsxsrc/components/Settings/SettingsAi/index.tsxsrc/hooks/useAiSearch.tssrc/i18n/locale/en.json
💤 Files with no reviewable changes (4)
- src/components/Discover/DiscoverSliderEdit/index.tsx
- server/constants/discover.ts
- src/components/Discover/index.tsx
- src/components/Discover/constants.ts
🚧 Files skipped from review as they are similar to previous changes (13)
- compose.ai.yaml
- docs/using-seerr/settings/ai.md
- src/hooks/useAiSearch.ts
- server/job/schedule.ts
- server/routes/discover.ts
- server/entity/AiRecommendation.ts
- server/migration/sqlite/1784393737543-AddAiRecommendations.ts
- server/migration/postgres/1784393737543-AddAiRecommendations.ts
- src/i18n/locale/en.json
- server/lib/aiRecommendations.ts
- server/routes/ai.ts
- src/components/Settings/SettingsAi/index.tsx
- server/api/ai/index.ts
The top-level regular.error check returned ErrorPage even in AI mode, where the background keyword search can fail independently of the AI view. Move the check into the non-AI branch so AI results and the toggle keep rendering; only the regular-search view swaps to ErrorPage. Addresses a CodeRabbit follow-up on seerr-team#3258. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dedupe the content/reasoning_content extraction now shared by callLLM and testConnection. Also fixes testConnection to honor reasoning_content (reasoning models like GLM/o-series may put the answer there), which the previous content-only check missed. Addresses a CodeRabbit review comment on seerr-team#3258. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Description
This adds an optional, fully opt-in AI layer to Seerr: personalized recommendations and natural-language search powered by any OpenAI-compatible LLM provider. Everything is off by default — Seerr behaves exactly as before until an administrator enables it.
Personalized recommendations — a "Recommended for You" discover slider and a dedicated page, generated per user from their requests, watchlist, and available library via a two-phase pipeline (taste profile → recommendations). Titles returned by the model are resolved to real TMDB entries by title/year search (model-supplied
tmdbIdvalues are deliberately not trusted). Recommendations persist between runs and age out via a configurable TTL rather than being regenerated wholesale.Natural-language search — an "AI Search" toggle on the search page. Regular keyword search stays the default (fast, no LLM); in AI mode the query is interpreted into TMDB discover parameters plus suggested titles, resolved to results, and shown with an "AI interpretation" badge describing how the query was parsed.
Feedback loop — 👍 / 👁️ / 👎 on recommendation cards. Likes bias future generation toward similar content; "already watched" and "not interested" remove the card and exclude the title going forward.
Provider-agnostic and self-hostable — works with OpenAI, OpenRouter, Ollama, LM Studio, LiteLLM, etc., so it can run entirely locally. Configured under a new Settings → AI Settings tab (provider, model, max results, minimum TMDB rating, recommendation TTL).
Inspired by the community projects SuggestArr and Recomendarr; reuses Seerr's existing discover-slider, settings, and scheduled-job infrastructure.
AI assistance disclosure: This feature was developed with AI assistance (Claude, via Claude Code) and the commits are co-authored accordingly. AI was used for substantial code generation across the feature. All design decisions, scope, and trade-offs were driven and reviewed by me as the contributor, every change was reviewed before commit, and the feature was manually validated end-to-end (see below). This is AI-assisted work under human review — not autonomous generation.
How Has This Been Tested?
Manual testing in a Docker dev container against an OpenAI-compatible provider (OpenRouter,
tencent/hy3:free):Automated checks:
pnpm build(next build + server tsc) passes,pnpm lint(0 errors),tsc --noEmit(clean),pnpm format:check(clean),pnpm i18n:extract(run; 58 keys added, none removed).Screenshots / Logs (if applicable)
The "Recommended for You" page is included in the new docs page at
gen-docs/static/img/seerr-ai-recommend.png.Checklist:
pnpm buildpnpm i18n:extractSummary by CodeRabbit