Skip to content

feat(watchlist): add titles that aren't in the library yet - #1711

Merged
Quick104 merged 12 commits into
mainfrom
t3code/watchlist-external-items
Sep 30, 2026
Merged

Quick104 merged 12 commits into
mainfrom
t3code/watchlist-external-items

Conversation

@Quick104

@Quick104 Quick104 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Related issue: N/A
Validation tasks: changes #1183 C1, C2; changes #1184 C1, C2, C3; changes #1202 C1, C2, C3, C4; changes #1203 C1; changes #1204 C1, C2, C3

A profile can only put titles that are already in the library on its
watchlist. Someone who finds a movie or series in Discover can request it but
can't save it for later. (Watch-provider sync drops Trakt, Simkl and MDBList
watchlist items that aren't in the library too. Importing those into the new
storage is planned follow-up work and not part of this PR.)

This change lets a profile add a TMDB movie or series to its watchlist before
the server has it. The watchlist page gets a "Not in your library yet" tab for
these titles. When a title arrives in the library, its entry moves to the
normal watchlist with its original date. Adding can also request the title,
so the existing "request fulfilled" notification tells the profile when it's
ready.

Approach

  • Storage. Titles outside the library live in their own tables:

    • watchlist_titles: one row per title, shared by every profile that adds
      it, with the current IDs and a display snapshot;
    • watchlist_title_aliases: every TMDB, IMDb or TVDB ID the title has had;
    • user_watchlist_titles: one row per profile entry.

    Nothing is written to media_items, and no catalog content_id is stored,
    so catalog queries and the content-ID rename and merge paths are unchanged.
    Placeholder catalog rows were ruled out because every catalog query would
    need to filter them out.

  • Arrival. Whether a title is in the library is worked out when it's
    needed, by matching its aliases against media_items,
    media_item_provider_ids and stale_media_ids in enabled libraries. The
    entry moves onto user_watchlist when the profile reads the watchlist (v1
    and v2), the catalog with source=watchlist, the home watchlist row, or the
    item's detail page. The move keeps the original added_at and fires the
    normal watchlist side effects once. There is no scheduled task. The item
    list, people and jellycompat reads don't move entries.

  • Changing IDs. Viewing a title's Discover page refreshes its stored IDs
    from the TMDB response the page already fetches. Reading the list checks up
    to three overdue titles in the background, with a one-hour claim so only one
    node checks each title. A TMDB 404 is only acted on after a second 404 at
    least 24 hours later. The title is then found again through its IMDb or TVDB
    ID (TMDB /find). If that finds another stored title, the two merge. Titles
    TMDB lists twice are marked as needing attention, and titles TMDB no longer
    lists are marked as such. Entries are never removed automatically.

  • Requests. Adding requests the title, or follows someone else's open
    request for it, when all of these hold:

    • the server setting request_settings.watchlist_requests is on (default on);
    • the profile setting requests.watchlist_auto_request is on (default on);
    • the viewer is allowed to request.

    Limits, approval and routing work as they do for the Request button.
    Requests created this way are recorded with media_requests.source = 'watchlist'. Removing a title cancels its request only if the watchlist
    created it and nothing has been sent to a download server yet.

  • API. /api/v2 additions only; /api/v1 and jellycompat are unchanged:

    • list, add and delete under /api/v2/watchlist/titles;
    • in_watchlist on Discover results and detail;
    • watchlist_titles_supported and watchlist_requests on the requests
      status;
    • source on requests.

    With requests disabled, the new operations answer 409 and the web shows no
    entry points.

  • Web.

    • The watchlist page has two tabs, "In your library" and "Not in your
      library yet". The second explains that its titles can't be played yet.
    • The poster status is a new request_status card overlay badge, so it
      follows the profile's overlay style, corner and on/off settings. Downloads
      also get the Continue Watching progress bar.
    • The caption under the poster always states the status in words.
    • Discover pages get an Add to Watchlist button.
    • A new Settings › Requests page holds the profile setting.
  • Settings contract. Revision 15 adds requests.watchlist_auto_request and
    the request_status overlay id.

  • Related fix. The notification interest hook now wraps AddToWatchlistAt.
    Before, series added to a watchlist by watch-provider and Plex imports never
    queued an interest recompute.

Validation

  • Go: build, vet and gofmt are clean, and make lint-changed reports 0
    issues.
  • Focused Go tests pass, including database tests against a local PostgreSQL
    18 for watchlist (with -race), requests, apiv2, catalog, handlers,
    notifications, pgstore and sections.
  • The migrations test passes, with timezone=UTC set on the test database
    URL. make migrate-validate passes. Before rebasing onto current main, the
    three new migrations were rolled down and back up, and make test-db-pins
    passed.
  • Contracts: verify-apiv2-openapi, -web-types, -contract and -fixtures,
    verify-settings-bindings-all and verify-local-paths pass. The contract
    diff reports 25 additive changes and none breaking.
  • Web: tsc, ESLint (0 errors), Prettier and the production build pass. make test-web passes (703 files, 6073 tests), and so does the bundle budget
    check.
  • Not verified:
    • Live TMDB /find responses and TMDB's behavior for deleted IDs. The tests
      use stubs.
    • The cost of moving arrived titles on each watchlist read, at production
      scale. There is no EXPLAIN ANALYZE against a large fixture.
    • The Kotlin and Swift settings bindings. The client repos weren't checked
      out.
  • These failures happen with or without this change:
    • TestOrphanCleanupAfterPlexTableDropPostgres, TestAdminSubtitleListPageDB
      and TestCatalogTransferPersistsJobsAndSignedLink also fail on main.
    • internal/librarymonitor TestFailedBackendIsReplaced is flaky under
      full-suite load.

Risks

  • Migrations. Three new migrations. The stale_media_ids (provider, provider_id) index is built CONCURRENTLY in its own non-transactional
    migration.
  • Requests. Auto-request is on by default, so watchlisting can create
    requests. They go through the usual limits and approval, and an admin can
    turn it off in Settings › Requests.
  • Read cost. Library watchlist reads, the home watchlist row and item
    detail each run one extra indexed query.
  • One-connection pools. With database.max_connections: 1, moving an
    arrived title writes the library watchlist entry before it locks the title,
    because the lock's transaction would hold the pool's only connection. A
    remove that races the move can leave the item on the library watchlist.
  • Bundle size. web/perf-budget.json goes up by about 1.2 KB brotli. That
    covers the overlay entry, three icons and the regenerated contract types.
  • Native clients. The Apple and Android apps need follow-up work for the
    tab, the Discover button, the profile setting and the request_status
    overlay id: Watchlist titles that aren't in the library yet silo-apple#560 and Watchlist titles that aren't in the library yet silo-android#428.
    The user manual needs the new tab, button, settings and badge:
    Manual: watchlist titles that aren't in the library yet siloserver.org#63.

Checklist

  • I read and can explain the complete diff.
  • This pull request addresses one concern.

AI Disclosure

  • Harness: Claude Code, run from T3 Code
  • Tool(s): Claude Code with its Workflow subagents
  • Model(s): claude-opus-5-5[1m]
  • Involvement: Fully AI-generated. A maintainer directed the design and
    approved the spec and the UI mockups. The code has not yet been verified by
    a human.
  • Adversarial review: four independent AI review agents audited the diff
    against the design spec, covering backend correctness and concurrency, API
    contract and access control, the web client, and spec coverage. They
    reported 11 findings, 2 of them major: the tabs and the Discover button
    stayed visible with requests disabled. All 11 were fixed with tests.

🤖 Generated with Claude Code

Profiles can add a movie or series from Discover to their watchlist before
the server has it. The entry is keyed by the title's provider IDs, kept in
its own tables, and moves onto the library watchlist when the title
arrives. Adding can also request the title, so the existing fulfilled
notification tells the profile when it lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 132 files, which is 32 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 88204fa7-3f3e-4815-8857-2d50c47e30e1

📥 Commits

Reviewing files that changed from the base of the PR and between 17eced4 and 9063529.

📒 Files selected for processing (132)
  • contracts/api/v2/fixtures/admin_request_settings_ok.json
  • contracts/api/v2/fixtures/admin_requests_ok.json
  • contracts/api/v2/fixtures/cancel_request_ok.json
  • contracts/api/v2/fixtures/create_request_ok.json
  • contracts/api/v2/fixtures/get_system_info_ok.json
  • contracts/api/v2/fixtures/list_my_requests_ok.json
  • contracts/api/v2/fixtures/request_status_ok.json
  • contracts/api/v2/openapi.json
  • contracts/settings/v1/conformance.json
  • contracts/settings/v1/manifest.json
  • contracts/settings/v1/schemas/card-overlays.json
  • docs/architecture/api-contract.md
  • docs/architecture/external-watchlist.md
  • docs/architecture/media-requests.md
  • docs/catalog-api.md
  • internal/api/handlers/admin.go
  • internal/api/handlers/admin_users_service.go
  • internal/api/handlers/admin_watchlist_sweep_test.go
  • internal/api/handlers/catalog_resources.go
  • internal/api/handlers/favorites.go
  • internal/api/handlers/profiles.go
  • internal/api/handlers/profiles_test.go
  • internal/api/handlers/watchlist_titles.go
  • internal/api/handlers/watchlist_titles_db_test.go
  • internal/api/router.go
  • internal/api/testdata/media_routes.txt
  • internal/apiv2/admin_requests.go
  • internal/apiv2/document.go
  • internal/apiv2/document_test.go
  • internal/apiv2/request_lifecycle.go
  • internal/apiv2/request_lifecycle_test.go
  • internal/apiv2/requests.go
  • internal/apiv2/router.go
  • internal/apiv2/watchlist_titles.go
  • internal/apiv2/watchlist_titles_test.go
  • internal/catalog/catalog_resolver.go
  • internal/catalog/provider_aliases.go
  • internal/catalog/provider_aliases_db_test.go
  • internal/contractledger/ledger_test.go
  • internal/metadata/tmdb/client.go
  • internal/metadata/tmdb/client_test.go
  • internal/metadata/tmdb/types.go
  • internal/notifications/interest_hooks.go
  • internal/notifications/interest_hooks_test.go
  • internal/requests/editor_concurrency.go
  • internal/requests/repository.go
  • internal/requests/repository_page_test.go
  • internal/requests/service.go
  • internal/requests/service_test.go
  • internal/requests/store.go
  • internal/requests/types.go
  • internal/requests/watchlist.go
  • internal/requests/watchlist_db_test.go
  • internal/requests/watchlist_test.go
  • internal/sections/fetcher.go
  • internal/settingskeys/keys.go
  • internal/settingsmigrate/plan.go
  • internal/userstore/pgstore/profiles.go
  • internal/watchlist/maintainer.go
  • internal/watchlist/titles.go
  • internal/watchlist/titles_db_test.go
  • internal/watchlist/titles_race_db_test.go
  • internal/watchlist/titles_repair.go
  • internal/watchlist/titles_repo.go
  • migrations/sql/20260930012209_add_watchlist_titles.sql
  • migrations/sql/20260930012210_add_stale_media_ids_provider_value_index.sql
  • migrations/sql/20260930015040_add_watchlist_requests_and_request_source.sql
  • web/perf-budget.json
  • web/src/App.tsx
  • web/src/api/types.ts
  • web/src/api/v2/adminRequests.ts
  • web/src/api/v2/operations.ts
  • web/src/api/v2/requests.test.ts
  • web/src/api/v2/schema.ts
  • web/src/api/v2/watchlistTitles.test.ts
  • web/src/api/v2/watchlistTitles.ts
  • web/src/components/RequestPosterCard.test.tsx
  • web/src/components/RequestPosterCard.tsx
  • web/src/components/RequestResultsGrid.tsx
  • web/src/components/RequestToAddSection.test.tsx
  • web/src/components/overlays/CardOverlays.test.tsx
  • web/src/components/overlays/CardOverlays.tsx
  • web/src/components/overlays/OverlayPreviewCard.tsx
  • web/src/components/overlays/OverlayPreviewVariantToggle.tsx
  • web/src/components/overlays/RequestDownloadBar.tsx
  • web/src/components/watchlist/WatchlistTabs.tsx
  • web/src/components/watchlist/WatchlistTitleCard.test.tsx
  • web/src/components/watchlist/WatchlistTitleCard.tsx
  • web/src/components/watchlist/WatchlistTitlesTab.test.tsx
  • web/src/components/watchlist/WatchlistTitlesTab.tsx
  • web/src/hooks/queries/keys.ts
  • web/src/hooks/queries/useRequests.ts
  • web/src/hooks/queries/watchlistTitles.test.ts
  • web/src/hooks/queries/watchlistTitles.ts
  • web/src/hooks/useOverlayPrefs.test.tsx
  • web/src/hooks/useSubmitMediaRequest.ts
  • web/src/hooks/useWatchlistTitleToggle.ts
  • web/src/lib/documentTitle.ts
  • web/src/lib/overlays/extractors.ts
  • web/src/lib/overlays/icons.tsx
  • web/src/lib/overlays/index.ts
  • web/src/lib/overlays/registry/ribbons.ts
  • web/src/lib/overlays/requestStatus.test.ts
  • web/src/lib/overlays/sample-data.ts
  • web/src/lib/overlays/schema.ts
  • web/src/lib/overlays/types.ts
  • web/src/lib/overlays/ui-constants.ts
  • web/src/lib/settingsConformance.json
  • web/src/lib/settingsContract.ts
  • web/src/lib/watchlistTitles.test.ts
  • web/src/lib/watchlistTitles.ts
  • web/src/pages/Catalog.collectionUnavailable.test.tsx
  • web/src/pages/Catalog.test.tsx
  • web/src/pages/Catalog.tsx
  • web/src/pages/Catalog.watchlistTabs.test.tsx
  • web/src/pages/ItemDetail/ExternalTitleContent.tsx
  • web/src/pages/ItemDetail/components/RequestActionBar.test.tsx
  • web/src/pages/ItemDetail/components/RequestActionBar.tsx
  • web/src/pages/RequestBrowse.test.tsx
  • web/src/pages/RequestDiscoverSection.test.tsx
  • web/src/pages/Requests.test.tsx
  • web/src/pages/Requests.tsx
  • web/src/pages/SettingsLayout.test.tsx
  • web/src/pages/SettingsLayout.tsx
  • web/src/pages/TitleDetail.test.tsx
  • web/src/pages/admin-requests/RequestQueue.tsx
  • web/src/pages/admin-requests/RequestQueueDialog.tsx
  • web/src/pages/admin-settings/RequestsSettings.test.tsx
  • web/src/pages/admin-settings/RequestsSettings.tsx
  • web/src/pages/settings/CardOverlaySettings.tsx
  • web/src/pages/settings/RequestsSettings.test.tsx
  • web/src/pages/settings/RequestsSettings.tsx

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T07:14:23.929616Z 9063529 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d50c67f974

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/watchlist/titles_repo.go
Comment thread internal/apiv2/request_lifecycle.go
Comment thread contracts/settings/v1/manifest.json
A request keeps the TMDB ID it was made under, but the watchlist looked up
requests only by the title's current ID. After TMDB replaced the ID, the card
showed no request, a repeat add could file a duplicate, and removing the title
left the watchlist-made request active. Request lookups, follows and
withdrawals now use every TMDB ID the title has had.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b1bf11f1d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/watchlist/titles.go
Comment thread internal/watchlist/titles_repo.go
Comment thread web/src/hooks/queries/watchlistTitles.ts Outdated
Promotion holds the title lock in a transaction while the library watchlist
write takes a second pool connection. With database.max_connections set to 1
that write waited for the request to time out, so watchlist and item reads
hung whenever a title was ready to move. A one-connection pool now writes to
the library watchlist first and locks afterwards.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@macroscopeapp

macroscopeapp Bot commented Sep 30, 2026

Copy link
Copy Markdown

The Validation tasks: line omits #1184 C3. The new Discover/external-title watchlist action uses an optimistic update and, on failure, restores membership and shows an error; that adds a flow covered by C3’s offline-toggle criterion. Please update the line to:

Validation tasks: changes #1183 C1, C2; changes #1184 C1, C2, C3; changes #1202 C1, C2, C3; changes #1203 C1; changes #1204 C1, C3

All result cells in these listed tasks are currently Not run; this is a coverage-scope correction, not a claim that a prior result needs re-validation.

Automated check: Macroscope check run agent (gpt-6-luna). No validation was performed.

Posted via Macroscope — v1 validation impact

@Quick104

Copy link
Copy Markdown
Contributor Author

Updated the Validation tasks: line to include #1184 C3. The Discover watchlist toggle updates optimistically and, on failure, rolls back and shows an error toast, so it falls under C3's offline-toggle criterion.

@macroscopeapp

macroscopeapp Bot commented Sep 30, 2026

Copy link
Copy Markdown

The Validation tasks: line omits three affected cases: #1184 C3 covers the new watchlist toggle's offline/optimistic behavior; #1202 C4 covers routing and the requests_enabled gate for watchlist-created requests; and #1204 C2 covers request status and fulfillment notification in the new web watchlist flow. The candidate task results I read are all Not run, so there are no recorded results to invalidate or unblock.

Please update the line to:

Validation tasks: changes #1183 C1, C2; changes #1184 C1, C2, C3; changes #1202 C1, C2, C3, C4; changes #1203 C1; changes #1204 C1, C2, C3

Automated check: Macroscope check run agent (gpt-6-luna). No validation was performed.

Posted via Macroscope — v1 validation impact

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1dd0fcc6c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/apiv2/watchlist_titles.go Outdated
Comment thread internal/api/handlers/watchlist_titles.go Outdated
Comment thread internal/watchlist/titles.go
Deleting a title removed its last entry, and with it the title's former TMDB
IDs, before withdrawing the watchlist's requests. A withdrawal that failed
part way could then never be retried under the former ID. The withdrawal now
runs first.

Library matching on add and remove also passes every TMDB ID the title has
had, so a library copy known only by a replaced ID is found. The
one-connection-pool promotion checks that the entry still exists before it
writes to the library watchlist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecce9887b4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/api/handlers/profiles.go
Comment thread internal/apiv2/watchlist_titles.go Outdated
Comment thread internal/api/handlers/watchlist_titles.go Outdated
Deleting a profile left the unsent requests its watchlist made active: the
profile's entries are gone by the time the purge runs, and requests only
reference the account. The profile delete now withdraws them by requester
profile and source.

An add and a delete of the same title could interleave so that the add's
request landed after the delete had withdrawn and removed the entry. The
delete now withdraws again after removing the entry, and the add rechecks its
entry after requesting and withdraws if it is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

Comment thread internal/apiv2/watchlist_titles.go Outdated
Comment thread internal/apiv2/watchlist_titles.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c43543c03e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/apiv2/watchlist_titles.go Outdated
if _, err := reg.deps.WatchlistTitles.RemoveWatchlistTitle(ctx, viewer, in.MediaType, in.TMDBID); err != nil {
return nil, serviceProblem(err)
}
if err := reg.withdrawWatchlistRequests(ctx, rv, in.MediaType, ids); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve a request created by a racing re-add

When a DELETE pauses here after removing the entry and a concurrent PUT then re-creates both the entry and its watchlist-sourced request, this second withdrawal cancels the newer PUT's request even though its entry remains. The final state therefore depends on timing and can leave a successfully re-added title without the automatic request it just created; condition the post-removal withdrawal on the entry still being absent or serialize it with re-adds.

AGENTS.md reference: AGENTS.md:L73-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the same remaining race as the thread on internal/apiv2/watchlist_titles.go further up. WatchlistTitleOff narrows it, but the absence check and the second withdrawal are still separate steps, so a re-add that finishes between them can lose its request. Making the withdrawal conditional and atomic, keyed to the entry that made the request, needs a change on the requests side. I've asked the maintainer to choose between that and the documented trade-off, and I'm leaving this open until they decide.

Comment thread internal/api/handlers/watchlist_titles.go Outdated
The add's recheck and the delete's second withdrawal treated a missing
external entry as "removed". An entry that promotion moved to the library
watchlist while the add was requesting, or that an overlapping re-add put
back, would then lose the request it should keep. Both now withdraw only
when the title is off the watchlist in both forms: no external entry and no
library watchlist entry for its library copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

Comment on lines +365 to +370
off, err := reg.deps.WatchlistTitles.WatchlistTitleOff(ctx, viewer, snap)
if err != nil {
return nil, serviceProblem(err)
}
if off {
if err := reg.withdrawWatchlistRequests(ctx, rv, in.MediaType, ids); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug high

Race condition identified between WatchlistTitleOff and the second withdrawal: a delete can read off=true, then an overlapping add can save its entry, create its request, and pass its membership check before the delete resumes, allowing WithdrawWatchlistRequest to cancel the re-add's unsent request or remove its follows despite the title being watchlisted. Serialize the membership/request lifecycle per profile and title rather than relying on a separate membership check.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the same remaining race as the thread on internal/apiv2/watchlist_titles.go further up. WatchlistTitleOff narrows it, but the absence check and the second withdrawal are still separate steps, so a re-add that finishes between them can lose its request. Making the withdrawal conditional and atomic, keyed to the entry that made the request, needs a change on the requests side. I've asked the maintainer to choose between that and the documented trade-off, and I'm leaving this open until they decide.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed—this is the same unresolved race, not a separate finding. A re-add can complete after WatchlistTitleOff returns true but before the second withdrawal, leaving the title watchlisted while its request is canceled or its follows are removed.

An atomic withdrawal tied to the entry that created the request would address that gap; another membership check would only narrow it. Pending the maintainer’s decision to fix it or explicitly accept the documented trade-off, the finding should remain open.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79d8f1c64c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +509 to +512
if err := addToLibrary(ctx); err != nil {
return false, false, err
}
moved, err = s.repo.promoteEntry(ctx, v.UserID, v.ProfileID, titleID, func(context.Context) error { return nil })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize single-connection promotion with removal

When database.max_connections is set to the supported minimum of 1, this path writes the library entry before acquiring the title lock. If a concurrent DELETE removes the title entry and finishes its library lookup before addToLibrary runs, this call then adds the library entry and promoteEntry finds no title to move, leaving the successfully removed title on the watchlist. The one-connection fallback needs coordination with removal rather than this check-then-write sequence.

AGENTS.md reference: AGENTS.md:L73-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the one-connection-pool trade-off already discussed and documented in docs/architecture/external-watchlist.md. With a single pool connection, the library write can't run while the title lock's transaction holds that connection, so this path writes first after checking the entry still exists. A delete landing between that check and the lock can still leave the item on the library watchlist. Pools of two or more connections use the locked path, where this can't happen. I'm leaving this open for the maintainer, together with the related re-add race.

Comment on lines +365 to +370
off, err := reg.deps.WatchlistTitles.WatchlistTitleOff(ctx, viewer, snap)
if err != nil {
return nil, serviceProblem(err)
}
if off {
if err := reg.withdrawWatchlistRequests(ctx, rv, in.MediaType, ids); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the absence check and second withdrawal atomic

When a PUT re-adds the title after WatchlistTitleOff returns true but before the second withdrawal, the DELETE cancels the new PUT's watchlist-created request even though its entry remains. Fresh evidence in this revision is that the attempted fix still leaves the absence check and withdrawal as separate service calls, so the same cross-node interleaving remains possible; serialize the recheck with withdrawal or make withdrawal conditional in one atomic operation.

AGENTS.md reference: AGENTS.md:L73-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the same remaining race as the thread on internal/apiv2/watchlist_titles.go further up. WatchlistTitleOff narrows it, but the absence check and the second withdrawal are still separate steps, so a re-add that finishes between them can lose its request. Making the withdrawal conditional and atomic, keyed to the entry that made the request, needs a change on the requests side. I've asked the maintainer to choose between that and the documented trade-off, and I'm leaving this open until they decide.

A delete removed the title's last entry, and with it the title's former TMDB
IDs, before removing the library watchlist entry. If that library step
failed, a retry could no longer find a library copy known only by a former
ID. The library entry now goes first, matched by every ID while the entry
stands, and is checked again after the entry is removed.

Reading the watchlist titles tab also moves titles the library now has onto
the library watchlist, so the web now refreshes the library tab after every
titles read, and the home row and item states when a listed title is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

Comment on lines +187 to +192
title, _, err := titles.Remove(ctx, viewer.watchlistViewer(), mediaType, tmdbID)
if err != nil {
return nil, apiError(http.StatusInternalServerError, "internal_error", "Failed to remove from watchlist")
}
if err := h.removeLibraryWatchlistItem(ctx, viewer, itemID); err != nil {
return nil, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug high

Retry information loss occurs when promotion recreates the library entry between the first removal and titles.Remove, then the second library removal fails: promotion/removal deletes the last title and its former IDs before the failing operation, so retrying DELETE with the current TMDB ID cannot find a library item carrying only a former ID. Preserve durable alias information until the final library cleanup succeeds.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This case needs three things together:

  1. a promotion runs in the gap between the delete's first library removal and its entry removal;
  2. the second library removal then fails;
  3. the library copy carries only a former TMDB ID.

A promotion moving the last entry deletes the title and its aliases by design, because the title is then a normal library item. So after (1), nothing in the watchlist tables can map the current TMDB ID back to that item. What's left is not hidden or lost: the item sits on the profile's library watchlist, shows on the watchlist page, and can be removed there through the normal DELETE /api/v2/watchlist/{item_id}. Keeping the aliases alive past promotion only for this retry would add a second lifetime rule to the title records. I'd rather not do that, and I'm leaving the thread open for the maintainer to weigh in.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed—the entry remains visible and removable through DELETE /api/v2/watchlist/{item_id}. This is a retryability gap, not loss of the watchlist entry, and the original high-severity classification overstates the impact.

The narrower concern still holds: after that race and failure, retrying the title-based DELETE with the current TMDB ID cannot finish the removal because the alias mapping is gone. Recovery requires switching to the library-item endpoint.

Given the three conditions required and the existing recovery path, retaining aliases beyond promotion is not clearly justified solely for this case. I’d leave this as a low-severity edge case for the maintainer to accept or address, rather than insist on the proposed alias-lifetime change. A focused regression test could document the limitation and confirm that the surviving library entry remains removable.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a7b598b5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +308 to +310
// AddSnapshot is Add for callers that already hold a snapshot, such as an
// import that keeps the source's added_at.
func (s *Titles) AddSnapshot(ctx context.Context, v Viewer, snap Snapshot, addedAt time.Time) (Entry, bool, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Feed unmatched provider watchlists into title storage

When a Trakt, Simkl, or MDBList watchlist row has no catalog match, watchsync.Service.importList still takes the unchanged match == nil branch at internal/watchsync/lists.go:269-278, increments Unmatched, and discards the row. Repository-wide search shows AddSnapshot is called only by the HTTP handler and tests, so this import-capable entry point is never wired into watch-provider sync; those users continue losing every outside-library title the feature is intended to retain. Route unmatched watchlist rows, but not favorites, through the new title store while retaining their provider timestamp and sync state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is deliberately out of scope for this PR. The design splits import and export of outside-library titles for Trakt, Simkl, MDBList and Plex into a follow-up phase, with its own review. The shadow table watch_provider_list_items is keyed by media_item_id, and removal sync needs a stand-in key and a way to move it on promotion; those need their own design. I've corrected the PR description, which read as if this PR fixed the sync drop. I'm leaving this thread open for the maintainer, who owns that phase.

Comment thread web/src/api/v2/watchlistTitles.ts Outdated
Comment thread internal/apiv2/watchlist_titles.go
Requests a watchlist add creates carried no overview or backdrop: the add
keeps only its own snapshot. CreateRequest now fills empty display fields
from the TMDB detail it already reads, for any caller.

The web read at most 25 pages of watchlist titles and showed the rest as the
whole list. It now follows the cursor until the server has no more pages and
fails on a repeated cursor instead of truncating.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e8b402247

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/watchlist/titles_repo.go
When TMDB reported an IMDb or TVDB ID another watchlist title already owned,
the alias was skipped but the ID still landed in the title's own fields,
which library matching and request presence read directly. The title could
then match the other title's library copy. Those fields now hold only IDs the
title owns as aliases: a refresh keeps the ID it already had, a new title
leaves the field empty.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e320cf4793

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/api/handlers/watchlist_titles.go Outdated
When a title matched several catalog items but the viewer could see only
one, promotion and the add/remove library lookup gave up as ambiguous before
checking access, so the title stayed outside the library for that viewer and
a delete could miss its library watchlist entry. Matches are now filtered by
the viewer's access first, and exactly one visible copy is required.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

Comment thread internal/watchlist/titles.go Outdated
Filtering each title's library matches by access ran one EnsureAccessible
query per matched copy on every watchlist read. Promotion now checks every
matched copy with one EnsureAccessibleIDs call and filters from that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02590f298b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if err != nil {
return nil, apiError(http.StatusInternalServerError, "internal_error", "Failed to look up the watchlist title")
}
tmdbIDs, imdbID, tvdbID := []int{tmdbID}, "", 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve aliases after promotion for stale deletes

When a repointed title is promoted through a former TMDB/IMDb/TVDB alias and no other profile keeps the title, promotion deletes its title and alias rows before a later DELETE can begin. This fallback then searches the catalog using only the URL's TMDB ID; if the catalog item is identifiable only by a discarded alias, a stale client receives 204 but the promoted library-watchlist entry remains. Fresh evidence beyond the earlier lookup-order fix is that promotion can remove the snapshot before this handler reaches Find, so resolving the item before this handler's own removal does not cover the race.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the same edge as the delete-retry thread on this file further up. Once promotion moves the last entry, the title and its aliases are deleted by design, because the title is then an ordinary library item. A stale client deleting by a TMDB ID the catalog item doesn't carry gets a 204, while the item stays on the library watchlist. It remains visible there and is removed through DELETE /api/v2/watchlist/{item_id}, which is what a client refreshing after promotion uses. Kody rated that thread low severity with a working recovery path. Keeping the aliases alive past promotion would give the title records a second lifetime rule. I'm leaving this open alongside that one for the maintainer to decide.

Comment thread internal/api/handlers/watchlist_titles.go Outdated
Comment thread web/src/hooks/queries/watchlistTitles.ts
Every page of a watchlist or watchlist-titles read ran the profile-wide
promotion, so reading a long list scanned the whole profile once per page.
Promotion now runs on the first page only.

The watchlist titles tab also refreshes every 30 seconds while any title is
downloading, as the other request surfaces do, so its progress doesn't
freeze.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kody-ai

kody-ai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Access your configuration settings here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9063529555

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +787 to +791
if h.WatchlistTitlesPurger != nil {
purgeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
if purgeErr := h.WatchlistTitlesPurger.PurgeProfile(purgeCtx, userID, profileID); purgeErr != nil {
slog.WarnContext(ctx, "profile watchlist-title purge failed after delete", "component", "api", "user_id", userID, "profile_id", profileID, "error", purgeErr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent late watchlist adds after profile deletion

When a watchlist-title PUT passes profile authentication before a concurrent profile deletion but reaches AddSnapshot after this purge finishes, it can recreate user_watchlist_titles for the deleted profile because that table deliberately has no profile foreign key; the subsequent auto-request can likewise persist with the deleted requested_by_profile_id. No later cleanup targets that profile, so a cross-node race leaves an unreachable entry and potentially an active download request. Coordinate title mutations with profile deletion or leave a durable tombstone that rejects/cleans late writes.

AGENTS.md reference: AGENTS.md:L13-L18

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that the window exists: a PUT that authenticated before the profile was deleted and writes after the purge can leave an orphaned entry. And, with auto-request on, it can leave a request naming the deleted profile. This isn't specific to watchlist titles, though. Every shared Postgres table keyed by profile (for example user_dropped_series and the device-library and notification rows) has the same window, because profiles can live in the SQLite user store and those tables deliberately have no profile foreign key. The migration comment and docs/architecture/external-watchlist.md say this table follows that pattern. Requests have never had a profile foreign key either: a direct request made just before a profile delete outlives it the same way, and it stays visible in the admin queue. Closing this properly means a deletion marker or cleanup covering all of those tables, which is a cross-cutting change I'd keep out of this PR. I'm leaving this open for the maintainer to decide whether that belongs in a follow-up.

@Quick104
Quick104 merged commit 8943abc into main Sep 30, 2026
26 of 27 checks passed
@Quick104
Quick104 deleted the t3code/watchlist-external-items branch September 30, 2026 12:46
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.

1 participant