Commit Graph
216 Commits
Author SHA1 Message Date
QuickandClaude Opus 4.8 ffef55283a fix(migrations): renumber trending_discover_snapshots 166 -> 167
The shared dev DB already recorded version 166 (166_trending_blend_collection_type
from another branch), so the integer-version migration runner silently skipped our
166 and the table was never created — the trending section errored out empty.
167 is the next free version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:57:57 -04:00
Quick 81d1644cd7 chore(sections): satisfy lint (wrap trakt errors, lift source/window constants) 2026-05-29 10:51:54 -04:00
Quick 59874c717d feat: wire trending refresh task and snapshot reader 2026-05-29 10:49:01 -04:00
Quick d27f21b10f refactor(sections): read trending_discover from persisted snapshot 2026-05-29 10:46:01 -04:00
Quick 13081f7d07 feat(tasks): add refresh_trending_discover task 2026-05-29 10:44:15 -04:00
Quick 0025fae492 feat(sections): add trending refresher with persisted snapshots 2026-05-29 10:43:54 -04:00
Quick b1c36ad04e feat(sections): list enabled trending_discover section configs 2026-05-29 10:42:09 -04:00
Quick b2b5c2ade2 feat(sections): add trending snapshot model and repository 2026-05-29 10:41:47 -04:00
Quick 9c713d43bf feat(sections): add trending_discover_snapshots table 2026-05-29 10:40:58 -04:00
QuickandClaude Opus 4.8 80120aad02 refactor(sections): tidy trending_discover fetch and cache helpers
Extract newTrendingEntry, reuse orderMediaItems, and collapse concurrent
cache-miss loads with singleflight. Baseline for the persistent snapshot work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:40:13 -04:00
QuickandClaude Opus 4.8 6848fe32fd docs: add trending_discover persistent snapshot implementation plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:37:27 -04:00
QuickandClaude Opus 4.8 feed82c15d docs: add trending_discover persistent snapshot design
Replace the in-process 1h trending cache with a background-refreshed,
persisted snapshot for reliability under upstream failure and sync-run
observability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:25:31 -04:00
QuickandClaude Opus 4.8 ecded4c94f feat(sections): add trending_discover home section
A library-agnostic home section that surfaces external global trending
(TMDB or Trakt, admin-selectable) mixing movies + series, matched to
titles in the viewer's enabled libraries. TMDB uses /trending/all/{window}
(natively mixed); Trakt merges trending movies + shows. Fetched live with
a 1h in-process cache, so no background job or stored collection — and no
per-library duplication.

Appears in the admin section gallery via its recipe presets (TMDB Trending
Today/This Week, Trakt Trending); featured -> hero via the existing flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:25:17 -04:00
QuickandGitHub 6faec2b2a9 Merge pull request #22 from Silo-Server/feat/case-insensitive-usernames
feat(auth): make usernames and emails case-insensitive
2026-05-28 21:46:07 -04:00
QuickandClaude Opus 4.7 7e4cacd151 feat(auth): make usernames and emails case-insensitive
Login identifiers were compared case-sensitively, so "John" and "john"
were distinct accounts and a user could not log in unless they matched the
exact casing used at registration.

Convert users.username and users.email to the citext type (migration 165).
citext compares case-insensitively while preserving the originally stored
casing for display, so the existing unique constraints become
case-insensitive and `WHERE username = $1` / `email = $1` lookups match
regardless of case with no change to the query code itself.

Also add auth.NormalizeUsername/NormalizeEmail (trim-only; case preserved),
applied at the repository chokepoints (Create, Update, GetByUsername,
GetByEmail) and before validation in the create paths, so surrounding
whitespace no longer defeats matching or creates lookalike accounts.

Verified non-destructively against the dev DB: mixed-case lookups resolve
to the same row, case-variant inserts are rejected by the unique
constraint, and the down migration cleanly reverts to text.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:44:51 -04:00
QuickandGitHub 1624c77fbd Merge pull request #21 from Silo-Server/fix/web-a11y-visual-qa
fix(web): accessibility & UX fixes from visual QA pass
2026-05-28 21:09:43 -04:00
QuickandClaude Opus 4.8 1d5b1f4f81 fix(startup): recover from panics in deferred background init
The deferred background-init steps run in a detached goroutine after the
HTTP listener is already accepting connections. An unrecovered panic in any
step (queue seeding, legacy cleanup, scrobble sweep) would crash the entire
live server. Wrap each step in a recover that logs the panic with a stack
and continues to the next step.

Found during PR #21 review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:39:08 -04:00
QuickandClaude Opus 4.8 6c8ddac627 revert(startup): keep open-scrobble sweep deferred for fast startup
Reverts 7161c86f. Running the sweep synchronously before the listener could
add up to 30s to restart-before-playback when a watch provider is
unreachable, which regresses the deliberate startup-deferral from dfa0f686.
Prefer the fast-startup behavior and accept the small window where a resume
immediately after restart may create a duplicate scrobble; the sweep returns
to the deferred background-init list. (Panic-safety for that list is added in
a follow-up commit.)

Per PR #21 review decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:37:33 -04:00
QuickandClaude Opus 4.8 f4d6966404 fix(catalog): scan air_timezone in paginated item queries
scanItemsWithTotal was not updated for the new air_timezone column, yet the
shared column lists it reads (itemColumns, qualifiedListItemColumns) include
it. Search and BrowseFavorites build their SELECTs from those lists with
COUNT(*) OVER (), so each row carried one more column than the scan had
destinations and every call failed at scan time with a pgx mismatch. Add the
missing &item.AirTimezone target between AirTime and ShowStatus.

Found during PR #21 review (critical: Search/Favorites runtime regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:37:18 -04:00
QuickandClaude Opus 4.8 7161c86f73 fix(startup): sweep open scrobbles before accepting playback
The open-scrobble sweep was queued in the deferred background-init list,
which runs concurrently with the HTTP listener; a resume immediately after
restart could start new scrobbles before the previous process's open
sessions were stopped, leaving overlapping/stale scrobbles on remote
providers. Run the sweep synchronously before the listener starts, bounded
by a 30s timeout so an unreachable provider can't hang startup (the heavier
non-critical init stays deferred).

Addresses PR #21 review (P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:26:15 -04:00
QuickandClaude Opus 4.8 8b82906c7b fix(metadata): persist a cleared air_timezone instead of skipping it
Clearing a previously-set air timezone sent JSON null, which decodes to a
nil *string that UpdateMetadata treats as "skip this column", so the old
value remained. The dialog now sends "" (accepted by ValidateAirTimezone),
and UpdateMetadata maps air_timezone through NULLIF so an empty value
persists as SQL NULL (matching the nullable column) rather than "".

Addresses PR #21 review (P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:26:15 -04:00
QuickandClaude Opus 4.8 b7979e2f9f fix(catalog): re-check orphan status when deleting library items
Orphan detection moved outside the media_items delete in the batched
library-delete rewrite, opening a TOCTOU race: a concurrent scan/import
could attach one of the collected content IDs to another library between
collectOrphanBatch and the delete, after which the unconditional
`DELETE FROM media_items WHERE content_id = ANY($1)` would still remove the
shared row and cascade away the newly-added membership — dropping the item
from the other library. Re-check the orphan invariant inside the delete
(NOT EXISTS a membership in another folder) and count rows actually deleted.

Addresses PR #21 review (P1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:26:15 -04:00
QuickandClaude Opus 4.8 f8493a47f1 fix(web): accessibility & UX fixes from visual QA pass
Accessibility (WCAG AA):
- Lighten the Standard-theme `--muted-foreground` (#6e6e78 -> #9696a0,
  ~3.4:1 -> >=5.3:1) and darken the light-theme equivalent so secondary
  text meets 1.4.3 contrast app-wide; the opt-in High Contrast mode is no
  longer the only conformant path.
- Give icon-only controls accessible names (4.1.2): the password show/hide
  toggle (also drop tabIndex={-1} so it's keyboard reachable), and the
  Edit/Delete/health/copy/refresh actions across the Users, Libraries,
  Nodes, API Keys, Catalog Maintenance and Job History admin tables.
- Fix the Switch off-state (invisible track -> visible border + fill) and
  the PlaybackSettings SettingRow label association (the <label htmlFor>
  pointed at a wrapping <div>; the id now lands on the Switch/SelectTrigger).
- Login: wrap the card in <main> and add an <h1>; Profiles: add an
  accessible PIN-protected label and a corner lock badge.
- Player + catalog: role="status" on the initial loading overlay; scope the
  catalog count ("0 in library" for search) and announce it via aria-live;
  trim the verbose poster-link name to the title.

UX / consistency:
- Emphasize overdue scheduled tasks (warning colour + icon + word, not
  colour alone).
- Per-source catalog subtitles instead of one shared string.
- Add a Reconnect affordance when the admin log stream drops (it does not
  auto-retry).
- Page titles for Watch Party + all admin sub-pages (incl. plugins); admin
  heading capitalisation normalised to Title Case.
- Show "dev build" instead of "unavailable" when no build revision is
  stamped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:47:11 -04:00
QuickandClaude Opus 4.8 5c3c92da28 build: stamp git revision via Makefile ldflags
`make build` did not inject buildinfo's `revisionOverride`/`dirtyOverride`
ldflags (the Dockerfile already does), so binaries built via make report
their version as "unavailable" in the admin Build panel whenever Go's VCS
metadata isn't embedded. Mirror the Dockerfile by computing the git
revision + dirty state and passing them through `-ldflags -X`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:46:56 -04:00
QuickandClaude Opus 4.7 731cdd35fc fix(migrations): make air_timezone column add idempotent
Use ADD COLUMN IF NOT EXISTS so re-running 162 on a database that already has
the column is a no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:11:11 -04:00
QuickandClaude Opus 4.7 dfa0f6861f perf(startup): defer non-critical init off the HTTP listener path
Collect catalog-size-dependent seeding (metadata match queues, legacy
series-group cleanup) and the watch-provider scrobble sweep into a
backgroundInit slice that runs sequentially in a background goroutine after
the server is ready, instead of blocking startup before the listener accepts
connections. Steps log failures and stop early on shutdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:11:11 -04:00
QuickandClaude Opus 4.7 196f753d1a fix(catalog): clamp still/poster/logo backdrops to largest cached variant
Episode stills used as backdrops only exist at w500/w300 in the cache, so
requesting a w1280/w1920 backdrop width 404s. Add catalog.BackdropVariantPath
+ imageTypeFromCachedPath and route featured (w1920) and Continue Watching /
Next Up (w1280) backdrops through it; still/poster/logo paths clamp to their
type's largest cached variant while real backdrops keep the requested width.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:11:11 -04:00
Quick c9294947b1 refactor(catalog): wrap orphan-batch iteration error 2026-05-28 17:02:53 -04:00
Quick 731235f6a5 fix(catalog): delete libraries in deadlock-retrying batches
Replaces the single multi-minute delete transaction with phased, batched
autocommit deletes (orphan items, media files, memberships, folder row),
each retried on deadlock. Holds only short locks, survives concurrent
writers, and is resumable on failure.
2026-05-28 17:02:53 -04:00
QuickandClaude Sonnet 4.6 486ecafbb5 refactor(catalog): make image-dir helpers querier-agnostic
Add rowQuerier interface satisfied by both *pgxpool.Pool and pgx.Tx.
Split collectImageDirs into collectRawImageDirs (raw collection) and a
thin wrapper that filters via filterUnreferencedImageDirs. Both helpers
now accept rowQuerier so a later task can call them from pool-level
batch deletes without an open transaction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:02:53 -04:00
Quick 0be713b30c feat(catalog): add deleteInBatches loop helper 2026-05-28 17:02:53 -04:00
Quick 485c6e28fd test(catalog): clarify cancel-path expectation in retry test 2026-05-28 17:02:53 -04:00
Quick e159f0b4ba feat(catalog): add deadlock-retry helper for batched deletes 2026-05-28 17:02:53 -04:00
QuickandClaude Opus 4.7 1ab4e1f269 docs: add implementation plan for resilient library deletion
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:02:53 -04:00
QuickandClaude Opus 4.7 9272203d8c docs: add resilient library deletion design spec
Batched, deadlock-retrying rewrite of delete_library to replace the
single multi-minute transaction that deadlocks on large libraries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:02:53 -04:00
Quick 0e73e6c240 Merge branch 'main' of https://github.com/Silo-Server/silo-server 2026-05-28 13:11:49 -04:00
QuickandGitHub b403954ced Merge pull request #19 from fluxis/mr/a-matcher-reliability
fix(metadata): matcher reliability — Sonarr token cleanup, bare-bracket IMDb IDs, year-corroboration, provider-priority tie resolution
2026-05-28 13:11:43 -04:00
Quick 3f49271f63 fix(metadata): tighten matcher and bare IMDb parsing 2026-05-28 13:09:56 -04:00
Silo Server DeveloperandClaude Opus 4.7 5d6be3ad4b chore(lint): gofmt single-space alignment in root_inference.go var block
When inferProviderTagRe was broadened to handle unsubstituted Sonarr token
placeholders ({TvdbId}/{imdb-}), the regex grew long enough that gofmt prefers
single-space rather than column-aligned spacing across the var block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:58:06 +02:00
Silo Server DeveloperandClaude Opus 4.7 416fba8dab feat(metadata): match sole exact-title candidate despite year off by <=2
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>
2026-05-28 17:58:06 +02:00
Silo Server Developer 17d156a7ca fix(naming): parse bare bracketed IMDb IDs ([tt10011226]/{tt...})
Folders tagged with a bare IMDb id in brackets (Plex/Kodi style, e.g.
'17 Blocks (2021) [tt10011226]') had the id silently dropped — folderIDPattern
needs an 'imdb-' prefix and trailingImdbIDPattern needs an un-bracketed trailing
tt-id. Recognize bracketed bare tt-ids so these items get the trusted-ID match
path instead of falling to title+year scoring.
2026-05-28 17:58:06 +02:00
Silo Server Developer 6c071e7248 diag(matcher): debug-log provider search query + per-provider result counts
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.
2026-05-28 17:58:06 +02:00
Silo Server Developer c89d31d0db feat(matcher): accept cross-source-corroborated ties without a hint year
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.
2026-05-28 17:58:06 +02:00
Silo Server DeveloperandClaude Opus 4.7 b5e4213e81 feat(matcher): resolve cross-source ties by library provider priority
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>
2026-05-28 17:58:06 +02:00
Silo Server DeveloperandClaude Opus 4.7 24f516077c diag(matcher): debug-log per-candidate match scores
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>
2026-05-28 17:58:06 +02:00
Silo Server Developer 747eef2c6b fix(matcher): tolerate concurrent-merge ErrItemNotFound in series episode-link ensure
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.
2026-05-28 17:45:38 +02:00
Silo Server DeveloperandClaude Opus 4.7 f3cd1e6ab5 test(matcher): exercise the single-distinct-show guard properly + conflicting-ID case; doc notes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:45:38 +02:00
Silo Server DeveloperandClaude Opus 4.7 e1320c8817 feat(matcher): auto-accept a year-corroborated single distinct show
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>
2026-05-28 17:45:38 +02:00
Silo Server DeveloperandClaude Opus 4.7 7922224839 test(naming): document bare-id trade-off; cover CJK title + movies numeric
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:45:38 +02:00
Silo Server DeveloperandClaude Opus 4.7 534f7bb901 fix(naming): numeric-only titles are not bare provider IDs
'86' / '22 7' were parsed as trailing tvdb ids, tripping the trusted-ID gate
so the correct title match was rejected. Require a letter in the name before
treating a trailing number as a bare id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:45:38 +02:00