feat(catalog): browse peer catalogs and request titles into *arr#50
Merged
Conversation
c0a49ad to
a30e396
Compare
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.
a30e396 to
a9dc73a
Compare
6bb9235 to
a134d65
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
/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.config.jack.tmdbApiKey) with a live connection-status indicator.How it's built
catalogmodule exposingGET /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), andPOST /catalog/request.requestDownload):server.addlooks the title up (tmdb:/tvdb:) and adds it monitored without triggering a search, thenstartDirectDownloadcreates ajack_manualdownload 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.ImportWatcherreconcilesimport_queuedrows — 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.TmdbClient.Tests / checks
Scope notes
import_queuedrows are not backfilled with the new stablesourceServerId; they keep matching their destination by name (intentional — consequences are minimal).Manual verification (needs a live peer + *arr + TMDB key)
Greptile Summary
This PR adds a cross-peer catalog and request flow for sending titles into Radarr and Sonarr. The main changes are:
Confidence Score: 5/5
This looks safe to merge.
Comments Outside Diff (8)
apps/backend/src/modules/catalog/catalog.controller.ts, line 2620-2622 (link)When
startDirectDownload()returnsduplicate, this movie path treats it as success and reportsstarted: 1even 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.apps/backend/src/modules/catalog/catalog.controller.ts, line 2643-2644 (link)The series loop increments
startedfor every result exceptfailed, soduplicateepisode 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.apps/backend/src/modules/downloads/import-watcher.ts, line 3044-3052 (link)After this branch successfully posts a ManualImport command, the row is only completed later if the history poll sees the derived hash in
downloadFolderImportedevents. Catalog downloads bypass the download-client import path, so if Radarr or Sonarr records ManualImport under a different event type or ignores the supplieddownloadId,manualImportTriggeredsuppresses retries while the row remainsimport_queuedforever.apps/backend/src/modules/downloads/import-watcher.ts, line 3032-3033 (link)Queued imports are grouped by the stored destination server name and later matched with
s.name. If a direct catalog download reachesimport_queuedand 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.apps/backend/src/lib/servers/arr/sonarr.ts, line 2337-2339 (link)A matching Sonarr manual-import candidate is dropped when Sonarr returns it without episode ids. For filenames Sonarr cannot parse, this leaves
filesempty 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.apps/backend/src/modules/catalog/catalog.lib.ts, line 2736-2739 (link)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.
apps/backend/src/modules/catalog/catalog.lib.ts, line 2817-2821 (link)pickBestPerEpisode()maps missingseasonandepisodeto0: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.apps/ui/app/composables/useCatalogMetadata.ts, line 3991-3999 (link)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.Reviews (5): Last reviewed commit: "fix(arr): fail rows on pruned manual-imp..." | Re-trigger Greptile