feat(exclusions): add watchlist exclusion system#1178
Conversation
…t loops When Delete Sync removes content but items remain on users' Plex watchlists, the sync engine would re-request them on the next cycle. This adds an exclusion system that lets admins mark items to skip during sync. Exclusions clear automatically when users remove items from their watchlists. Backend: migration, DB methods, API routes, sync engine integration Frontend: table-based management UI with filters, sorting, per-row actions Docs: utilities page, README, intro, API reference, OpenAPI tags
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR implements a complete watchlist exclusions feature: DB migration and methods, API schemas/routes, frontend store/hooks and UI, routing/sidebar entry, and integration into the watchlist sync engine to skip excluded items. ChangesWatchlist Exclusions Feature
Sequence DiagramssequenceDiagram
participant User
participant ExclusionsPage
participant Store
participant API
participant Database
User->>ExclusionsPage: Load page
ExclusionsPage->>Store: useExclusions()
Store->>API: GET /v1/exclusions
API->>Database: getAllExclusions()
Database-->>API: exclusions with usernames
API-->>Store: response
Store-->>ExclusionsPage: exclusions, loading=false
ExclusionsPage->>ExclusionsPage: Fetch watchlist items
ExclusionsPage->>ExclusionsPage: Join and render table
User->>ExclusionsPage: Click Exclude button on row
ExclusionsPage->>Store: createExclusion(key, userIds)
Store->>API: POST /v1/exclusions
API->>Database: excludeWatchlistItem()
Database-->>API: created count
API-->>Store: response
Store->>Store: refresh exclusions
Store-->>ExclusionsPage: update state, show toast
User->>ExclusionsPage: Click Unexclude, confirm
ExclusionsPage->>Store: removeExclusion(id)
Store->>API: DELETE /v1/exclusions/:id
API->>Database: removeExclusion()
Database-->>API: success
API-->>Store: response
Store->>Store: remove from list, close modal
Store-->>ExclusionsPage: update state, show toast
flowchart TD
Start["Watchlist sync begins"] --> GetMap["Fetch exclusion map<br/>from database"]
GetMap --> ProcessItem["For each watchlist item"]
ProcessItem --> Check{"Is user excluded<br/>for this key?"}
Check -->|Yes| Skip["Skip item<br/>increment skippedDueToExclusion"]
Check -->|No| Process["Process item normally"]
Skip --> Continue["Continue to next item"]
Process --> Continue
Continue --> Complete["Finalize sync summary<br/>with skip counts"]
🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 6
🧹 Nitpick comments (1)
src/client/features/utilities/pages/exclusions.tsx (1)
157-169: ⚡ Quick winUse a precomputed exclusion lookup to avoid O(n×m) joins.
At Line 159, doing
findper row scales poorly as data grows. A memoized map keyed by${user_id}-${key}keeps lookup O(1).🤖 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/client/features/utilities/pages/exclusions.tsx` around lines 157 - 169, The current tableData computation does an O(n×m) join by calling exclusions.find for each watchlistItems row; replace this with a memoized lookup map (keyed by `${user_id}-${key}`) computed with React.useMemo (e.g., build an exclusionsByKey map from exclusions) and then map over watchlistItems to set id, isExcluded, and exclusionId by doing a constant-time lookup into exclusionsByKey; update references to exclusions, watchlistItems, and the produced exclusionId/isExcluded in the tableData useMemo dependencies accordingly.
🤖 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/client/features/utilities/hooks/useExclusions.ts`:
- Around line 50-54: The effect in useExclusions.ts calls fetchExclusions(false)
without handling rejections, which can cause unhandled promise rejections
because the store action rethrows; update the useEffect to invoke
fetchExclusions inside an async IIFE or attach a .catch() handler to handle
errors (e.g., call fetchExclusions(false).catch(err => {/* log or noop */})),
referencing the existing useEffect, fetchExclusions, and hasLoadedExclusions so
the initial fetch failure is caught and prevented from bubbling as an unhandled
rejection.
In `@src/client/features/utilities/pages/exclusions.tsx`:
- Around line 498-510: The icon-only refresh Button lacks an accessible name;
update the Button (the component rendering the refresh control that uses
onClick={handleRefresh}, disabled={isRefreshing}, and children
Loader2/RefreshCw) to include an aria-label (and optionally title) that clearly
describes the action (e.g., "Refresh exclusions" or switch to "Refreshing" when
isRefreshing is true) so screen readers can announce it; keep the existing props
and visuals but add the aria-label/title attributes to the Button element.
- Line 124: The early return when users is empty leaves the watchlist load flag
unset; update the branch in the component that currently does "if
(!users?.length) return" to mark the watchlists as loaded before returning by
calling the state updater (e.g., setHasLoadedWatchlists(true)) or otherwise
setting hasLoadedWatchlists to true, so the skeleton/unresolved initial-load
state is cleared when users length is zero; keep the existing return to avoid
running later logic after setting the flag.
In `@src/schemas/exclusions/exclusions.schema.ts`:
- Around line 19-21: The numeric exclusion IDs currently accept floats and
negatives; update the zod schemas to require positive integers by changing the
array element to .array(z.number().int().positive()) for userIds and use
.coerce.number().int().positive() for the individual userId and id parameters
(wherever the fields named userId and id are defined) so all incoming exclusion
IDs are coerced to integers and validated as positive before DB use.
In `@src/services/database/methods/exclusion.ts`:
- Around line 33-34: The insert-count logic in methods like the one setting
`inserted` uses dialect-specific shapes (`this.isPostgres` and `(result as
unknown as { rowCount: number }).rowCount` vs `(result as unknown as
number[])[0]`) which is unreliable; change the insert query that produced
`result` to include `.returning('id')` so it consistently returns inserted rows
and then compute `const inserted = Array.isArray(result) ? (result as
unknown[]).length : 0` (or simply `(result as any).length`) and return that
count (Number or 0) instead of using `rowCount`/indexing. Ensure the code that
constructs the Knex insert uses `.returning('id')` and the `inserted`
calculation uses `.length`.
---
Nitpick comments:
In `@src/client/features/utilities/pages/exclusions.tsx`:
- Around line 157-169: The current tableData computation does an O(n×m) join by
calling exclusions.find for each watchlistItems row; replace this with a
memoized lookup map (keyed by `${user_id}-${key}`) computed with React.useMemo
(e.g., build an exclusionsByKey map from exclusions) and then map over
watchlistItems to set id, isExcluded, and exclusionId by doing a constant-time
lookup into exclusionsByKey; update references to exclusions, watchlistItems,
and the produced exclusionId/isExcluded in the tableData useMemo dependencies
accordingly.
🪄 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: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 6e3e2747-ce3e-4ef2-826d-80853f977d44
⛔ Files ignored due to path filters (4)
docs/docs/api-documentation.mdis excluded by!docs/**docs/docs/intro.mdis excluded by!docs/**docs/docs/utilities/watchlist-exclusions.mdis excluded by!docs/**docs/sidebars.tsis excluded by!docs/**
📒 Files selected for processing (18)
README.mdmigrations/migrations/091_20260516_add_watchlist_exclusions.tssrc/client/components/AppSidebar.tsxsrc/client/features/utilities/components/exclusions/exclusions-delete-confirmation-modal.tsxsrc/client/features/utilities/components/exclusions/exclusions-skeleton.tsxsrc/client/features/utilities/hooks/useExclusions.tssrc/client/features/utilities/pages/exclusions.tsxsrc/client/features/utilities/store/exclusionsStore.tssrc/client/router/router.tsxsrc/plugins/external/swagger.tssrc/routes/v1/exclusions/exclusions.tssrc/schemas/exclusions/exclusions.schema.tssrc/services/database.service.tssrc/services/database/methods/exclusion.tssrc/services/database/methods/watchlist.tssrc/services/database/types/exclusion-methods.tssrc/services/watchlist-workflow/orchestration/sync-engine.tssrc/types/exclusion.types.ts
| userIds: z | ||
| .array(z.number()) | ||
| .min(1, { error: 'At least one user ID is required' }), |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For Zod 4 route/body validation, what is the recommended schema pattern to enforce IDs as positive integers (including for coerced path params)?
💡 Result:
To enforce IDs as positive integers in Zod, the recommended pattern is to use z.coerce.number combined with.int and.positive [1][2][3]. This approach is particularly effective for path parameters or query strings, where inputs are typically received as strings and require conversion [4][3]. Recommended Schema Pattern: const idSchema = z.coerce.number.int.positive; Explanation: 1. z.coerce.number: This attempts to convert the input (e.g., a string from a URL path parameter) into a number [4][3]. 2..int: This ensures the resulting number is a whole number (integer) [1][3]. 3..positive: This restricts the value to be greater than zero [1][3]. If you need to handle more complex scenarios—such as ensuring the input is strictly a string before coercion or applying custom error messages—you can use the.pipe method to chain the coercion to a more specific validation schema [5][6]: const strictIdSchema = z.coerce.string.pipe( z.coerce.number.int.positive({ message: "ID must be a positive integer" })); This pattern ensures that the data is coerced, validated as an integer, and confirmed to be positive, providing a robust way to handle route and body parameters [4][6].
Citations:
- 1: https://v4.zod.dev/api?id=integers
- 2: https://zod.dev/api
- 3: https://zod.dev/api?__from__=talkingdev&id=strings
- 4: https://medium.com/@kemaltf_/using-zod-to-validate-data-41b588a29b2b
- 5: https://stackoverflow.com/questions/79435905/coerce-string-to-literal-number-with-zod
- 6: https://www.telerik.com/blogs/zod-typescript-schema-validation-made-easy
🏁 Script executed:
cat -n src/schemas/exclusions/exclusions.schema.tsRepository: jamcalli/Pulsarr
Length of output: 2740
Constrain exclusion IDs to positive integers.
Lines 19-21, 39, and 50 accept broad numeric input (including floats and negatives). Add .int().positive() to each: .array(z.number().int().positive()) for userIds, and .coerce.number().int().positive() for userId and id parameters. This prevents invalid data from reaching database operations.
Suggested fix
export const CreateExclusionSchema = z.object({
key: z.string().trim().min(1, { error: 'Key is required' }),
userIds: z
- .array(z.number())
+ .array(z.number().int().positive())
.min(1, { error: 'At least one user ID is required' }),
})
@@
export const GetUserExclusionsParamsSchema = z.object({
- userId: z.coerce.number(),
+ userId: z.coerce.number().int().positive(),
})
@@
export const RemoveExclusionParamsSchema = z.object({
- id: z.coerce.number(),
+ id: z.coerce.number().int().positive(),
})🤖 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/schemas/exclusions/exclusions.schema.ts` around lines 19 - 21, The
numeric exclusion IDs currently accept floats and negatives; update the zod
schemas to require positive integers by changing the array element to
.array(z.number().int().positive()) for userIds and use
.coerce.number().int().positive() for the individual userId and id parameters
(wherever the fields named userId and id are defined) so all incoming exclusion
IDs are coerced to integers and validated as positive before DB use.
There was a problem hiding this comment.
Skipped — the existing api-keys.schema.ts uses bare z.number() and z.coerce.number() for IDs without .int().positive(). Kept the same convention to stay consistent.
There was a problem hiding this comment.
@AhmedNSidd, understood — sticking to the existing convention is a valid call. I'll note this pattern for future reviews.
✏️ Learnings added
Learnt from: AhmedNSidd
Repo: jamcalli/Pulsarr PR: 1178
File: src/schemas/exclusions/exclusions.schema.ts:19-21
Timestamp: 2026-05-17T02:38:58.823Z
Learning: In jamcalli/Pulsarr, ID fields in Zod schemas (both body arrays and coerced path params) intentionally use bare `z.number()` and `z.coerce.number()` without `.int().positive()`, following the convention established in `src/schemas/api-keys/api-keys.schema.ts`. Do not flag the absence of `.int().positive()` on ID fields in this codebase.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: jamcalli
Repo: jamcalli/Pulsarr PR: 842
File: src/services/database/methods/analytics.ts:197-200
Timestamp: 2025-12-29T18:26:34.049Z
Learning: In SQLite usage within TypeScript code, ensure JSON_GROUP_ARRAY(DISTINCT col) is used for distinct aggregation of a single column. Do not use GROUP_CONCAT(DISTINCT col, delimiter) because the 2-argument form with DISTINCT and a delimiter is invalid. When reviewing analytics-related SQL strings in src/services/database/methods/analytics.ts, prefer JSON_GROUP_ARRAY(DISTINCT <column>) for distinct arrays and avoid the invalid GROUP_CONCAT(DISTINCT <column>, <delimiter>) pattern.
Learnt from: jamcalli
Repo: jamcalli/Pulsarr PR: 956
File: src/utils/logger.ts:234-235
Timestamp: 2026-02-02T21:38:02.949Z
Learning: In code that formats dates using pino-pretty's translateTime with the dateformat library, prefer the Z token for timezone output when you want human-readable US timezone abbreviations (EST, PST, MDT, etc.). For non-US timezones, Z falls back to GMT/UTC offsets like GMT-0500. Avoid relying on the o token, which outputs numeric offsets like -0500. This guidance applies to any TS file in the repo that uses translateTime with dateformat.
Learnt from: jamcalli
Repo: jamcalli/Pulsarr PR: 958
File: src/services/sonarr.service.ts:109-125
Timestamp: 2026-02-03T00:30:52.801Z
Learning: Ensure that URL inputs are validated to include http:// or https:// by parsing with new URL(url) and checking the protocol before use. Apply this validation in URL-related schemas (e.g., HttpUrlSchema, HttpUrlOptionalSchema) so that any non-empty baseUrl is validated prior to reaching service code, preventing malformed URLs from being stored or processed.
Learnt from: jamcalli
Repo: jamcalli/Pulsarr PR: 1060
File: src/server.ts:34-37
Timestamp: 2026-03-16T07:24:04.884Z
Learning: Do not rely on request.socket?.remoteAddress for authentication or access-control decisions. Use request.ip (as resolved by proxy-addr or equivalent) and ensure trustProxy is correctly configured. This guidance applies across TS codebases reviewing Node.js web servers behind proxies; verify that proxy trust settings are in place and that IP decisions use the framework-provided, proxy-aware value rather than raw socket addresses.
- Handle zero-users case in watchlist fetch (avoids permanent skeleton)
- Set hasLoadedExclusions on fetch failure (avoids stuck loading state)
- Add aria-label to refresh button for accessibility
- Use .returning('id') for insert count instead of dialect-specific logic
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/client/features/utilities/pages/exclusions.tsx (1)
161-173: ⚡ Quick winConsider using a Map for O(1) exclusion lookups.
The current implementation is O(n×m) due to
.find()inside.map(). Pre-building a keyed Map would reduce this to O(n+m).♻️ Proposed refactor
const tableData = React.useMemo<ExclusionTableRow[]>(() => { + const exclusionMap = new Map( + exclusions.map((e) => [`${e.user_id}-${e.key}`, e]) + ) return watchlistItems.map((item) => { - const exclusion = exclusions.find( - (e) => e.key === item.key && e.user_id === item.userId, - ) + const exclusion = exclusionMap.get(`${item.userId}-${item.key}`) return { ...item, id: `${item.userId}-${item.key}`, isExcluded: !!exclusion, exclusionId: exclusion?.id ?? null, } }) }, [watchlistItems, exclusions])🤖 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/client/features/utilities/pages/exclusions.tsx` around lines 161 - 173, The tableData creation is currently O(n*m) because it calls exclusions.find() for each watchlist item; instead, inside the same React.useMemo for tableData, first build a Map keyed by the same identity used for row ids (e.g. `${e.user_id}-${e.key}`) from the exclusions array, then replace the .find() with a constant-time Map lookup to set isExcluded and exclusionId for each watchlist item (keep the same row shape: id: `${item.userId}-${item.key}`, isExcluded, exclusionId); ensure types (ExclusionTableRow) still align and keep the useMemo dependency array as [watchlistItems, exclusions].
🤖 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/client/features/utilities/pages/exclusions.tsx`:
- Around line 155-159: The effect's guard uses users?.length which is falsy for
an empty array and prevents fetchAllWatchlistItems from running; update the
React.useEffect condition to check for users being defined (e.g., isInitialized
&& users !== undefined && !hasLoadedWatchlists) or simply remove the
users?.length check so fetchAllWatchlistItems is invoked when users is present
(even if empty), since fetchAllWatchlistItems already handles the empty-array
case; keep references to React.useEffect, users?.length, fetchAllWatchlistItems,
hasLoadedWatchlists, and isInitialized when making the change.
---
Nitpick comments:
In `@src/client/features/utilities/pages/exclusions.tsx`:
- Around line 161-173: The tableData creation is currently O(n*m) because it
calls exclusions.find() for each watchlist item; instead, inside the same
React.useMemo for tableData, first build a Map keyed by the same identity used
for row ids (e.g. `${e.user_id}-${e.key}`) from the exclusions array, then
replace the .find() with a constant-time Map lookup to set isExcluded and
exclusionId for each watchlist item (keep the same row shape: id:
`${item.userId}-${item.key}`, isExcluded, exclusionId); ensure types
(ExclusionTableRow) still align and keep the useMemo dependency array as
[watchlistItems, exclusions].
🪄 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: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 4b747b81-d08e-46a6-9c46-d8ba6b6f5823
📒 Files selected for processing (3)
src/client/features/utilities/pages/exclusions.tsxsrc/client/features/utilities/store/exclusionsStore.tssrc/services/database/methods/exclusion.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/services/database/methods/exclusion.ts
- src/client/features/utilities/store/exclusionsStore.ts
The useEffect guard checked users?.length which is falsy for an empty array, preventing the early-return path from setting hasLoadedWatchlists.
|
Hi @AhmedNSidd Thanks ill set some time aside to go over this. I agree that a exclusion system is desirable and has something that's been on the docket for a while. Can you explain the delete sync loop a bit though? I don't fully understand that loop as delete sync is supposed to check all watchlists prior to running, so it shouldnt actually remove anything still on users watchlists in the first place. Regardless, this is a welcomed addition. |
|
Hey @jamcalli Appreciate it. The scenario I'm hitting isn't really about delete sync specifically — it's more about wanting to clean up stale content from my library manually. My users request a lot of stuff, watch it briefly, then it sits untouched for months. Occasionally I want to clear out that stale media from Sonarr/Radarr, but since the items are still on their Plex watchlists, Pulsarr just picks them up again on the next sync cycle and re-adds them. I can't easily clean other users' watchlists for them, and unmonitoring in the arrs is a workaround but not ideal since it clutters things up. Exclusions give me a way to say "stop syncing this item" without needing users to manage their own watchlists. |
fastify-autoload derives the URL prefix from the directory structure, so routes in routes/v1/exclusions/ already have the /v1/exclusions prefix. The route handlers were redundantly specifying /exclusions again, resulting in /v1/exclusions/exclusions instead of /v1/exclusions.
When a user removes an item from their Plex watchlist and re-adds it, the exclusion for that item should be cleared so the watchlist sync can request the item again. The existing logic clears the exclusion inside deleteWatchlistItems, which only fires during the 2-hour full reconciliation — too slow for an interactive re-request flow. This routes the same clear through the real-time path. When RSS or ETag polling surfaces an item that has a matching exclusion for the user, treat that as the re-add signal: clear the exclusion and force-route the item past the categorizer's already-linked dedup so the content router actually fires. Adds findExcludedKeys helper and updates clearExclusions to return the delete count.
|
Makes sense. Without having time to thoroughly sit down and comment, but just upon a first glance the most noticeable deviation: The client is half migrated to using react query, and this appears to be using the old zustand fetch approach. Could you please mirror the query format used by session monitoring, the dashboard, and approvals etc? |
Move exclusions data fetching off the zustand store and onto the same react-query pattern used by approvals and session-monitoring: - useExclusions: query hook backed by useAppQuery, with an exclusionKeys factory for targeted invalidation - useExclusionMutations: useCreateExclusion / useRemoveExclusion using useAppMutation, both invalidating the list cache on success - Drop exclusionsStore.ts entirely — server state lives in the cache, per-row UI state moves to component-local React state (activeExcludeRowId for the create spinner, mutation.variables comparison for the delete spinner) - Clean up an unused itemKey prop on the delete confirmation modal that was tripping noUnusedParameters Mirrors the pattern flagged on the PR review.
- Remove CreateExclusionData from exclusion.types.ts. It was never imported — excludeWatchlistItem takes (key, userIds) as separate params, so the wrapper type was dead. - Group findExcludedKeys with the other read methods in exclusion.ts/exclusion-methods.ts. Was previously sandwiched between two write methods.
|
Hey @jamcalli — pushed a few updates. The big one is the react-query migration you flagged. Both the fetch and the mutations now go through While I was in there I also went back through the whole PR diff against the codebase conventions to catch anything else I might have miscopied from an older pattern. Two small things came out of that:
There's also one architectural decision I want to flag separately that came up while I was testing this end-to-end. I had the exclusion-clear inside |
The dedup gate in checkExistingApprovalRequest was sending auto_approved records to the default branch (silently skip routing) while approved records called routeUsingApprovedDecision and re-routed. Both statuses mean "this user is cleared for this content" — the only difference is whether an admin had to click a button. Falling through to the same handler makes re-requests work for users with bypass approval enabled. Surfaced while testing the watchlist exclusion re-add flow: after the exclusion cleared correctly, the content router still refused to send the item to Sonarr because of a stale auto_approved record from the original request.
|
This looks fairly good. Could I request you just clean up the jsdocs a bit? They are quite verbose. |
|
I'll try to carve some time out either this evening or in the coming days and I'll do that yeah, I had a feeling the docs were a bit too verbose as well. Also let me know if there's any other preferences you have or things you want changed with respect to how the UI looks too. I was debating between either calling it Watchlist Exclusions or just Exclusions in the sidebar. And I also added sorting ability on the "Exclude" column as well which should allow the admin user to group which watchlist items are excluded / not excluded faster. I also had the thought that we should probably also add a button for "Exclude all" which would turn into a "Unexclude all", probably would be helpful for some admin users so they wouldn't have to individually try to exclude every item that's appearing in the table list, but we can always save this for a future feature PR as well |
|
Looks pretty decent to me. It would be worthwhile to have a global setting and not per user (as the re-add clears and flow anyways). Should it just be global per item and not per specific users? This will lay the backbone for the maintainerr integration that I have been thinking through, where it will send webhooks when it removes items which will add to the exclusions list (which will not be user aware). As for the button handlers, please bind the button to the mutation's Right now you clear it in |
Hey, I've been running Pulsarr for my family's Plex server and ran into a re-request loop. Delete Sync removes content from Sonarr/Radarr, but the items stay on users' Plex watchlists. Next sync cycle, Pulsarr picks them up again and re-requests them. The only fix right now is asking everyone to manually remove items from their watchlists, which doesn't really work at scale.
This adds a watchlist exclusion system — you can mark items to skip during sync directly from a new Utilities page. Exclusions clear automatically when a user removes the item from their watchlist, so re-adding it later still works normally.
What's included
Backend
watchlist_exclusionstable (migration for SQLite and PostgreSQL)/v1/exclusions/exclusions, auto-registered via fastifyAutoloadskippedDueToExclusionstatFrontend
Docs
Testing
Been running this on my own instance. Happy to adjust anything that doesn't fit with the direction of the project.
Summary by CodeRabbit
New Features
UX
Documentation