Folder years routinely differ from provider release years by a year or two
(festival vs wide release, regional dates), zeroing the year bonus and leaving
a lone exact-title candidate at 63-68 — just under the single-candidate >=70
gate (e.g. Dead Reckoning 1947 vs 1946, 17 Blocks 2021 vs 2019, Stasi FC). Add
title corroboration to the existing lone-result rule: a sole distinct show whose
normalized title exactly matches and whose year is within +/-2 is accepted. The
55 floor still rejects low-similarity titles (e.g. Hotel Transylvania Puppy! vs
Puppy!). No 55/70/15 threshold change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds DEBUG logs in the ModeInitialMatch search path: each provider's result
count for the query, and the assembled raw/candidate totals. Lets us see when a
provider search returns zero ('no metadata found') vs a scoring/tie issue.
Zero behavior change.
When the top tie-group is one distinct show returned by 2+ distinct providers
(candidatesAreSingleDistinctShow already verifies matching title+year), accept it
even if the hint has no parsed year (year-less folders like '100 Deeds for Eddie
McDowd'). Multi-source agreement substitutes for the year guard; lone single-source
no-year results stay subject to the single-candidate >=70 gate. Thresholds unchanged.
Accept a year-corroborated single distinct show when the TOP tie-group (within
15 pts of best) is one show across providers, ignoring low-score noise below it,
and pick the winner by the library's metadata-provider chain order (providerPriority,
highest-first; falls back to top-scored). Recovers items like '100 Days Wild'
that are returned identically by TVDB and TMDB. Thresholds (55/70/15) unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a DEBUG-gated log in selectInitialMatchCandidate printing each scored
candidate (title/year/type/sources/provider_ids/score) against the hint, so
operators can see why an item did or didn't auto-match. Zero-cost when debug
logging is off; no change to matching behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A scan drainer and the background MatchWorker can process the same folder
concurrently. When a provider-ID merge moves a series' episodes to the survivor
and deletes the source, an in-flight ensureSeriesEpisodeLinks(sourceID) hits
catalog.ErrItemNotFound and was failing the whole scan. The episodes are already
reattached, so this is benign: log and continue (matching the lenient call sites)
instead of failing. Genuine errors still abort.
A search that resolves to one distinct show (one candidate, or the same
title+year returned once per source as unmerged TVDB/TMDB rows) whose year
matches the parsed year is now auto-accepted via the existing top-ranked
candidate, even when the fuzzy title score is in the 55-69 band. The 55/70/15
thresholds are unchanged; this only adds a year-gated acceptance for
effectively-unique results (recovers lone-correct-result items like 1201 (1993)).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Score candidate metadata completeness and auto-match the richer duplicate when title/year/type tie
- Enrich near-duplicate candidates via the provider chain before initial match selection
- Seed both movie and series match queues for mixed-type libraries and wait for TV queue settle
- Add taskmanager worker test coverage and a plan doc for the tie-breaker work
- Add TTL caches with singleflight for DiscoverSection, DiscoverPage, and GetExternalIDs to reduce upstream TMDB calls
- Reuse CastCarousel with a fullBleed variant on RequestDetail and drop the bespoke RequestCastRow
- GetExternalIDs now uses the dedicated /movie/{id}/external_ids and
/tv/{id}/external_ids endpoints instead of fetching the full detail
with append_to_response=external_ids. The dedicated payload is
one or two orders of magnitude smaller for the same fields.
- Document PosterPath/BackdropPath on MediaResult as raw TMDB path
fragments that callers must prefix with the image base URL.
- normalizeCast switches from inline insertion sort to sort.SliceStable.
The output is identical; the new form is one line and O(n log n).
- normalizeIntegration no longer reuses integration.Tags' backing
array via Tags[:0]; the slice is callable code, so reusing the
array would silently corrupt the caller's slice if it kept a
reference. Allocate a fresh slice instead.
- HandleGet now requires a profile, matching the rest of the
/requests user-group handlers. Router middleware enforces this
already, but the inline check is defense-in-depth for any future
remount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The client previously fell back to a hardcoded project-level API key
when the caller passed an empty string. Once committed the key cannot
be rotated without a rebuild, and the silent fallback meant that
misconfiguration produced unattributed traffic against a shared key.
Remove the default. Clients constructed with an empty key now return
ErrMissingAPIKey from every API call so operators see the
configuration problem immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
syncRefreshDebtFailure unconditionally set RefreshDebtReasonRefreshFailure
on matched items, even when the refresh failed because the item is
missing a provider id. The priority logic already preferred
ProviderIDIncomplete (priority 240) over RefreshFailure (priority
190), so scheduling was correct — but the persisted reason mask
combined both bits and the reason-count metric reported these items
as "refresh failures" instead of "provider id incomplete". Guard the
OR so the failure bit only lands on items that actually have a
provider id and failed for a different reason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Batch integration upserts in a single transaction
- Treat radarr/sonarr lookup results as arrays and require exact matches
- Prefer queue failures over downloading state when evaluating arr queues
- Allow retrying queued/downloading requests and block declines once fulfillment started
- Fall back to pending when auto-approval integration check fails
- Rename requests query hooks file and fix discover card request affordance
- Add `media_type=all` to request search, backed by TMDB `/search/multi` filtered to movies and series
- Default the Requests page filter to All and refresh search results grid styling
- Refine RequestPosterCard with status accent bar, richer fallback poster, and fluid grid layout
Wire curated TMDB-backed studios/networks/genres discovery into the requests service and UI, replacing on-demand logo fetches with fixed duotone logos and adding browse routes plus tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add GetMediaDetail TMDB client returning normalized detail with cast, crew, recommendations, and certifications
- Add /api/requests/detail/{media_type}/{tmdb_id} endpoint overlaying availability and request state
- Add RequestDetail page and link poster cards to it
- Treat empty/truncated Radarr/Sonarr POST responses as accepted; drop pre-submit existence lookups
- Add request domain, repository, service, and reconcile task
- Add Radarr/Sonarr fulfillment adapters and TMDB discovery
- Expose user and admin request APIs with quota and approval rules
- Add web UI for browsing, requesting, and admin queue management
- Migration 139 introduces media_requests and related tables