Skip to content

feat(catalog): browse peer catalogs and request titles into *arr#50

Merged
roziscoding merged 27 commits into
mainfrom
feat/tmdb-peer-catalog
Jun 28, 2026
Merged

feat(catalog): browse peer catalogs and request titles into *arr#50
roziscoding merged 27 commits into
mainfrom
feat/tmdb-peer-catalog

Conversation

@roziscoding

@roziscoding roziscoding commented Jun 27, 2026

Copy link
Copy Markdown
Owner

What

Browse a unified, cross-peer media catalog and request titles into Radarr/Sonarr — Jack downloads the file straight from the peer and hands it to *arr.

  • One catalog page (/catalog) aggregates every connected peer's library into a single poster wall with a peer picker and movie/TV filter, enriched with TMDB artwork, year, and rating, plus a per-title detail slideover.
  • Request a title → pick the destination *arr and root folder → Jack adds the title to Radarr/Sonarr monitored with search disabled, direct-downloads the matching release(s) from the peer, then triggers a *arr manual import so the file lands in the library. No indexer grab is involved.
  • TMDB key is managed in Settings (config.jack.tmdbApiKey) with a live connection-status indicator.

How it's built

  • New backend catalog module exposing GET /catalog (fans out to every initialized peer, groups their releases into unified titles), GET /catalog/tmdb/status, GET /catalog/tmdb/:mediaType/:tmdbId (per-card metadata), GET /catalog/request-options (destinations + root folders), and POST /catalog/request.
  • Request flow (requestDownload): server.add looks the title up (tmdb:/tvdb:) and adds it monitored without triggering a search, then startDirectDownload creates a jack_manual download row per release bound to the destination + import target. Movies pick the single best release; series pick the best release per episode, all bound to one series id.
  • Import: the ImportWatcher reconciles import_queued rows — a match against *arr's import history (by derived hash) flips the row to imported, otherwise it triggers a *arr manual-import command and tracks it by command id (completed / failed / pending). A pruned command record (404) is terminal so the row fails instead of polling a vanished id forever; transient errors retry next tick.
  • TMDB metadata comes from a small cached, concurrency-capped TmdbClient.
  • Frontend: unified catalog page (instant-load skeletons, peer picker, type filter), poster card with image fallback, title detail slideover with a "View in Logflix" link, a per-peer download request modal, plus TMDB settings and an About section (Nuxt UI).

Tests / checks

  • 386 backend tests pass — catalog grouping, TMDB mappers, status branching, request routing, MSW-mocked Radarr/Sonarr add + manual-import payloads, and manual-import command-status recovery.
  • eslint clean on the touched files.

Scope notes

  • *arr is added monitored with search off — the actual file comes from the peer direct-download + manual import, not a Jack torznab indexer / blackhole grab.
  • Catalog pagination/virtualization is intentionally deferred (posters lazy-load).
  • Pre-upgrade import_queued rows are not backfilled with the new stable sourceServerId; they keep matching their destination by name (intentional — consequences are minimal).

Manual verification (needs a live peer + *arr + TMDB key)

  • Browse the catalog, open a title, request it to a destination, and confirm Jack downloads the release and the file imports into Radarr/Sonarr.

Greptile Summary

This PR adds a cross-peer catalog and request flow for sending titles into Radarr and Sonarr. The main changes are:

  • New backend catalog endpoints for browsing peer releases, TMDB metadata, request options, and title requests.
  • Direct-download request handling that adds titles to Radarr or Sonarr and queues manual imports.
  • Import watcher updates for manual import command tracking and stable destination matching.
  • Frontend catalog, detail, request modal, and TMDB settings UI.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Comments Outside Diff (8)

  1. apps/backend/src/modules/catalog/catalog.controller.ts, line 2620-2622 (link)

    P1 Duplicate Downloads Report Success

    When startDirectDownload() returns duplicate, this movie path treats it as success and reports started: 1 even though no new row was created and no download was started. A repeated request or filename collision can tell the user the title is downloading while the catalog flow did not queue anything new.

    Fix in Claude Code Fix in Codex

  2. apps/backend/src/modules/catalog/catalog.controller.ts, line 2643-2644 (link)

    P1 Duplicate Episodes Count As Started

    The series loop increments started for every result except failed, so duplicate episode downloads are counted as newly started. If two selected episodes resolve to the same destination filename, or one episode was already queued, the API overstates the started count and the skipped episode is never downloaded or imported by this request.

    Fix in Claude Code Fix in Codex

  3. apps/backend/src/modules/downloads/import-watcher.ts, line 3044-3052 (link)

    P1 Manual Imports Can Stay Queued

    After this branch successfully posts a ManualImport command, the row is only completed later if the history poll sees the derived hash in downloadFolderImported events. Catalog downloads bypass the download-client import path, so if Radarr or Sonarr records ManualImport under a different event type or ignores the supplied downloadId, manualImportTriggered suppresses retries while the row remains import_queued forever.

    Fix in Claude Code Fix in Codex

  4. apps/backend/src/modules/downloads/import-watcher.ts, line 3032-3033 (link)

    P1 Renamed Servers Orphan Imports

    Queued imports are grouped by the stored destination server name and later matched with s.name. If a direct catalog download reaches import_queued and the destination is renamed before the watcher processes it, no connector matches the old name, the group is skipped silently, and ManualImport is never triggered for that row.

    Fix in Claude Code Fix in Codex

  5. apps/backend/src/lib/servers/arr/sonarr.ts, line 2337-2339 (link)

    P2 Unparsed Episodes Retry Forever

    A matching Sonarr manual-import candidate is dropped when Sonarr returns it without episode ids. For filenames Sonarr cannot parse, this leaves files empty on every watcher tick, so the row stays queued and logs the same manual-import failure repeatedly instead of marking the download failed or surfacing the unimportable file.

    Fix in Claude Code Fix in Codex

  6. apps/backend/src/modules/catalog/catalog.lib.ts, line 2736-2739 (link)

    P2 Same-Name Titles Can Merge

    The alias map is first-writer-wins by normalized title name, so two different id-bearing titles with the same display name can be collapsed under whichever strong key appears first. For remakes or same-name TV series from different years, the catalog can mix releases under one title and send the request flow the wrong external id.

    Fix in Claude Code Fix in Codex

  7. apps/backend/src/modules/catalog/catalog.lib.ts, line 2817-2821 (link)

    P2 Unnumbered Episodes Collapse Together

    pickBestPerEpisode() maps missing season and episode to 0:0, so every unnumbered release for a series competes for the same slot. If a peer returns specials, extras, or poorly parsed TV releases without S/E fields, only one is downloaded and the rest are silently skipped.

    Fix in Claude Code Fix in Codex

  8. apps/ui/app/composables/useCatalogMetadata.ts, line 3991-3999 (link)

    P2 Metadata Errors Block Retry

    The cache guard treats an error entry as already loaded because { status: 'error', data: null } is truthy. A transient TMDB or backend failure while a card first loads leaves that title blank for the rest of the page session, even after the service recovers.

    Fix in Claude Code Fix in Codex

Reviews (5): Last reviewed commit: "fix(arr): fail rows on pruned manual-imp..." | Re-trigger Greptile

@roziscoding roziscoding force-pushed the feat/tmdb-peer-catalog branch from c0a49ad to a30e396 Compare June 28, 2026 11:04
@roziscoding roziscoding marked this pull request as ready for review June 28, 2026 11:04
Add a management catalog module that groups a peer's releases into titles,
a /catalog/[peerId] page that lists them, and clickable peer entries.
Add an optional tmdbApiKey to config.jack, a TmdbClient that reads movie/tv
metadata, and a /catalog/tmdb/status endpoint. Expose a managed TMDB key
field and connection status in the Settings Jack section.
Enrich grouped titles with TMDB artwork/overview/year/rating (concurrency-
capped, cached) and redesign the catalog page as a poster grid with a
per-title detail slideover.
Fetch each destination *arr's quality profiles and root folders via a
/catalog/request-options endpoint, and add a Download modal that pre-fills
them for the *arr matching the title's media type.
Add addAndSearch to the *arr connectors (lookup the title, then add it
monitored with auto-search) and a POST /catalog/request endpoint, and wire
the Download modal to it so requesting a title hands it to *arr.
Emit torznab attr tag=internal on every item; *arr maps it to the Internal
indexer flag, so releases are targetable by a custom format.
Auto-register (and ensure on demand) a 'Jack' custom format matching the
Internal indexer flag and a 'Jack' quality profile that only accepts releases
with it (minFormatScore 1, upgrades off). Catalog downloads force that profile
so *arr grabs the title only from Jack, never from other indexers. The
Internal flag value is per-app (Radarr 32, Sonarr 8).
The feed embedded the deprecated main key in each release's download URL, so
grabs 401'd when *arr authenticated the indexer with a managed key (the main
key is often unset). Thread the requesting key (apikey query / x-api-key
header) through to each download URL so the grab passes auth.
The catalog endpoint ran every TMDB lookup server-side, 8 at a time, before
returning — freezing the page on large peers. Return titles unenriched and add
a per-title GET /catalog/tmdb/:mediaType/:tmdbId route the grid calls once per
visible card, so metadata fills in progressively. Paginate (48/page) to bound
how many lookups fire at once.
Replace the addAndSearch request path with a direct download + explicit
*arr ManualImport. Connectors gain add() (no search, idempotent, returns
entity id) and manualImport(); downloads carry an import_mode/import_target
marker (0008 migration) and a startDirectDownload entry; the ImportWatcher
pushes ManualImport once per jack_manual row, confirmed via the existing
deriveHash history match. Movie catalog flow picks the best peer release
and wires the UI; Sonarr methods land too so the package compiles.
Add the series branch to requestDownload: add the series without search,
pick the best release per episode (pickBestPerEpisode), and start a direct
download per episode tagged import_target {kind:'series',seriesId}; the
ImportWatcher pushes ManualImport with the candidate episodeIds. Adds a
TV-only 'downloads every episode' hint in the catalog detail UI.
requestDownload no longer forces a Jack-only quality profile (it uses
direct download + ManualImport), so ensureJackCustomFormat /
ensureJackQualityProfile, their autoregister provisioning branch, the
CreatedResourceId schema, the ManagedRegistrationMeta.profileId field, and
the corresponding dead tests are removed. The torznab Internal-flag tagging
(tag=internal) and the indexer/download-client registration are untouched.
startDirectDownload returns 'started'|'duplicate'|'failed' but requestDownload
ignored it and always reported success, leaving a title added to *arr with no
jack_manual row. The movie branch now throws on 'failed'; the series branch
counts only non-failed starts, throws when none started, and returns the real
started count instead of best.length.
- Implement useFallbackImage composable that cycles through sources when TMDB requests fail under load.
- Refactor CatalogPosterCard with three-state status machine for clearer metadata enrichment and error handling.
- Add lazy-loading via IntersectionObserver to defer metadata fetches until cards approach viewport.
- Remove inline metadata fetching from catalog page in favor of observer-driven lazy pattern.
- Add TMDB API v3 reference documenting authentication, endpoints, and image handling best practices.
Adds a Settings > About section: the jack mark plus version (sourced from
package.json via runtimeConfig.public.appVersion) and the TMDB-required
attribution notice and logo, kept visually subordinate to the app mark.
Derive a stable accent color per peer from its id (sorted-membership
assignment with a per-id hash fallback), and render it as a dot on
catalog poster peer badges. Reuse it for connection status: ConnDot now
draws a plug icon (shape = status, color = peer identity when given),
and ConnectorCard forwards an optional accent.
Centralize peers, servers, and derived peer colors into a single
useState-backed store fetched once per session, so every view stays
consistent and the config endpoints aren't refetched per component. The
store self-polls while any connector is connecting or down (and idles
once everything is connected) via a new reusable usePolling helper, which
also replaces useAutoRefresh's hand-rolled ticker. PeersSection,
ServersSection, and the dashboard now read from the store.
Load the catalog lazily and render poster skeletons so navigation is
instant, and move the filter tabs inline into the navbar. The title
detail now lists each peer as a card (release count, size, available
qualities) with its own Download button that opens the request modal
pre-targeted to that peer; single-peer titles keep a friendlier summary.
Gate the Logflix link on resolved TMDB metadata and move it under the
description, next to the metadata it relates to.
@roziscoding roziscoding force-pushed the feat/tmdb-peer-catalog branch from a30e396 to a9dc73a Compare June 28, 2026 11:11
Comment thread apps/backend/src/modules/catalog/catalog.controller.ts
Comment thread apps/backend/src/modules/catalog/catalog.controller.ts Outdated
Comment thread apps/backend/drizzle/0009_bumpy_ozymandias.sql
@roziscoding roziscoding force-pushed the feat/tmdb-peer-catalog branch from 6bb9235 to a134d65 Compare June 28, 2026 12:10
Comment thread apps/backend/drizzle/0009_bumpy_ozymandias.sql
Comment thread apps/backend/src/modules/downloads/import-watcher.ts
A persistent status-read failure for a stored manualImportCommandId left
the import row stuck forever: the watcher logged and retried the same
vanished command id every tick. Treat a 404 from the command endpoint as
terminal and return a failed state so the watcher marks the row failed,
while transient errors still throw and retry.
@roziscoding roziscoding merged commit 6f613d3 into main Jun 28, 2026
10 checks passed
@roziscoding roziscoding deleted the feat/tmdb-peer-catalog branch June 28, 2026 12:35
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