0f9584ef42fd277d4e589f4bbbc352d4a4b355dc
727
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0f9584ef42 |
fix(overlays): derive dynamic-range badges from Dolby Vision metadata (#365)
* fix(overlays): derive dynamic-range badges from Dolby Vision metadata
Dolby Vision files were labeled with a generic "HDR" badge everywhere
outside the Media Info dialog, because badge sites only consulted the
bare FileVersion.hdr boolean even though the payload carries
video_tracks[].dolby_vision / dv_profile / video_range_type /
color_transfer.
Web: add a shared helper (web/src/lib/videoRange.ts) that derives a
display label ("DV", "DV HDR10", "DV HLG", "HDR10+", "HDR10", "HLG")
from the probed video tracks, mirroring the server-side normalizeHDR
vocabulary, with the hdr boolean kept as the last-resort fallback for
stale pre-DV probe rows. Use it in QualityBadges, VersionDropdown,
VersionFlyout, and the player HUD (playback-info) in place of the
hardcoded "HDR" literal. Badge styling is unchanged.
Server: in internal/overlays/summary.go, break bestFile resolution
ties by richness of dynamic-range metadata (DV > explicit HDR10/HLG
via color_transfer > bare hdr boolean > SDR) so a first-scanned
generic-HDR file no longer masks a Dolby Vision sibling on card
overlays. Full ties still keep the earliest file, so the selection
stays deterministic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(overlays): detect Dolby Vision via dv_profile and DOVI range type
Probed rows set DolbyVision and DVProfile together, but catalog-seeded
or imported tracks can carry only dv_profile or a DOVI* video_range_type
with an empty dolby_vision string. Share one hasDolbyVision predicate
between rangeRank and normalizeHDR so those rows rank and label as DV,
matching the web videoRange helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(overlays): read video_range_type and hdr10_plus in hdrTypeFromTracks
Review follow-up: hdrTypeFromTracks only inspected color_transfer, so
server card overlays could never label HDR10+ and a track whose only
signal is video_range_type (e.g. catalog-seeded rows without probed
color metadata) fell through to the bare-boolean tier of rangeRank —
inconsistent with hasDolbyVision, which already reads the range type.
The server now shares the web helper's detection order exactly, making
the web/server vocabulary mirror claim true.
Also drops the unused video_range field from the web VideoRangeTrack
interface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
|
||
|
|
f377be9d6e |
feat(catalog): include per-episode overlay summaries on season episode listings (#364)
* feat(catalog): include per-episode overlay summaries on season episode listings Episode cards already render user-configurable overlay badges everywhere except the series/season detail pages: the season-episodes endpoint never included overlay_summary, so SeasonEpisodeGrid and EpisodeRow had nothing to render. Server: add overlay_summary (omitempty) to episodeResponse and populate it in buildEpisodeResponses via overlays.BuildSummary over the episode's already-loaded media files, access-filtered with FilterMediaFilesByAccess to match the browse/sections paths. No extra queries; the files were already batch-fetched for the files[] payload. Additive-only API change. Web: extend EpisodeListItem with overlay_summary, add overlayDataFromEpisodeListItem (shared extract helper), and render CardOverlays (variant="wide") on the episode stills in SeasonEpisodeGrid and EpisodeRow using useOverlayPrefs, matching the ContinueWatchingCard pattern. The hook already honors the admin kill switch and per-user prefs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(catalog): apply library restrictions in FilterMediaFilesByAccess without a quality ceiling FilterMediaFilesByAccess short-circuited whenever MaxPlaybackQuality was empty, skipping the AllowedLibraryIDs/DisabledLibraryIDs checks that FileAllowedByAccess enforces. For viewers with library restrictions but no quality ceiling, callers (episode listings, browse/section overlay summaries, item detail versions) received files from restricted libraries. Short-circuit only when no access criteria are set at all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
035ef457a4 |
fix(web): derive settings language options from canonical language list (#363)
The profile and device settings language dropdowns (preferred subtitle language, spoken language, metadata language, device audio language, and per-library audio/subtitle overrides) were built from two hardcoded 12-language lists, so Dutch and 24 other languages supported by the canonical list in languageNames.ts could not be selected and stored codes like "nl" rendered as raw codes instead of names. Derive both settings option lists from the shared LANGUAGES array and route getLanguageLabel through getLanguageName, so every supported language is selectable everywhere and stored values always render a proper display name (e.g. "nl" -> "Dutch"). Options are now alphabetized by label, matching the other language dropdowns that already use LANGUAGES; the "No preference" sentinel stays first. Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c36d70dcba |
feat(playback): enable protocol v3 by default
The playback.protocol_v3_enabled flag shipped seeded to 'false' as a rollout safety valve and was never exposed in any settings UI. Current Android clients are v3-only for video, so against a server with the flag off they refuse playback with a misleading "update your server" error even on the newest image. Flip the flag to 'true' everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
be3bfafeaa |
fix(api): log events websocket upgrade failures with handshake shape
The events handler silently swallowed gorilla upgrade errors, which hid a client bug that produced 19k+ failed upgrades in a week (the Android client's auth plugin was demoting wss to https, arriving here as a plain GET). Log the error plus the upgrade-relevant request headers so a failing client is diagnosable from the server alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18283c2c9b |
fix(playback): HLS-safe audio policy and surround-preserving transcodes
Two audio fixes on the V3 planner and transcode pipeline: - Copied DTS in an HLS route drags Media3's audio clock (device stall corrections, ~0.3x pacing, frozen position reports). DTS/TrueHD/PCM are not HLS-native codecs regardless of the client's progressive decode claims, so HLS remux routes now convert them to AAC. Validated on the Shield: the copy-remux fallback went from ~0.3x pacing to exactly real time. - Transcodes no longer hard-downmix to stereo: multichannel sources keep 5.1 through the AAC re-encode (384k, -ac 6), plumbed through TranscodeOpts, the planner result, and the transcode-node protocol (new optional target_audio_channels field, ignored by older nodes). Also logs one "playback plan decided" line per V3 start (decision reason, delivery, play method, DV profile, quality inputs) so route selection is reconstructible from server logs alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3ddc74f784 |
fix(playback): apply DV7 RPU strip on client-driven copy restarts
HandleStartTranscode built its ffmpeg recipe purely from the client request, so a Dolby Vision Profile 7 source restarted with copy video (the V3 recovery fallback after a progressive failure) shipped raw BL+EL+RPU NALs labeled as plain HEVC. The V3 start path derives the strip from the plan and the audio-switch restart derives it from the durable session route; this endpoint now derives it the same way (session RemuxDVMode or source DV profile 7 + copy video) for both the local transport and the pooled-node dispatch, including the node's reconstruct recipe card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a196b0844e |
feat(notifications): add Android FCM push delivery via the relay (#409)
Extend the push pipeline to Android devices through the Silo push relay's /v1/fcm/send endpoint. push_devices gains platform-conditional FCM token columns (encrypted at rest with row AAD, hashed like APNs tokens), the generic POST /notifications/push/devices endpoint the Android client already calls registers FCM tokens, and fanout, operational dispatch, retries, and terminal UNREGISTERED device disabling all reuse the existing Apple machinery. Delivery is gated by a new notifications.android_push_delivery_enabled setting, advertised through the capability endpoint's android_push block, and testable via POST /admin/notifications/push/fcm/test. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
075e217477 |
feat(playback): plan v3 routes from pooled node capabilities (#408)
* feat(playback): plan v3 routes from pooled node capabilities Protocol v3 planning previously gated every server transformation on the API host's local ffmpeg probe, so deployments whose toolchain lives on transcode nodes (libx264/aac/dovi_rpu on nodes, minimal binary locally) received conversion terminals before transport preparation ever consulted the selected node's capabilities. Planning now draws on two registries split by executor pool: - Registry stays the local probe and keeps gating progressive remux routes, which execute in this process and can never offload. - HLSRegistry widens availability for HLS deliveries with the pooled transcode nodes' advertised transformations (name and recipe version pinned to the local specs), fetched concurrently under a short planning deadline through the existing TTL cache. Failures are now negatively cached so an unreachable node costs one timeout per window rather than one per start. The remux family picks the executor per branch: a recipe needing transformations only nodes carry skips the progressive remux and ships the same recipe on the HLS remux delivery instead. The local-fallback path in prepareTransportV3 now validates plans against the local registry's advertised set — mirroring the per-node validation — and returns the existing retryable transcode_node_capability_unavailable terminal when no executor can run the recipe, instead of spawning an ffmpeg that would fail at runtime. Deferred from PR #398 review (comment 3579105380). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): harden union capability planning from review Addresses all four review findings on the capability-union feature: - Select capability-matching nodes: plans carrying server transformations now restrict node selection to nodes whose advertised capabilities validate against the plan (nodepool.PlanSessionWith with a set-lookup predicate), so heterogeneous pools cannot load-balance a recipe onto a node that would reject it while a capable sibling exists. Transformation-free plans keep pure load-based selection. - Split the capability cache by consumer: planning honors negatively cached fetch failures (one timeout per window), while the transport path fetches through them — a memoized 3s planning deadline must not reject an already-selected node that the 10s transport budget could still validate. - Gate node-widened availability on the HLS engine: a progressive-only client that needs audio conversion keeps its specific retryable audio_conversion_unsupported terminal instead of falling through to a non-retryable adaptation_unavailable for routes it can never run; the DV strip union flag is gated identically. - Make HLSRegistry a lazy, memoized producer: the planner only builds the widened registry when a route decision depends on node capabilities, so direct-play and other source-preserving starts never wait on node capability fetches (or their dead-node deadlines). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
854d07cf8f |
feat(playback): add protocol v3 planning and recovery (#398)
* docs(playback): plan protocol v3 server implementation * docs(playback): incorporate protocol v3 review * feat(playback): implement protocol v3 server * fix(playback): persist empty route diagnostics * feat(playback): harden protocol v3 HDR routing * feat(playback): complete protocol v3 client contract * fix(playback): harden protocol v3 recovery * fix(playback): restore dovi_rpu strip filter for DV remuxes The v3 work renamed the Dolby Vision strip recipe to a dovi_split=mode=bl bitstream filter that does not exist in stock FFmpeg or jellyfin-ffmpeg; the probe failed closed on every deployment, disabling the new validated DV7-to-HDR10 route and regressing the previously working dovi_rpu=strip=1 remux path from main. Restore dovi_rpu across the probe, remux and HLS copy arguments, and the recipe-card constant. Also from review: validate the remux DV mode for every profile (garbage modes on non-P7 sources silently no-opped), reject preserve mode for P7 outright (a base-layer-only remux cannot preserve dual-layer DV), tag dvhe sample entries only for the explicit v3 preserve recipe so legacy web/jellycompat remuxes keep their pre-v3 hev1 labeling, and honor the token-frozen DV mode in the proxy remux path instead of legacy-auto. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): correct v3 planner policy and contract validation Review fixes to the v3 planner and wire contracts: - Bar Profile 7 sources from the non-strip progressive remux route: a base-layer-only remux can never deliver native dual-layer DV, so the planner no longer emits plans claiming validated Dolby Vision while the executed remux drops the enhancement layer. - Accept the device-quirks feature flag from either capability location, matching every other dual-location feature check. - Treat legacy hdr_unknown rows as HDR10 for HDR10-capable clients with a degradation warning instead of leaving them unplayable under v3. - Honor bandwidth_cap_kbps as a hard ceiling in every quality mode and wire the previously dead Metered signal into conservative auto rungs. - Degrade to the validated source-quality route instead of a terminal when only an implicit quality reduction demanded an unsupported transcode; explicit user-selected rungs keep terminal behavior. - Bound inner capability lists and strings; compare attempt keys exactly instead of case-folded; make ParseTrackIDV3 strict about canonical numerics; accept dvdsub/pgssub/dvbsub aliases and stop promising burn-in for unknown subtitle codecs; probe every h264 encoder rather than requiring libx264; normalize the file-level bitrate fallback. - Evaluate subtitle renderability against the engine each candidate route executes on, not always media3_direct. - Pin the with-quirks attempt-key preimage arity in the cross-language fixture so the Kotlin client stays in lockstep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): harden v3 control-plane reliability Review fixes to the v3 session, store, and handler layer: - Bound concurrent replans with a slot semaphore: each replan pins a pooled connection for its advisory lock while issuing further store queries from the same pool, so an unbounded recovery storm could turn every connection into a lock holder and deadlock the server. - Make CompleteReplan a real compare-and-swap (base-revision predicate, ErrReplanSupersededV3) and map BeginReplan insert races to a replay instead of a raw unique violation. - Fingerprint start requests (request_digest column): an attempt ID reused with different input is now a 409-style conflict rather than a silent replay, and both replay paths check session liveness so dead sessions surface as retryable terminals. - Pre-delete expired attempt rows on SaveAttempt so a retry during the cleanup window cannot wedge on an unreachable conflict. - Align the in-memory store's semantics with Postgres and add DB-backed planstore tests (SILO_TEST_DATABASE_URL), including a regression test inserting every route-event name against the real CHECK constraint. - Session manager: v3 route-set updates own RemuxDVMode outright so a replan onto an SDR source clears a stale strip mode; replacement reservations survive unrelated legacy stream updates; replacement admission excludes the replaced session explicitly instead of decrementing totals it may no longer be part of; the admission CAS loop is bounded and decider errors are logged. - Map transient store failures to 500s instead of terminal 404/403s; authorize route events via identity-only projections after the rate limiter; keep sanitized diagnostics deterministic. - Merge the server-computed durable plan key into replan exclusions so unreproducible client history cannot re-select the failed route. - Remap tracks only when the effective edition changes (a same-file replan no longer switches audio to a lookalike track) and remap ID-only subtitle selections on edition fallback. - Cache the v3/shadow feature flags for five seconds instead of one settings SELECT per playback request; stop remote transports best-effort when the start call times out; carry dvm/tid claims and the transport-scoped job identity through the legacy audio-change re-mint; index playback_route_events(received_at) for the retention delete; run store maintenance for DB-less deployments too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transcode): reap idle node jobs and gate WebVTT conversion - Add an idle reaper to the transcode node: a job untouched by manifest or segment requests for ten minutes is closed and unregistered. After a v3 replan retires a transport ID, a stale in-flight stream token could resurrect the old job via reconstruct and encode to end-of-file for nobody; jobs waiting on readiness count registration as access and are never reaped mid-wait, and reaping keeps the recipe so a still-valid token reconstructs on the next hit. - Reject bitmap subtitle tracks (PGS) on the .vtt conversion path with 415 before headers are written instead of spawning an ffmpeg command that always fails mid-response, and make the extract-format override fall back to source-driven mapping for bitmap codecs. - Drain error bodies on non-202 node responses so the HTTP transport can reuse connections. - Pin the transcode-dir cleanup separator-boundary semantics with a regression test (a session ID sharing another's prefix must not retain foreign directories). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): close v3 planner policy gaps from review - Clamp the final transcode bitrate to bandwidth_cap_kbps: the ladder has no rung below 480p/1500kbps, so lower caps were silently exceeded even though the cap is documented as a hard delivery ceiling. - Treat video-only media as audio-compatible instead of forcing an AAC conversion (or an audio_conversion_unsupported terminal) onto a file with no audio stream. Tracks whose codec failed to probe keep the gate. - Only promise a bitmap subtitle sidecar for embedded PGS with an engine that renders embedded bitmap: external/downloaded bitmap and embedded DVD/DVB published artifact URLs that always failed at fetch. They now fall through to burn-in or its terminal. - Accept client_video_transformations_v1 from either client_features or the nested context when validating client-executor transformations, matching the planner's dual-source reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): probe and execute DV remuxes with one ffmpeg binary The v3 transformation registry probed the configured playback.ffmpeg_path while progressive remux execution resolved the process-global discovery path, so a deployment where only one binary carries dovi_rpu could plan a server_dv7_to_hdr10 route and then fail it at stream time. Resolution now goes through a shared ResolveFFmpegPath (configured path first, discovery fallback — the same rule the transcode pipeline already used), the dovi_rpu probe is cached per binary path, and the stream handler and proxy worker pass their configured path into ServeRemuxWithDVMode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): harden v3 replan identity and control-plane limits - Seed failure-replan track selections from the durable current plan before overlaying the request: after an alternate-version fallback the normalized request still carries requested-edition track IDs, so a replan omitting unchanged tracks was rejected as a track/file mismatch. - Remap ID-only audio selections across edition changes (parse the ID to an index like the subtitle remap already does) instead of leaving a stale file-bound ID to fail validation. - Release the node planner reservation when a prepared remote transport rolls back after the node accepted the job; repeated failed starts could otherwise pin max-job/bandwidth budgets for the full reservation age. - Size the replan semaphore below the PostgreSQL pool via a store capacity advisor: with max_connections at or below the fixed bound, advisory-lock holders could starve the inner store queries they need to finish. - Contain shadow-planner panics with a recover boundary; it runs on a bare goroutine where an escaped panic kills the process for what is telemetry-only work. Document why the memory store's session lock is deliberately a no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transcode): serialize node job teardown against reconstructs - Look up and touch manifest/segment sessions in one critical section so the idle reaper cannot unregister a job between the lookup and its liveness refresh. - Re-validate each reap candidate under the per-session lifecycle lock before closing it: Close removes the output directory, and without the lock it could race a token reconstruct and wipe the segments the fresh ffmpeg is writing. - Take the lifecycle lock in handleStop so a stop racing a RequireReady start's readiness wait blocks until registration and tears the job down, instead of 404ing and orphaning the ffmpeg until the reaper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
28c6ddc237 |
feat(playback): add per-user transcoding controls (#375)
* feat(playback): add per-user transcoding controls * fix(playback): enforce forced video transcode permission * chore: address transcode control review feedback * fix(playback): recheck transcode permission on audio switch --------- Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com> |
||
|
|
3fd0912cb3 |
fix(notifications): align with Worker push relay (#374)
* fix(notifications): align with Worker push relay * fix(notifications): disable invalid APNs tokens |
||
|
|
df9cacc8da | fix(web): keep playback running during next up | ||
|
|
02b6648995 | feat(notifications): harden push relay credentials (#373) | ||
|
|
c1c110e3d2 |
feat(playback): improve web subtitles and track selection (#362)
* fix(player): keep text subtitles in sync across copy-mode restarts and sparse cue windows - Rebase already-loaded cues in place when streamOriginSeconds changes (copy-mode session restart) instead of leaving them offset by the delta. - Stop inferring end-of-input from where a window's cues stop; only the known media duration marks EOF, so a dialogue gap no longer silently ends prefetching for the rest of playback. - Anchor the first window fetch to the intended start position (resume target or pending seek) while the element still reports currentTime=0, and reset coverage on forward seeks past the fetched window. * fix(playback): align encoded transcode start with the declared segment boundary A mid-segment start (resume, seek restart, audio switch) spawned ffmpeg at the raw seek position while labeling its first segment with the grid number, whose synthetic-manifest start is up to one segment earlier. hls.js aligns the first fragment's content to that declared position, shifting the whole session's timeline late by seek mod segment_duration (0-2s): subtitles trail dialogue by a constant per-session offset and progress/resume positions drift by the same amount. Snap the ffmpeg start position down to the segment boundary for encoded sessions so declared and produced timelines match exactly; the player still seeks to the precise requested position. Copy-mode sessions serve ffmpeg's real manifest and keep the raw seek. * fix(api): forward http.Flusher through response-writer middleware wrappers Streamed subtitle extracts (and any progressive response) flush per chunk via an http.Flusher assertion, but none of the status-capturing middleware wrappers implemented Flush, so the assertion failed and cues sat in Go's response buffer until ffmpeg finished. On large remuxes where a 600s window takes 20s+ to demux, captions appeared only when the whole window completed instead of within the first seconds. Give every wrapper a Flush() (satisfies plain assertions, including chi's Compress) and Unwrap() (satisfies http.ResponseController). The jellycompat image-proxy tag rewriter flushes only in passthrough mode since it buffers JSON bodies for rewriting. Regression test asserts the API chain forwards Flush end to end. * feat(player): PGS subtitles honor size, position, and background settings Port the tvOS/iOS bitmap-cue styling to the web player. libpgs now decodes in worker mode but draws to a hidden source canvas on the main thread; a compositor detects cue regions from the frame's alpha channel and re-places them on a visible overlay canvas per the shared subtitle appearance settings: size scale (with the 0.85 authored-size compensation), vertical position preset (dialogue-band cues only — floating signs keep authored placement, matching the Apple implementation), and the background box. Font family, text color, and outline are baked into the source pixels and remain inapplicable. * fix(player): anchor PGS position presets to the text overlay's reference frame The initial port used silo-apple's 30/1080 bottom margin and video-relative lower-third/top anchors; the web text overlay anchors to a 16:9 reference frame with 7%/18% offsets that extends into the letterbox for wide content. Use the same anchors so PGS dialogue lands exactly where SRT text does. * feat(player): size PGS cues to match the text subtitle line height Replace the authored-size ladder (0.85 × font-size ratio) with per-cue text-line matching: the region detector reports the tallest text line inside each cue, and the compositor scales the cue so one line of bitmap text renders at the same pixel height as the SRT overlay's font at the current preset. Authored size differences between discs no longer leak through; upscaling is capped at 2.5× to keep small bitmaps from going blurry. * feat(subtitles): opt-in windowed PGS extraction to cut mid-file load latency PGS extracts always demuxed the source from byte 0, so starting a large remux mid-file meant minutes before the first bitmap cue. The web player now opts in to windowed extraction (?windowed=1&position=&duration=) and re-points libpgs at a fresh window on seeks and near coverage end; ffmpeg input-side -ss with -copyts keeps absolute source timestamps. Without the explicit opt-in the endpoint behaves byte-identically, so Apple/Android and other single-fetch consumers are unaffected. ASS remains unconditionally non-windowed (its header only exists at offset 0). * feat(playback): cache extracted PGS subtitle tracks Every selection of an embedded PGS track re-ran a full ffmpeg extract that demuxes the entire source file from byte 0 — minutes for a large remux — and responses were Cache-Control: no-store, so repeat selections, re-watches, and multiple viewers all paid full price. Add a disk cache for full-track .sup extracts under <transcode_dir>/subtitle-cache, created lazily: - Keyed by source path hash + subtitle stream ordinal + source mtime+size (encoded in the filename), so a replaced source file implicitly invalidates its entries; the source is stat'ed on every lookup. - Cache miss: ffmpeg stdout is teed to the response (first viewer still streams progressively, first-byte latency unchanged) and into a temp file that is fsynced and atomically renamed into the cache on clean ffmpeg exit. Any error — ffmpeg failure, client disconnect, tee write failure, or the source changing mid-extract — discards the temp file, so a partial entry is never served. - Cache hit: served via http.ServeContent (Range support, Content-Length, Last-Modified from the source mtime) with a revalidatable Cache-Control instead of no-store. - Concurrent requests for the same in-flight track run their own uncached extract (mutex + in-flight key set) rather than blocking on another client's connection. - Scan-on-commit LRU eviction under a 2 GiB cap (recency tracked by bumping entry mtime on hit; atime is unreliable under relatime), plus sweep of crash-orphaned .part temp files. - Windowed PGS requests (?windowed=) bypass the cache in both directions: their output covers only a slice of the track. Both the integrated API handler and the standalone proxy subtitle path share the same playback.SubtitleCache.ServeSUPExtract helper. VTT (already windowed and fast) and ASS (small) stay uncached. No API surface change. AI-use disclosure: implemented with Claude Code. * fix(playback): check Close error returns in subtitle cache paths Silence errcheck on the cache-hit defer and the test's simulated disk-full Close. AI-use disclosure: implemented with Claude Code. * feat(playback): warm PGS cache in background and window from cached track Windowed PGS requests bypassed the cache entirely, so every window fetch re-demuxed the multi-GB original file. Now a windowed miss kicks off a detached background warm (full-track extract into the cache, at most 2 concurrent server-wide, coalesced with client-driven fills), and once the entry exists windowed extracts read the 15-80MB cached .sup instead — seeks and re-enables become near-instant after the first load. Verified empirically that ffmpeg preserves absolute PTS when windowing a sup input. * feat(player): hold playback while PGS subtitle cues load When a PGS track is enabled (or a seek lands outside the fetched window), extraction takes seconds and dialogue could play unsubtitled. The player now pauses until the renderer's parsed data covers the playhead — tracked via libpgs' parsed-timestamp watermark, the exact predicate it renders by — showing a "Loading subtitles…" indicator after 500ms. User play/pause always wins over the hold, a 20s safety timeout prevents stranding playback, and background prefetch never pauses. If future libpgs versions reshape the observed internals the hook degrades to the old play-through behavior. * perf(player): shrink uncached PGS window to 600s Draining a windowed extract from the source reads the full interleaved container across the window (~1GB per 100s of remux on measured hardware); a 3600s window cost ~12GB of reads per fetch while cold. Once the server cache is warm a window costs milliseconds regardless of size, so smaller windows only add trivially cheap re-fetches. * feat(subtitles): burn in PGS/bitmap subtitles for the web player The web player rendered PGS client-side via libpgs, which required extracting the .sup track — a cold ffmpeg demux that took seconds even windowed, since c:s copy still reads the whole interleaved container up to the playhead. Every other server (Plex, Jellyfin default, Emby) burns image subtitles into the video instead, and that is the only path with no per-seek extraction cost. Selecting a bitmap subtitle (PGS/DVD/DVB) now restarts the transcode with subtitle_burn_in at the current aligned position, reusing the same restart machinery as an audio/quality switch so the segment-boundary timeline alignment holds. The server composites the decoded subtitle onto the video with an overlay filter_complex graph (libass's subtitles= filter is text-only); overlay runs at native resolution before any target scaling, and hardware pipelines round-trip through CPU like the text path. Burn-in forces a video encode, so copy-video recipes are upgraded to h264 both client- and server-side. Text subtitles keep the instant, styled, client-side path. The .sup streaming endpoints, cache, and windowing are retained for the Apple client, which renders PGS natively. The now-dead web PGS stack (usePGSSubtitles, pgsPlacement, libpgs dep) is removed. Tradeoff: bitmap subtitles no longer honor web appearance settings (baked into the video) and toggling one restarts the transcode (~1-2s buffering), matching Plex behavior. * fix(player): rebuild text subtitle track when turning off PGS burn-in Selecting an SRT track that turned off bitmap burn-in restarted the transcode, and the client TextTrack built in that same moment was orphaned when the <video> element reloaded, so the subtitles never rendered (and a seek could not recover the dead track). Rebuild the text track once the new stream settles, gated on the burn-in-off transition so quality/audio switches and copy-mode seek restarts keep their subtitles without a needless re-extract. * fix(player): render web subtitles behind the control HUD The text subtitle overlay sat at z-20, above the controls layer (z-10), so cues painted over the bottom HUD and cluttered the control bar. Drop it to z-[5] — above the video, below the controls — so the HUD paints over the cues while it is visible. When controls are hidden the whole controls layer is opacity-0, so cues remain fully visible. * feat(player): lift web subtitles above the control bar while it's visible Rather than hiding bottom-anchored cues behind the HUD, raise them just above the control bar (measured height + a small gap) whenever the bar is visible in the foreground player, then settle them back when it hides. The bar is a roughly fixed pixel height while the cue offset scales with the player, so the bar is measured via ResizeObserver rather than hardcoded. Top-anchored cues never collide with the bottom HUD, so they stay put. z-[5] is retained as a safety so any residual overlap tucks behind the bar. * fix(player): coalesce same-tick transcode restarts into one dispatch Starting playback with a persisted bitmap subtitle fired transcode/start twice within milliseconds: the auto-start effect dispatched before subtitle auto-selection restored the burn-in, whose effect then forced a second start. The first request was already on the wire (no abort signal was passed to fetch), so the server spawned an ffmpeg only to kill it for the second start — visible in production as an ffmpeg exit error ~1ms after every such session start, and slowing time to first frame. Defer the network dispatch by one macrotask so back-to-back restart calls in a tick collapse into a single request carrying the final parameters; state updates stay synchronous. Pass the abort signal into playerFetch so a superseded in-flight request is actually cancelled, and drop any deferred dispatch on unmount so a stray transcode/start cannot land after the session's exit DELETE. * fix(catalog): resolve effective subtitle defaults for movie item details Movie pre-play subtitle selectors were missing the effective defaults (including per-item overrides saved from a previous play) that episodes and watch payloads already resolve. Extract applyToItemDetail/ applyToWatchDetail helpers and apply defaults for movies in buildMediaItemDetail. The SubtitlesPopover now also eagerly loads downloaded subtitles when the saved preference points at one so the closed trigger's Auto summary reflects the override. * feat(player): scale subtitle font size with the rendered video Replace fixed rem font sizes with px values defined at a 720px 16:9 reference height, scaled proportionally with the actually-rendered video (object-fit: contain) so subtitles keep the same relative size as the window grows or shrinks, with a 12px legibility floor. Rename useSubtitlePositionStyle to useSubtitleLayout, returning both the position style and the font scale, and add unit tests for the appearance helpers. * fix(player): satisfy strict index checks in transcode quality test * feat(player): let the pre-play Auto option clear the saved subtitle override A manual in-player subtitle selection persists as an 'always' override for that movie/series, but nothing in the UI could undo it — auto selection stayed pinned to the chosen track forever. Choosing 'Auto' in the pre-play subtitles popover now also deletes the stored preference (movie content ID / episode series ID) and invalidates item details so profile-level auto selection applies again. * feat(player): persist pre-play subtitle selections as the item override Choosing a track (or Off) in the pre-play subtitles popover only lived in component state: it applied to that playback session but vanished on returning to the detail page. Persist it through PUT /subtitle-prefs — the same 'always'/'off' override a manual in-player selection saves — keyed by movie content ID or episode series ID, and invalidate item details so the effective defaults reflect it immediately. * feat(ui): show the saved subtitle override and richer pre-play pill summaries A stored per-item override displayed as 'Auto: <language>', hiding both that an override exists and which track it is. The pre-play subtitle pill now shows the resolved track directly (name with (SDH)/(Forced) markers plus format, skipping markers the name already carries), the matching list row gets the checkmark instead of Auto, and the Auto row reads 'Reset to profile defaults'. Subtitle, audio, edition, and version pill summaries also truncate much later (max-w-44/sm:max-w-64). * fix(player): recover text subtitles from stream reloads and failed window fetches Three failure modes could silently freeze or stop web text subtitles: - A stream restart (seek-triggered transcode restart, quality/audio switch) reloads the <video> element and can orphan the programmatic TextTrack — cuechange stops firing and the last cue freezes on screen. Only the PGS-burn-in-off transition rebuilt the track. Now every settled stream URL change bumps the generation, and the rebuild carries loaded cues (converted back to source time) and window coverage over so it costs no refetch. - The sliding-window fetcher committed windowEnd before the fetch ran, so a failed or hung window counted as covered and was never retried — subtitles silently stopped for up to 10 minutes. Coverage now commits only after the window streams in fully; failures leave the range uncovered and retry after a 5s backoff. - A hung extraction (one fetch in flight at a time, no deadline) blocked every future window for the session. Reads now arm a 30s stall timer that aborts a response which stops delivering chunks; slow-but- progressing streams keep resetting it. Diagnosed from a session where ffmpeg took 69s to stream one subtitle window and a transcode restart landed mid-fetch, freezing the active cue. * fix(playback): keep subtitle selections stable across file changes * fix(web): clarify subtitle labels and positioning * fix(player): hide HUD when pointer leaves * fix(web): tidy subtitle track badges * fix(playback): remap audio tracks across file versions * fix(web): tidy audio track labels * docs(playback): clarify bitmap subtitle appearance * fix(playback): preserve selection state on restart * fix(http): preserve response state across flushes * fix(web): preserve pending quality for burn-in * fix(playback): preserve subtitle inventory identity --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
9cfd01e9c2 | Add remote playback identity handoff (#360) | ||
|
|
b7292a9473 |
fix(streaming): stop killing healthy streams at the server WriteTimeout (#361)
The main API server's WriteTimeout (120s) is an absolute deadline from request start, so every streaming response still being written at T+120s was cut mid-body with a clean close. Clients saw multi-GB direct streams truncate every two minutes; the Apple client's cursor-resume reconnect absorbed most kills silently, but one landing during backpressure or a demuxer resync exhausted its retry budget and forced a full player teardown (visible stop + historical audio desync seeding). Fix: internal/httpstream.RollingDeadlineWriter pushes the connection's write deadline forward with progress via http.ResponseController — a response that keeps moving lives indefinitely, a stalled one is still reaped within the window (180s default, SILO_STREAM_WRITE_STALL_TIMEOUT to override). ReadFrom delegates in bounded slices so http.ServeContent keeps its sendfile fast path. Wired into direct play, remux, downloads, the transcode-node proxy, and ebook serving; the server-level 120s guard stays for every other route. The metrics and request-logger response writers now implement Unwrap — without it http.ResponseController cannot traverse to the connection and SetWriteDeadline fails, silently disabling the fix (exactly what the first dev deploy showed). A middleware-chain integration test locks the whole path down against future wrappers missing Unwrap. Validated on dev: 200s/512MB direct and 300s/768MB via CDN sustained range-GETs (previously dying at 120s), zero duration_ms=120000 stream entries since deploy. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
98a22dafd2 | docs: use canonical plugin repository names (#359) | ||
|
|
648e5c76ef | test(plugins): use canonical metadata repository name (#358) | ||
|
|
58eed826a4 | test(plugins): cover repository rename compatibility (#357) | ||
|
|
10e15798e0 | feat(plugins): add approved community catalog hub (#355) | ||
|
|
d68e70bb47 |
feat(autoscan): Sonarr/Radarr webhook intake without arr API keys (#353)
* docs(autoscan): add arr webhook intake spec and implementation plan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(autoscan): add webhook intake schema migration Adds delivery_mode to autoscan_sources, the autoscan_webhook_endpoints table, and delivery_mode/provider_event_type on autoscan_events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(autoscan): add built-in arr-webhook source identity Host-discovered scan-source entry so webhook-mode sources need no plugin installation; composite lister appends it to plugin discovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(autoscan): persist delivery mode, webhook endpoints, event metadata Sources carry delivery_mode; autoscan_webhook_endpoints CRUD with SHA-256 token lookup and AAD-bound encrypted redisplay; events record delivery_mode/provider_event_type; CreateEvent gains SkipRunningCheck so webhook deliveries are never dropped by the poll exclusion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(autoscan): share the consume path and add webhook IngestChanges Extracts consumeSourceChanges from PollOnce (marker semantics preserved, existing poll tests unchanged); PollOnce skips webhook sources; IngestChanges feeds deliveries through the shared pipeline without markers and without the running-event exclusion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(autoscan): add Sonarr/Radarr webhook payload parser Host-side arrwebhook package: provider inference, import/rename/delete path extraction with vanished-path-friendly previous paths, subtree fallback, exact-path dedupe, and no-op unknown events. Fixture-backed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(autoscan): add public webhook delivery route and admin endpoint management Public POST /api/v1/autoscan/webhooks/{token} with per-IP rate limiting, 256KiB body cap, 202-for-noop semantics, and token/body kept out of logs; admin create/rotate/delete endpoint routes; source responses carry delivery mode + webhook status/URL; create/update validate delivery mode against source identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): add webhook delivery mode to Autoscan admin UI Webhook sources get a generate/copy/rotate webhook URL section, provider selector, delivery status, and a connection-free Add-source flow; activity rows badge webhook deliveries with the arr event type. Path rewrites stay editable in both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): redact secret path params from request and activity logs The request logger and activity-log middleware recorded raw URLs, so bearer credentials in secret path segments (autoscan webhook {token}, webhook-sync {secret}) were persisted to app logs and activity_log. Redact the secret segment via the chi route params in both sinks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(autoscan): make webhook delivery reliable --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8b9149a789 |
feat(catalog): remove smart collection item limit cap (#340)
* feat(catalog): remove smart collection item limit cap MaxSmartCollectionItemLimit silently clamped every smart collection to 500 items, and an unset limit defaulted to 100. On servers with large libraries this truncates collections with no indication to the admin — a per-language smart collection (original_language is fr) has 10,625 real matches and showed under 1% of them. Remove the cap and make an unset limit resolve the full result set: the default becomes a large sentinel so every consumer's int plumbing (SQL LIMIT) keeps working. An explicit positive limit in the query definition is still honored as-is — membership size is an admin decision, and every read path that serves collection items either paginates in SQL or scales with the actual collection size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(jellycompat): page smart collection browse in SQL The Jellyfin BoxSet children path resolved a smart (live-query) collection's entire membership and sliced one page locally. With the item cap removed (smart collection membership is now uncapped, an admin decision), a large collection browsed from a Jellyfin client materialized tens of thousands of rows per request — latency and memory scaling with collection size. For the default browse (no explicit SortBy), page the smart query directly in SQL via QueryExecutor.PreviewPage(offset, limit), hydrating only the requested page. Per-request work is now proportional to page size, not collection size. TotalRecordCount still reports true membership (the count query caps at the query's item limit), and past-the-end pages preserve the real total. Shared query prep is extracted into prepareSmartCollectionQuery, used by both the new paged path and the existing full-resolution path (still used by the explicit-sort allowlist branch, whose output is already SQL-paginated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> |
||
|
|
04c4344f52 |
feat(metadata): reconcile artwork cache after public S3 provider changes (#349)
* feat(metadata): reconcile artwork cache after public S3 provider changes Changing the public S3 provider previously broke every cached image permanently: the DB keeps bucket-relative keys, the image cache pipeline treats a cached path as its durable dedup marker and never re-enqueues, and clients eat the 404s straight from S3 so the server never notices. Add a storage identity fingerprint (s3.public_storage_identity, seeded via SetIfAbsent at boot) and a reconcile_artwork_cache task whose startup trigger only fires when the identity changed; manual runs always sweep, doubling as bucket-data-loss recovery. The task probes a random sample of cached objects, then either bulk-resets (near-total miss) or per-row verifies. Missing provider-sourced artwork is reset to its *_source_path so the existing enqueue loop re-caches it; surfaces without a re-downloadable source (chapter thumbnails, collection artwork, library posters, branding refs, embedded book covers) are cleared so their owning pipelines refill them. Small upload-holding tables are always per-row verified so bulk mode cannot blind-clear an upload that survived migration, and transport errors never reset rows. Users never see broken images during the transition: reset rows serve the provider's original URL via the existing absolute-URL pass-through and thumbhashes are preserved. The storage settings page now warns that uploads cannot be re-downloaded when the identity fields are edited. Part of #348 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(metadata): harden artwork reconcile per code review Address the confirmed findings from the PR review: - Fingerprint the key prefix case-sensitively and slash-trimmed exactly as s3client applies it (new exported NormalizeKeyPrefix): a case-only prefix edit is a real storage move and must reconcile; a slash-only edit is not and must not. - Certify the storage fingerprint immediately after the artwork sweep succeeds and make the 4-object branding check non-fatal (reported in the task message), so a transient branding error cannot discard a completed catalog sweep and force it to repeat every boot. - Fail closed on conditional-task preflight errors in the task manager (previously fail-open ran the task), and retry transient settings reads in ShouldRun since the startup trigger fires once per process. - Track probe HEAD errors against a separate baseline so a flaky probe cannot consume the sweep's error budget. - Probe before counting: bulk mode skips the per-surface count(*) full scans entirely, and probe sampling drops ORDER BY random() (plain LIMIT answers "is the cache in this bucket" just as well). - Verify chapter thumbnails across a whole 500-file batch in one HEAD fan-out instead of per file, keeping the worker pool saturated. - Replace the 10 inline non-provider-scheme ARRAY literals in the enqueue query with the shared nonProviderImageSchemesSQL constant. Part of #348 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(metadata): guard bulk reset against degraded probes, certify only clean sweeps Address bot review feedback on the reconcile hardening: - A probe where more than half the HEAD requests error aborts the run: errored requests are excluded from the sample, so a partial outage could otherwise present a handful of surviving 404s as a ~100% miss rate and bulk-reset the catalog. Bulk mode additionally requires a minimum number of successful samples; thinned probes and tiny catalogs take the safe per-row verify path. - Track sweep errors separately from probe/branding errors (stats.sweep_errors) and certify the storage fingerprint only when the sweep completed with zero of them — skipped rows were never verified, so the next startup retries. Applied resets stay durable. - Give each ObjectExists attempt its own timeout so a stalled HEAD fails that attempt instead of pinning the retry loop to the run context. - Report branding assets checked (not just cleared) in stats.Checked. - Drop the dead settingsRepo/brandingSvc nil guards in cmd/silo and sync spec numbers with the implementation constants. Part of #348 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4d99597966 |
feat(watchlist): honor provider list order by default (#352)
The watchlist catalog page silently returned the stored list order while its sort dropdown claimed "Date Added", and mirroring a provider's list order (e.g. MDBList) was off by default, so synced watchlists appeared in first-sync-time order with no way to tell what was happening. - Web: the watchlist source now uses the same source-order sentinel as collections — the dropdown shows "List Order" as the default, and an explicit "Date Added" pick sends sort=added_at instead of being stripped (previously indistinguishable from the default). - Server: on personal lists (watchlist/favorites) an explicit added_at sort now takes the source-order path, where added_at means "date added to the list"; the query executor path sorted by the library's created_at instead. History keeps the executor path since its ID loading ignores the sort. - watchsync: new connections default sync_watchlist_order_enabled to true, with a migration flipping existing rows to match. Providers without the provides_watchlist_order capability ignore the flag at sync time. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e96a8a0cf8 |
feat(search): binary-quantized embedder vectors — optional, default-on for fresh installs (#351)
* feat(search): binary quantization setting for the Meilisearch embedder
New server setting catalog.search.meilisearch.binary_quantized
(default false) threads into the embedder index settings
("binaryQuantized": true) and into the schema-version hash, so flipping
it closes the sync gate and mandates a rebuild in both directions —
Meilisearch cannot de/re-quantize an index in place.
With 3072-dimensional embeddings this cuts vector storage ~32x
(≈12KB → 384B per document), keeping the whole vector store in page
cache: rebuilds and hybrid queries get sharply cheaper. Hybrid search
(keyword + semantic) cushions the small relevance cost of sign-only
vectors.
The hash token is appended only when the flag is set, so indexes built
before this change keep their schema version while it stays off.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(search): binary quantization default-on for fresh installs + admin toggle
- Migration seeds catalog.search.meilisearch.binary_quantized=true only
when no active catalog search index exists. Existing deployments stay
unset (= off): flipping quantization changes the index schema-version
identity, which closes the incremental-sync gate until a full rebuild
runs — that must never happen implicitly on upgrade. Fresh installs
have no index yet, so their first rebuild simply starts quantized.
- Search settings page gains the toggle with an explicit
"requires a full index rebuild" warning, a status row, and settings-
search keywords.
Prod benchmark (607.9k docs, 3072-dim vectors, N=10 medians, replicated):
hybrid 0.5 unchanged (7.5ms float vs 8.0ms quantized, within ±2ms
keyword-control jitter); pure semantic 9ms → 4ms; on-disk index 18G →
8.3G; rebuild duration unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(search): address review feedback on binary-quantized embedder
- Add catalog.search.meilisearch.binary_quantized to the restart-required
registry. The provider freezes BinaryQuantized into MeilisearchProviderConfig
at construction, so without this a toggle-then-rebuild in the same process
builds a quantized schema while the live provider still compares against the
old value and falls back until restart (Codex P2).
- Validate binary_quantized in HandleUpdateSetting, mirroring semantic_enabled.
A raw API write of a non-bool previously persisted unnormalized, then failed
CatalogSearchSettingsFromMap on load and silently reverted the entire search
config to Postgres defaults.
- Gate the binary_quantized token in the schema-version identity on
semanticEnabled: with semantic off the index has no embedders, so the flag
has no on-index effect and must not force a pointless rebuild. Stays
byte-identical to a pre-flag index. Covered by a new test.
- Clarify the seed migration comment (guard is "no active index", which also
covers Meilisearch-configured-but-never-indexed deployments) and the UI hint
(~30x smaller raw vectors, index roughly halves; only applies with semantic).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
3e340552a8 |
fix(search): raise Meilisearch task-wait timeout from 5m to 2h (#347)
Semantic (vector-embedded) index batches can take Meilisearch many minutes on modest hardware, and Meilisearch auto-batches consecutive queued document tasks — with rebuild_task_queue_depth tasks in flight the oldest task's completion covers the whole fused unit. WaitTask's 5-minute default deadline killed four prod rebuilds mid-batch (2026-06-29 x2, 2026-07-08 x2) with "context deadline exceeded", each time leaving incremental sync blocked on a stale schema version and an unbounded event backlog (1.6M+ events). The wait still exits immediately when the task reaches a terminal state (succeeded/failed/canceled); the deadline only bounds tasks genuinely still processing, where giving up guarantees rebuild failure and gains nothing. Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a2ef26bece |
perf: root-cause fixes for endpoints still slow after #292 (NextUp, series badges, resume tail, subtitle fonts) (#350)
* docs(plans): root-cause analysis for endpoints still slow after PR #292 Five endpoint groups stayed slow after the home/Continue Watching/Latest latency work shipped: Resume (110s p95), NextUp (17s p95), Latest (17s), /Items, and the home sections routes. The caps and caches from PR #292 are live in the deployed binary; they bounded how many rows the loops touch but not what each underlying query costs. Documents the four confirmed root causes (4.3M stale completed-with-position progress rows + missing resume index, unbounded next-up anchor scan, per-episode series rollup fanout, two index-starved history/scanner paths) with live EXPLAIN ANALYZE measurements and the fix plan implemented by the follow-up commits. AI-use disclosure: analysis and doc produced with AI (Claude) assistance. * perf(catalog): bound the global next-up anchor scan to recent completions The completed_episodes CTE in buildListNextUpQuery derived per-series anchors from the profile's ENTIRE completed history — DISTINCT ON over 233k rows joined to episodes for the worst bulk-import profile, then a per-series LATERAL that scans every episode of a fully-watched series before yielding nothing. 648 slow executions in a 19h window, 44.7s worst; this drove /Shows/NextUp (17.1s p95) and the next-up injection on the native home sections aggregate. Global queries now derive anchors from the profile's nextUpAnchorMaxRows (500) most recent completed rows — an ordered index walk on idx_uwp_profile_completed, with the hidden-items exclusion and date cutoff applied inside the bounded scan so hidden/old rows never consume the anchor budget. A next-up rail surfaces ~24 series; the 500 most recent completions cover every series that can realistically rank on it. Series-scoped calls (the show-detail tile) keep the unbounded shape: they must anchor on the series' last completed episode no matter how long ago it was watched, and are naturally bounded by one series. Measured on the live worst-case profile with the exact generated SQL: 44.7s worst / ~2.6s avg before; 10ms after (together with the one-time stale-resume-point data repair applied directly to the deployment DB — see docs/superpowers/plans/2026-07-06-slow-endpoint-root-causes.md). AI-use disclosure: implemented with AI (Claude) assistance. * perf(jellycompat,userstore): aggregate series watch-state rollup in SQL The series Played/UnplayedItemCount badge on list rails (per-library Latest, library browse, search results) and series detail pages was computed by materializing EVERY episode of every series on the page (episodeRepo.ListBySeriesIDs) and then batching per-episode progress+history lookups in 500-id chunks. A 50-series page of an episode-heavy library (Sports) expanded to 32,467 episode rows and ~65 sequential queries — measured 17-18s per /Items/Latest request, and PR #292's cached Latest fast path pays it on every response for series libraries. The same fanout made /Items?searchTerm=... slow whenever the result set was mostly series (Meilisearch itself answers in milliseconds). New optional store capability userstore.SeriesEpisodeRollupStore, implemented by PostgresUserStore as one GROUP BY e.series_id aggregate with semantics identical to the chunked path (episode availability via episode_libraries, hidden-items visibility on progress rows, completed-history fold, in-progress = not watched with position > 0 — verified value-for-value against the old semantics on a real 1,586-episode series). enrichSeriesListUserData and enrichDetailUserData use it when present; SQLite-backed stores and rollup query failures keep the existing chunked path as fallback. catalog.SeasonUserDataFromCounts pins the counts-to-DTO mapping to EpisodeRollupUserData. Measured on the live worst-case profile against the real 50-series Sports Latest page: ~17s of chunked round-trips before, 119ms in one query after. Part of docs/superpowers/plans/2026-07-06-slow-endpoint-root-causes.md. AI-use disclosure: implemented with AI (Claude) assistance. * perf(catalog): bound superseded-episode completed walk to recent history The Resume / Continue Watching superseded-episode filter loaded a profile's *entire* completed history into memory on every request that contained an in-progress episode: CompletedProgressSnapshots paged user_watch_progress WHERE completed=TRUE with no upper bound. The 2026-07-06 slow-query comparison showed this surviving as a 60-116s Resume tail even after the in-progress index landed live, because the 4.3M zeroed Plex-import rows are still completed=TRUE and were re-walked every load. A completed episode can only supersede an in-progress one it was finished more recently than (the query gates on done_progress.updated_at > ip_progress.updated_at), so only completed rows newer than the oldest in-progress entry can matter. Compute that cutoff in SupersededEpisodeProgressIDs and pass it to CompletedProgressSnapshots, which — since the completed listing is ordered updated_at DESC — stops paging as soon as it crosses the cutoff. Import-heavy profiles whose back-catalogue predates their current in-progress items now stop on the first page instead of paging hundreds of thousands of irrelevant rows. Correctness is unchanged: no relevant superseding row is excluded. * perf(catalog): hard-cap superseded-episode completed walk at 5 pages The updated_at cutoff added in the previous commit bounds the completed walk on the relevance axis, but a very old in-progress entry sitting behind a large volume of newer completions could still page deep. Add a 5-page (2,500-row) hard backstop on top of the cutoff: normal profiles still stop on page one via the cutoff, and only the adversarial tail hits the cap. When it engages the tail of the completed set goes unscanned, so a superseded episode could momentarily survive on Continue Watching — we log a warning when that happens (with profile_id + rows scanned) rather than mis-filter silently, and it self-corrects once the stale in-progress entry ages out of the scanned window. * perf(playback): extract subtitle fonts in a single ffmpeg pass Embedded ASS/SSA font extraction spawned one ffmpeg process per font attachment, each re-opening the (usually CephFS-backed) media file. Anime releases carry 15-47 fonts, so the per-spawn file-open cost dominated and pushed GET /api/v1/stream/{sid}/subtitles/{track}/fonts to a 17-60 s plateau (p95 ~33 s in the live logs). Collapse the N spawns into one ffmpeg invocation that dumps every attachment to a temp dir (-dump_attachment:idx path ... -i file -map 0:t? -c copy), then read the files back. The file is opened once instead of N times, taking p95 from ~30 s to ~1-2 s with no change to output. Safety is preserved. The 32-attachment / 32 MiB caps still apply: attachment size is stat'd before read so an over-limit font never enters memory, and a watchdog polls the dump dir and kills ffmpeg if its on-disk output crosses the cap -- restoring the hard bound the old pipe-per-attachment reader enforced by killing at maxBytes+1, so a container with oversized "font" attachments can't fill the disk. Part of the slow-endpoint follow-up; see slow-query-analysis/subtitle-fonts-extraction-findings.md. * fix(review): report enforced font-byte cap; correct doc subtitle scope Address PR #350 review: - dumpFontAttachments reported the maxSubtitleFontBytes package constant in both over-limit errors instead of the maxBytes argument the caller passed, so the message misstated the enforced bound whenever a different cap was in effect (as the tests use). Interpolate maxBytes in both messages. - The root-cause plan claimed subtitle extraction was 'out of scope' while the branch actually optimizes /subtitles/{track}/fonts. Scope the out-of-scope note to subtitle *track* conversion and record the fonts single-pass work as deliverable 5. |
||
|
|
203a18ae83 |
feat(observability): OpenTelemetry logs+traces with secret redaction and slog standardization (#290)
* feat(observability): OpenTelemetry logs+traces with secret redaction Part of #265. Adds opt-in OpenTelemetry (logs + traces) alongside the existing stderr + opslog pipeline, plus secret redaction on all sinks. Default-off: with no OTEL_* / SILO_OTEL_ENABLED config, behavior is unchanged. Bootstrap (internal/telemetry): - Setup() builds one shared resource, a TracerProvider (parent-based trace-id ratio sampler), a LoggerProvider, and the W3C TraceContext+Baggage propagator from env. It installs NO MeterProvider — metrics stay on Prometheus, and the built-in no-op global MeterProvider keeps the trace instrumentation libs from double-emitting. Shutdown is deferred with a flush timeout. - Logs are bridged via otelslog fan-out (slog.MultiHandler), level-gated by the shared LevelVar and best-effort so a failing collector can't break the console or DB branches. stderr + opslog stay untouched. Secret redaction (internal/logredact): - A slog.Handler masks secret-keyed attributes (password, token, api_key, authorization, cookie, ...) — including .With-bound attrs, nested groups, secret-keyed group subtrees, and values behind a LogValuer — on the console and OTLP sinks, with a no-op fast path when a record has no secret keys. opslog.shouldRedact delegates to logredact.SecretKey so all sinks share one marker list. Rotation is infra-managed (no custom file sink): container runtime for stderr, collector/backend for OTLP, opslog partition-pruning for the DB. Documented in docs/architecture/observability.md. Verification: go build ./..., go vet, gofmt -l — clean; go test ./internal/telemetry/ ./internal/logredact/ -race pass. AI-use disclosure: implemented with AI assistance (Claude Code), including adversarial reviews that hardened the bootstrap and fixed two redaction leak paths; reviewed by the author. * refactor(observability): slog context+component sweep, sloglint gate (phase 3) Part of #265. Builds on the OTel bootstrap + redaction commit. Standardizes every log call site onto the context-carrying slog variants so records correlate with the active OpenTelemetry trace, and locks the standard in with a machine gate so future code (human- or AI-authored) can't drift back. - Call-site sweep: converted the remaining slog.<Level>(...) calls to the slog.<Level>Context(ctx, ...) form wherever a context.Context is in scope (background/init calls with no ctx are left as-is), across 183 files. Applied via a type-aware AST codemod. Log levels and message strings are preserved verbatim; a component attr (canonical per-package name) is added to direct package-level slog calls. Bound-logger calls keep their existing .With bindings. The main.go and telemetry package conversions rode with their file in the previous commit to keep each file within a single commit. - Enforcement (.golangci.yml): enable sloglint with context=scope, static-msg, key-naming-case=snake, no-mixed-args. After the sweep all four report zero violations repo-wide (tests included), so make lint / CI now blocks any regression to the non-context form. The gate ships with the sweep because it cannot be green until the legacy sites are converted. Metrics remain on Prometheus; no behavior change to /metrics or Grafana. Verification: go build ./..., go vet ./..., gofmt -l — clean; sloglint (all 4 rules) 0 violations repo-wide; log levels verified unchanged. AI-use disclosure: implemented with AI assistance (Claude Code), including the codemod; reviewed by the author. * fix(observability): honor per-signal OTLP protocol and secret WithGroup names Two Codex review findings on PR #290: - telemetry: OTEL_EXPORTER_OTLP_{TRACES,LOGS}_PROTOCOL now override the generic OTEL_EXPORTER_OTLP_PROTOCOL per signal, so mixed collector setups (e.g. HTTP logs + gRPC traces) build the right exporter. - logredact: entering a group whose name is secret-bearing (e.g. WithGroup("authorization")) now masks every leaf in that subtree, matching how slog.Group("authorization", ...) is masked as a whole. * fix(observability): address review feedback on telemetry bootstrap - Telemetry setup failure no longer kills boot: Setup returns usable no-op providers alongside the error and main logs and continues with telemetry disabled, honoring the best-effort contract. - Honor OTEL_TRACES_SAMPLER (always_on/off, traceidratio, parentbased_* variants); unsupported values fall back to parentbased_traceidratio. - Attach node identity as semconv service.instance.id instead of the non-semconv node.name. - Rename opslog retention-scope log attrs to target_component/target_level so they no longer collide with the canonical component routing key, and tag those lines with component=opslog. - Fix stale levelGated comment casing; use WarnContext in the telemetry shutdown defer; document the LogValuer double-resolve on the redaction slow path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
76b10b822e |
feat(jellycompat): surface audio profile and spatial format on media streams (#344)
* feat(scanner): capture audio stream profile metadata Store ffprobe audio stream profiles in the existing audio track metadata JSON. This mirrors the video track profile field and is additive for clients that want to display or inspect codec profile details. * feat(catalogseed): carry audio profile in seed export records Keep AudioTrackRecord in parity with VideoTrackRecord, which already mirrors the video profile field, so seed exports do not drop the new audio profile metadata. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(jellycompat): surface audio profile and spatial format on media streams Set MediaStream.Profile from the captured ffprobe audio profile and derive AudioSpatialFormat (DolbyAtmos / DTSX) from it, matching Jellyfin's case-insensitive substring semantics. Audio display titles now prefer the profile over the codec name (except the AAC "LC" profile), matching Jellyfin, so DTS-HD MA and Atmos tracks read correctly in clients like Infuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Silo Contributor <silo-contributor@example.invalid> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7f4f609948 |
feat(scanner): capture audio stream profile metadata (#337)
* feat(scanner): capture audio stream profile metadata Store ffprobe audio stream profiles in the existing audio track metadata JSON. This mirrors the video track profile field and is additive for clients that want to display or inspect codec profile details. * feat(catalogseed): carry audio profile in seed export records Keep AudioTrackRecord in parity with VideoTrackRecord, which already mirrors the video profile field, so seed exports do not drop the new audio profile metadata. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Silo Contributor <silo-contributor@example.invalid> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3df5ed4e09 |
feat(web): show audio stream profile in Media Info (#339)
Expose the optional audio stream profile field in the web API type and include it in the Media Info audio section when present. This mirrors the existing video profile row and is additive for servers that do not emit audio profiles yet. Co-authored-by: Silo Contributor <silo-contributor@example.invalid> |
||
|
|
0deff23985 |
fix(profiles): enforce per-account profile name uniqueness (#342)
* fix(profiles): enforce per-account profile name uniqueness Profile create and rename accepted any name, so one account could hold unlimited profiles all called "Laura" (every client allowed it too). Reject a create or rename whose trimmed, case-insensitive name matches another profile on the same account with 409 name_conflict. Scoping is per account by construction — the check runs against a single user's profile store, so different accounts can still each have a "Laura". Renames exclude the profile being updated, so re-saving a profile under its own name (e.g. avatar-only edits that resubmit the name) still works. Also reject whitespace-only names on create and rename; a name of " " previously passed the blank check. Additive-only per the v1 API rules: new 409 error code on existing endpoints, following the profile_limit_reached pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(profiles): store the trimmed profile name The conflict check compared trimmed names but create/rename persisted the raw input, so " Laura " could land with stray whitespace and render inconsistently. Normalize to the trimmed form before storage on both paths. Addresses the CodeRabbit review finding on PR #342. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(profiles): cover whitespace-only rejection and rename trimming Also document the check-then-write race in profileNameConflicts: the userstore backends carry no unique index on name, so concurrent creates can still race past the guard, same as profile_limit_reached. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
021e54a03c |
fix(scanner): fix slow-scan regressions from #319 and #322 (#341)
* fix(scanner): stop classifying "other" content folders as extras Regression from #322 (trailers and extras for movies and series), which introduced the extrasDirKinds map. The extras directory classifier mapped the generic labels "other" and "others" to ExtraKindOther. These are not part of the Jellyfin/Plex extras folder convention the map claims to mirror, and they collide with real content-scope folder names. A library organized as "movies/other/<Title (year) {ids}>/<file>" tripped the depth-2 ancestor lookup in classifyExtraPath: every title two levels under the scope folder "other" was classified as an "other"-kind extra. Such files are partitioned out of primary root/group inference and matching, then deferred in processExtraFiles because their parent cannot resolve (they are the primary titles, not children of one). The result on one deployment was ~10k movies under a folder named "other" funneled through the slow extras path every scan (parent-unresolved deferrals at ~9.5/s), stalling the scan and freezing that scope for new/changed primary content. Remove "other"/"others" from extrasDirKinds. The ExtraKindOther kind stays reachable through genuine convention labels (extra/extras/interviews/ scenes/shorts). Add regression coverage asserting titles under a scope folder named other/others stay primary. * perf(scanner): rewrite identity-only changes without re-probing A pure identity/grouping change on an already-probed file — a root_assignment_changed or group_assignment_changed reason with nothing else — used to fall into the full update branch, which unconditionally ran ffprobe (probeFile) and then upserted every column, including probe columns, from the freshly built row. When a group-key or root scheme changes across the library (see #319), this reprobed nearly every file on the next scan: an incremental scan that normally takes ~1h ran 7h+ as a full-library ffprobe storm, even though the media bytes were untouched. Add a metadata-only update path in processFile: when identityOnlyUpdateReasons reports every reason is a root/group reassignment, rewrite just the derived identity columns via the new FileRepository.UpdateIdentity and skip ffprobe, OSHash, and marker fetch entirely. UpdateIdentity issues a targeted UPDATE of the root/group/identity and edition/presentation columns only, mirroring Upsert's column handling, and leaves probe data, file bytes/mtime/hash, subtitles, chapters, markers, and content/episode/extra linkage intact. The stored group key converges to the recomputed value on the next scan, so the file takes the unchanged fast-path thereafter — without a probe storm. The shared identity-column population is extracted into populateScanIdentity so the full path and the metadata-only path stay in lockstep. Verification: unit test for the identityOnlyUpdateReasons classifier; a DB-backed test (skipped without SILO_TEST_DATABASE_URL) asserting UpdateIdentity rewrites grouping while preserving probe/linkage columns; the UPDATE statement was also exercised against the live schema inside a rolled-back transaction. * fix(scanner): harden identity fast path and extras scope classification Review follow-ups for the two scan-regression fixes on this branch, addressing both Codex review comments on PR #341 plus adversarial-review findings. Identity fast path (processFile/UpdateIdentity): - Gate the metadata-only path on existing.ExtraID == "": a row still linked as an extra reaching processFile is being reclassified as primary, and only the full upsert clears extra linkage; UpdateIdentity would have frozen it out of matching forever (match backlog filters extra_id IS NULL). - Gate on existing.FileHash != "": the full path backfills the OSHash and fetches hash-keyed S3 intro/credits markers, which no later scan reason would repair; hash-less legacy rows now take the full path once instead of silently losing that repair channel. file_hash is added to the scan-state row shape to support the gate. - Clear match_suppressed_at like every other scan write, so files with fresh identity re-enter the match backlog (suppression is documented as lasting "until retried or seen by a new scan"). - Write media_folder_id, mirroring Upsert's ON CONFLICT reassignment. - Return ErrFileNotFound when the row vanished mid-scan (concurrent delete) and fall through to the full upsert path instead of surfacing a per-file scan error. - Return only the row id instead of RETURNING all ~75 columns: the fast path fires once per file during library-wide grouping migrations, and dragging the track/chapter JSONB payloads along for a million rows dominated the cost of the path built to be cheap. - Extract identityColumnDefaults shared by Upsert and UpdateIdentity so the defaulting rules cannot drift, and drop the no-op editionConfidence indirection copied between them. - Use populateScanIdentity in the new-file insert path too; it still carried a verbatim copy of the extracted block (with a provably dead existingByPath lookup). Extras classification: - Restore "other" to extrasDirKinds: it is part of both the documented Jellyfin and Plex extras-folder conventions (the removed-label fix overshot and broke "movies/<Title>/Other/<file>" libraries, ingesting their extras as bogus primary titles). "others" stays removed - it is in neither convention. - Replace label removal with the structural guard the PR had deferred: classifyExtraPath now rejects a supplemental-named directory sitting at library-scope depth (the dir, any supplemental ancestor, or the first non-supplemental ancestor is a configured library root). This fixes the original "movies/other/<Title>" defer-storm generically, covering every convention label (shorts, scenes, extras, ...) used as a content-scope folder. - Scope extras parent binding by folder.Paths instead of the walk roots, so a subtree scan targeting a single movie folder still binds that movie's own extras instead of deferring them. Tests: eligibility-gate unit tests, scope-guard classifier cases (convention Other/ inside a title binds; scope-level other/shorts stay primary), and the DB-backed UpdateIdentity test now also covers folder moves, suppression clearing, and ErrFileNotFound. Full scanner suite ran green against a migrated scratch PostgreSQL 17 container. * refactor(scanner): simplify extras scope guard to title-folder rule Replace the ancestor-walking supplementalDirAtScopeDepth loop with the plain rule it was approximating: a convention-named directory counts as an extras dir only when it sits inside a title folder — it must not be a configured library root or directly under one. Same outcome for the layouts that matter (movies/other/<Title> stays primary, <Title>/Other classifies), less machinery. * test(scanner): assert all rewritten identity columns in UpdateIdentity test * fix(scanner): make extras scope classification structure-aware The title-folder rule from 53632022 anchored on library roots, so it missed both directions: chained convention dirs at the root ("movies/extras/behind the scenes/clip.mkv") classified as extras with an unresolvable parent (deferred forever), and category folders nested below the root ("movies/4K/other/<Title>/") still misclassified their titles. Replace the root-distance heuristic with the structural property that actually distinguishes the two cases: a convention-named directory only counts as an extras dir when its owner (first non-supplemental ancestor) is a title folder — a directory that holds media of its own. The new extrasClassifier derives that from the scan's walked path list (no extra I/O): movie folders must hold a file directly beside the extras dir; series folders may hold episodes one level down in season folders (media hiding inside a folder's own extras dirs doesn't count). Library roots never qualify. Watch-event scans, which have no walked list, probe ownership with bounded os.ReadDir instead. This handles title folders at any depth below the root and keeps scope/category folders primary at any depth, with two known edges: a title folder holding only extras (its media file missing) stays primary until the file appears, and a mixed dir holding both loose media and a category folder degrades to deferral, never wrong linkage. resolveExtraParent's inline supplemental-chain walk is extracted into the shared firstNonSupplementalAncestor. |
||
|
|
0694787504 |
fix(overlays): show_status persistence + card overlay layout fixes (#335)
* fix(metadata): persist series show_status from provider metadata
Plugin-reported series status (proto status field 31) was mapped into
MetadataResult.ShowStatus but dropped by both metadataResultToItem and
itemToMetadataResult, so media_items.show_status stayed empty for every
movie and series - only the manga enrichment path ever wrote it. This
left the Show Status card overlay permanently blank for series.
- carry ShowStatus through both converters; series values normalize to
a canonical lowercase domain (returning/ended/cancelled/in_production/
upcoming) so TMDB "Returning Series"/"Canceled" and TVDB
"Continuing"/"Upcoming" converge on one spelling
- pass non-series values through verbatim so the manga status domain
("Ongoing", ...) can never be mangled by a generic refresh round-trip
- round-tripping the existing item's status also stops refreshes from
wiping a previously persisted value via show_status = EXCLUDED.show_status
- extend the web overlay formatter with continuing/upcoming/planned
TMDB/TVDB plugins need follow-up changes to actually emit the status
field; prepped separately in their repos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): overlay badge layout, ordering, and wordmark rendering
Fixes from a full card-overlay audit (every issue verified by DOM
geometry measurement before/after):
- render each card edge as one flex row holding both corner stacks so
opposing badges share the width (min-w-0 + truncate) instead of
overlapping on narrow cards; long labels ellipsize instead of
wrapping over the opposite corner
- honor prefs.order via orderedOverlaysForPosition — the renderer
previously ignored the stored order entirely
- cap corners at 3 badges so maxed-out configs can't collide with the
opposite vertical corner
- lift bottom-right badges above the card menu button, which is always
visible on touch devices and occluded them
- suppress the text label when a wordmark icon (HDR10/ATMOS/AV1/HDR)
already spells it — pill/vibrant presets rendered "HDR10 HDR10" —
and widen the wordmark viewBoxes, which clipped their own text;
drop the never-used iconOnly flag the wordmark rule supersedes
- standalone resolution badge now uses prettyResolution ("4K", not
"2160P"), matching the combined badge
- manga cards skip generic overlays (their status/count chips own both
top corners) and the two chips now share a row and truncate instead
of overlapping each other
- useOverlayPrefs returns null while loading so cards no longer flash
default badges before the user's config or admin kill switch arrives
- settings rows for Resolution/HDR now say why they're hidden while
the combined badge is enabled
- add a CardOverlays test suite covering every registered overlay,
ordering, suppression, wordmarks, the corner cap, and menu clearance
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4e03f4b807 |
fix(playback): last-write-wins progress and DV P7 RPU strip on remux (#334)
* fix(playback): last-write-wins progress and DV P7 RPU strip on remux Progress: UpdateProgress (the live playback-session path) clamped position_seconds to GREATEST(new, old), so a deliberate backward seek could never persist — "rewind and stop" resumed at the stale later position on every client. Position is now last-write-wins, matching the /sync/progress path that was always unconditional. The completed latch and rewatch re-entry semantics are unchanged. Remux: profile 7 Dolby Vision remuxes drop the enhancement-layer track (-map 0:v:0 keeps only the base layer) but previously left the dangling dual-layer RPUs on the BL — broken metadata that a DV-honoring display can mis-render. Remuxes of P7 files now strip DV RPUs via the dovi_rpu bitstream filter, yielding a clean HDR10 stream (the same fallback presentation the Apple client's P7 HDR10 toggle produces). Profile 8 RPUs are kept: the BL is self-contained and DV clients render it. Adds MediaFile.PrimaryDVProfile() and threads the profile through ServeRemux callers; the proxy path (no track metadata in claims) keeps prior behavior. True P7->8.1 DV conversion needs dovi_tool alongside FFmpeg (the dovi_rpu bsf only strips/recompresses); the remux plumbing now carries the DV profile so that can slot in later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(userdb): apply last-write-wins progress to the SQLite backend too Review follow-up (P2): the LWW change only covered pgstore; the SQLite userdb UpdateProgress kept the MAX clamp, so rewind-and-stop still resumed at the stale later position for sqlite-backed installs. The conflict clause now matches Postgres (position last-write-wins, completed latch and rewatch re-entry unchanged), with a backward-seek regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): harden LWW progress and DV RPU strip from review - Probe ffmpeg for the dovi_rpu bitstream filter once per process and fall back to a no-strip remux (the pre-existing behavior) when it is missing: on pre-7.1 ffmpeg the unknown filter aborted the process, turning every Dolby Vision profile 7 remux into a hard playback failure. - Skip zero-position heartbeats in persistProgress, mirroring the stop path and the jellycompat report path. Under last-write-wins an early zero heartbeat (e.g. before the client seeks to its resume point) would wipe the stored resume position; GREATEST previously masked this. - Carry the DV profile in stream token claims (dvp, omitempty) so standalone proxy nodes strip profile 7 RPUs the same way integrated mode does. Old tokens decode as 0 and keep prior behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
11704a1701 |
feat(sections): fix broken home section templates and add six new ones (#332)
* feat(sections): fix broken home section templates and add six new ones Fixes templates that silently produced nothing: - award_winners: hide from gallery (resolver is a stub until award data exists); saved sections keep resolving - seasonal_themed: christmas/st_patricks/thanksgiving get an interim title-keyword resolver, and multi-theme selection skips themes without an executable query so a data-less theme can no longer black out the section during its own window (previously killed the section all of December) - taste_match: empty genre now auto-picks the profile's strongest taste cluster (fallback: server top genre); the default preset was permanently empty - because_you_watched: honor the recipe's anchor_item_id key (fetcher only read legacy source_item_id, so pinning an anchor did nothing) - editorial_spotlight: reject subject_type=franchise (validated but could never resolve); fix drawer misrepresenting pinned presets as auto-rotate - admin_curated_list: add a catalog-search item picker so Editor's Picks is actually addable; block saving an empty list; hide admin_only recipes from profile-facing galleries - discovery fetchers (hidden_gems, forgotten_favorites, critically_acclaimed): honor single/multi library scope, intersected with viewer access; implement hidden_gems max_play_count New templates: returning_shows (new season of shows you've watched), genre_roulette (rotating top-genre spotlight with title override), anniversaries (milestone release anniversaries this month), short_watches (well-rated movies under a runtime cap), family_movie_night seasonal theme (Fri/Sat evenings), and a "New in 4K" format_showcase preset via a new sort=recent param. Adds a blanket test asserting every visible gallery preset's defaults pass its own recipe validation — the gap that let taste_match and Editor's Picks ship broken. New SQL shapes validated with EXPLAIN against the dev database. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sections): address PR #332 review findings Codex review: - returning_shows: the new-season file check now applies the effective library scope (section scope ∩ viewer-allowed, minus disabled) to media_files.media_folder_id, so an episode file that only exists in an out-of-scope folder can no longer surface the series - buildLibraryScope: replaced the media_item_libraries row join with EXISTS / NOT EXISTS semi-joins. An item in several in-scope libraries now yields exactly one row in the non-GROUP BY rails (short_watches, anniversaries, seasonal keyword, format_showcase, new_to_library, ...), and the disabled-library check is item-level, closing the join-row leak where membership in an allowed library masked membership in a disabled one. Deny-only mode keeps the positive-membership guard, mirroring catalog's appendDiscoveryLibraryScope. CodeRabbit review: - recommendations reader: a taste cluster whose cached items are entirely filtered out now falls through to the next cluster / global fallback instead of returning an empty row - genre_roulette: multi-library scopes get distinct rotation seeds - returning_shows: reject negative lookback_days at validation - shared oneOf() enum validator replaces per-recipe switch duplication - SeasonalTitleOverride usable-filter contract covered by a direct test - web NumberParamField: integer-only guard + step=1 (backend fields are Go ints; fractional values failed unmarshalling at save) - curated list picker: search failures show an error instead of a misleading "No matches."; pre-existing item_ids hydrate display titles via the watch-detail endpoint instead of rendering raw ids Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
43d9056b01 |
fix(collections): repair broken builtin collection templates (#331)
* fix(collections): repair broken builtin collection templates A live audit of the builtin template catalog (all 40 MDBList URLs and all 10 TMDB franchise IDs fetched) found two dead sources, a silent bundle-apply collision, and several templates whose defaults contradict their descriptions: - Repoint mdblist_misc_a24 and mdblist_misc_criterion_collection to live lists; the original irvingbeano/shtluck lists were deleted on MDBList (404), so every sync of those collections failed. - Retitle mdblist_charts_popular_movies to "IMDb MovieMeter Top 100". It shared the "popular-movies" title slug with tmdb_popular_movies, and bundle apply dedupes by slug per library, so applying all_defaults silently skipped it. Poster regenerated from the raw plate with the new title; new handler test asserts builtin title slugs stay unique. - Raise the shared default limit 50 -> 100, give the IMDb Top 250 templates an explicit 250 (limit*4 fetch trim previously never scanned entries 201-250), and drop the limit on catalog lists (Criterion, A24) so they hold every owned title. - Correct IFC Films to MediaMovie (live list is 100% movies; as MediaMixed it was offered to TV libraries where it always synced empty) and fix the Trakt Popular descriptions (ratings-based, not "most-watched"). - Update stale limit docs in collection-templates.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(collections): raise import limit caps above IMDb Top 250 default The IMDb Top 250 templates now default to 250 items, but the template config forms rendered their Max Items input with max=200 and the user import API rejected limits above 200, so applying those templates from the direct galleries failed native validation or got a 400. Raise the cap to 500 on both sides, wired to shared constants: sync's fetch trim (collectionSourceFetchMax) never scans more than 500 source entries, so a larger explicit limit could never be satisfied anyway. collectionutil.MaxExplicitItemLimit backs validateOptionalLimit, and COLLECTION_MAX_ITEMS in lib/collectionTemplates backs all seven Max Items inputs (gallery forms + admin import/editor dialogs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a26b2de454 |
fix(metadata): stop specialist providers hijacking new library chains (#294)
* fix(metadata): seed specialist providers off and scope chains to declared levels New library provider chains were seeded from every enabled metadata provider, ordered purely by each plugin's declared default_priority and enabled whenever that priority was > 0. Two consequences: - A specialist provider (e.g. silo.sportarr, which declares series/season/ episode) could out-rank the general providers and land at position 1, enabled, on every new TV series library. - Single-purpose providers that declare only their own level (audiobook / ebook / manga metadata) were still attached as disabled rows to series and movie libraries, cluttering the chain editor with providers that cannot serve that content. Introduce a `default_enabled` capability-metadata flag (defaults to true, so every existing plugin is unaffected). A provider sets it false to be seeded installed-but-disabled while keeping its declared priority, so a user can opt in per-library and it slots in where the manifest intends instead of jumping to the top. At the same time, seedDefaultChain and AppendProviderToAllChains now drop providers that do not declare a content level, reusing the same providerSupportsLevel rule as the chain-less fallback (issue #106). LookupSeedPlacement resolves support/priority/enabled with a single metadata fetch. buildSeededChainEntries is extracted as a pure, unit-tested helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): standardize metadata provider slug casing in library chain editor The library provider-chain editor showed the same provider differently depending on where the chain came from: a freshly defaulted chain used the capability display name ("TMDB"), while a chain loaded from the server used the capability id ("tmdb", which the API returns as provider_slug). So a provider read one way before saving and another after, and differed between library types depending on which levels already had a saved chain. Standardize on the capability id everywhere (matches the server's provider_slug and the mono/slug styling). Extract the provider mapping into a pure, unit-tested metadataProvidersFromInstallations helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): mirror server seeding rules in the library form's default chain The form builds its own default chain client-side, and any touch (including changing the library type on create, the normal path for a series library) marks it dirty and POSTs it after create — replacing the server-seeded chain. That chain still enabled every provider with a declared priority and listed unsupported providers as disabled rows, so the server-side fix evaporated on the UI create path. buildDefaultLevelChains now applies the same rules as buildSeededChainEntries: providers that don't declare the level are dropped, a declaring provider is enabled only if it doesn't opt out via default_enabled, and a legacy catch-all (no declared levels) is parked last, disabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web,api): serve default provider chains from the server Replace the form's client-side reimplementation of the seeding rules with a new additive endpoint, GET /api/v1/libraries/provider-defaults?library_type=X, which returns the exact chain seedDefaultChain would write for that type. The create form now renders those server-computed defaults, changing the library type just refetches them (no longer marking the chain dirty), and a create with an untouched chain lets the server-seeded chain stand instead of writing one back. Editing an existing library uses the same defaults to fill levels its saved chain doesn't cover. Types the server seeds no metadata levels for (e.g. podcasts) return an empty levels map rather than an error. This removes buildDefaultLevelChains / metadataProvidersFromInstallations and the default_priority/default_enabled manifest parsing from the frontend — one source of truth for default ordering and enablement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): show a loading state in the provider chain editor While the server chain (for an existing library) or the type's defaults are still in flight, the editor rendered empty provider lists for a moment. Show a spinner row instead; local edits always render immediately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
4a76016015 |
fix(jellycompat): tolerate item-id mediaSourceId on direct-play streams (#313)
Native clients that call PlaybackInfo, reuse the returned PlaySessionId on the
/Videos/{id}/stream?static=true request, but send the *item* id as mediaSourceId
(per the Jellyfin MediaSource.Id == Item.Id convention) rather than Silo's
fileID-based source id got a 400 "Media source is required" and could not
direct-play at all.
The PlaySessionId branch of resolvePlaybackRoute returned findMediaSource's nil
result directly, with no fallback -- unlike the FindByRoute and
createStaticPlaySession paths. Because resolution returned a nil error,
HandleVideoStream's Static fallback never fired either, so every direct-play
request 400'd.
Fall back to the session's primary source only when mediaSourceId is empty or
equals the route item id (the MediaSource.Id == Item.Id convention), matching
Jellyfin's StreamingHelpers. Any other unmatched id -- a stale/foreign id, or a
wrong version on a multi-version item -- stays rejected so it can't silently
stream the wrong file. Exact source-id matches are unchanged.
|
||
|
|
af0e3e75a9 |
feat(web): Media Info dialog + consolidated media-spec formatters (#323)
* refactor(web): consolidate media-spec formatters into lib/mediaFormat
Media-spec formatting was duplicated with divergent behavior across six
modules: versionFormatUtils (GB/"" fallback), playback-info (GiB/"—"
fallback, CODEC_LABELS), overlay tech.ts (prettyResolution,
compactHdrSuffix), versionRankingUtils (mapAudioLabel), plus private
formatBitrate copies in ItemCard and useTranscodeQuality.
One canonical module now owns these helpers; the deliberate per-surface
display differences (fallback text, GB vs GiB labels) are parameters
instead of copies. All call sites migrated, including two cross-boundary
imports (adminActivityPresentation reaching into @/player/playback-info,
player components reaching into page-level utils). The quality-menu Mbps
label in useTranscodeQuality stays local as a distinct display policy,
renamed formatQualityBitrate. Also removes the unused TrackSection/
TrackRow components and renames versionFormatUtils.tsx to .ts.
Behavior-preserving: playback-info.test.ts and VersionFlyout.test.ts are
unmodified and assert exact rendered strings ("7.1 GiB", "640 kbps",
"Dolby Vision Profile 8.1 (HDR10)").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): add Media Info dialog with detailed per-file technical specs
Adds a curator/admin-gated "Media Info" dialog to movie and episode
detail pages showing full technical specs per file: per-stream bitrates,
Dolby Vision profile with base-layer compatibility and EL flags, HDR10+,
chroma subsampling derived from pixel format, color primaries/transfer/
space, bit depth, codec profile/level (H.264, HEVC, and AV1 level
decoding), audio layouts/sample rates, and subtitle stream details.
Frontend-only: the item-detail response already serialized all of this;
VersionVideoTrack in api/types.ts was just missing the DV/HDR10+/
video_range_type fields the backend sends. Entry points: a "Media Info"
item in the detail-page overflow menu (opens on the selected version)
and a per-row info button in Media locations (opens on that file). All
versions render as accordion sections with the target expanded.
Spec rows are built by pure section builders in mediaSpecSections.ts
with fixture tests covering SDR/HDR10/DV 7/DV 8.1/HDR10+/multi-track/
external-subtitle cases; the dialog is a thin renderer over them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): recover DV compatibility detail from video_range_type
Review follow-ups on the Media Info dialog's dynamic-range labels:
- When ffprobe omits dv_bl_signal_compatibility_id, the scanner still
derives video_range_type (DOVIWithHDR10/HLG/SDR) from color_transfer.
formatDolbyVisionLabel now falls back to that enum for the
compatibility detail instead of showing a bare "Dolby Vision
Profile 8"; explicit DV side-data fields still win when present.
- formatVideoRangeLabel checks the video_range_type mapping before the
standalone HDR10+ shortcut, so DOVIWithHDR10Plus tracks without a raw
dolby_vision string keep their Dolby Vision hint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e140bd9424 |
feat(metadata,scanner): trailers and extras for movies and series (#322)
* feat(metadata,scanner): trailers and extras for movies and series Remote provider videos (TMDB trailers/teasers/featurettes/...) are fetched through the unified match/refresh pipeline into the new item_videos table, filtered per-library via media_folders.trailer_kinds, merged across providers with site/provider dedup, and lockable via FieldVideos. The movie scanner stops discarding supplemental directories (Trailers/, Featurettes/, Behind The Scenes/, ...) and classifies them — plus Jellyfin-style filename suffixes (-trailer, -behindthescenes, ...) and series-root supplemental dirs — into the new media_extras entity backed by ordinary media_files rows (extra_id ownership, content_id/episode_id NULL so existing version/matching queries stay structurally blind to extras). Series Extras/SxxExx season-0 mapping is unchanged. Extras are playable watch targets via a GetWatchDetail fallback tier (episodes precedent), with contentid.ForLocal minting stable ids. API: ItemDetail gains additive videos/extras arrays (single + batch parity); library settings expose trailer_kinds. jellycompat now populates RemoteTrailers, LocalTrailerCount/SpecialFeatureCount, and serves real /LocalTrailers + /SpecialFeatures items playable through PlaybackInfo. Requires silo-plugin-sdk v0.9.0 (VideoRecord) before go.mod can bump; builds locally via go.work against the SDK feat/metadata-videos branch. Part of trailers/extras capability work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): trailers and extras sections, library trailer-kinds setting TrailersSection (YouTube thumbnails + youtube-nocookie modal) and ExtrasSection (plays extras through the standard watch controller) on movie and series detail pages; admin library form gains a trailer-kinds allow-list synced with the server default (all provider kinds), now also honored on library create. Part of trailers/extras capability work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scanner): scan extra_id in scanMediaFiles; review cleanups scanMediaFiles (the plural row scanner behind GetByContentID/GetByFolder/ GetByExtraID and 20+ other queries) was missing the scan destination for the new extra_id column, which would have failed every media-file read at runtime with a column/destination count mismatch. Also: extend the batch equivalence test to seed item_videos/media_extras so the new videos/extras prefetch wiring is actually proven; drop the one-off pgxRows interface for the repo-wide pgx.Rows convention; reuse formatClock instead of a third duration formatter in ExtrasSection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(deps): bump silo-plugin-sdk to v0.9.0 for VideoRecord Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(matching): exclude extras files from match queues and bulk content linking Dev verification caught extras media_files rows (content_id NULL by design) being swept into the movie/series match queues and the root-claim bulk relink: a '-featurette' suffix extra was matched onto its parent as a version, and a Trailers/ file minted a spurious local skeleton item that shadowed the extra's watch target. Add 'extra_id IS NULL' to the queue eligibility conditions, root/group claim relinks, observed-root content assignment, and the admin unmatched-files listing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): authorize local extras files through their parent item Dev verification: playback/start (and the shared MediaFileAuthorizer used by markers/subtitles/ebook reader) resolved file ownership only via episode_id/content_id, so extras files (extra_id only) 404ed. Add an ExtraLookup tier that resolves media_extras and gates on the parent item's access, mirroring the episode->series pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(catalog): resolve local extras through GetItemDetail for compat playback jellycompat PlaybackInfo (and any per-item consumer resolving arbitrary content ids) goes through GetItemDetail, which lacked the extras tier that GetWatchDetail has — so Jellyfin clients got zero MediaSources for extras. Add buildExtraItemDetail (minimal detail + ordinary playback surface, parent-gated access) as the fourth resolution tier, and map the extra type to Jellyfin's Video kind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): allow youtube-nocookie embeds in CSP; trailer modal a11y The frontend CSP's frame-src blocked the trailer modal's youtube-nocookie.com iframe (found on dev verification). Also add the missing sr-only DialogDescription and drop the redundant allowFullScreen attribute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address PR review findings for trailers/extras - Extras watch/item detail no longer stamp SeriesID/SeriesTitle for movie-owned extras (players key episodic post-roll flows off series_id); series-owned extras keep them (Codex). - processExtraFiles resolves the parent and upserts media_extras before the unchanged fast-path, and the fast-path now also compares mtime, so rematched parents / reclassified kinds / same-size replacements converge (Codex + CodeRabbit). - media_files upsert clears content/episode linkage atomically when extra_id is set (ownership mutual exclusion in one statement); the now-redundant MarkFileAsExtra helper is removed (CodeRabbit). - ScanFile's extras branch runs syncPresentLibraryState + reconcileLibraryMemberships so converting a primary file to an extra cleans stale library membership immediately (CodeRabbit). - media_extras migration adds the media_files FK as NOT VALID + VALIDATE to avoid a full-scan exclusive lock on large tables (CodeRabbit). - trailer_kinds input is trimmed/lowercased/deduped and unknown values are dropped instead of silently widening the allow-list to 'other' (CodeRabbit). - Extras authorization branches match the episode branch's posture: unconfigured lookup is a config error, nil extra is a 404 (CodeRabbit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ea518d2316 |
fix(autoscan): advance marker past paths outside any library folder (#314)
* fix(autoscan): advance marker past paths outside any library folder A filesystem-watcher scan source (e.g. the CephFS plugin) observes the entire volume, so it legitimately returns changes for paths that live in folders which are not registered as Silo libraries (unregistered decade buckets, 4k-dv, anime-dub, download/recycle dirs, etc.). Previously, when a poll returned paths but none resolved to a library folder, PollOnce recorded an "unresolved" error and refused to advance the marker, on the assumption this only happens for a freshly-enabled source with unconfigured path rewrites. For a whole-volume watcher that assumption is wrong: a single change under an unregistered folder pins the journal marker in place, so every subsequent poll re-reads the same window and autoscan stalls permanently. Real imports pile up behind the stuck marker and are never scanned until the next full library scan. Treat "paths returned but none resolved" as a benign, expected condition: log a warning (so an operator can still investigate a genuinely misconfigured source) and advance the marker so the queue keeps moving. Partial resolution was already handled correctly — the unresolved subset is simply outside Silo's libraries. Observed in production: one file added under movies/4k-dv (not a library path) froze the CephFS source for ~18 days; ~42k journal changes had accumulated behind it. With this change the marker advances and the backlog drains on the next poll. * fix(autoscan): hold marker on transient resolve failures, keep unresolved events visible Review follow-up: advancing past a window where nothing resolved is only safe when every path is genuinely outside Silo's libraries (RequestError). When resolve attempts fail internally (resolver/database fault), the same resolvedAny=false state now holds the marker and records the error so the window is retried once the fault clears, instead of silently skipping imports until the next full library scan. The benign outside-libraries case still advances, but finishes its poll event as "unresolved" (with an explaining message) instead of "success", so the condition stays visible in poll history and the existing unresolved status filter in the API/admin UI keeps working. Stale doc comments on PollOnce and AdvanceMarker updated to match the new behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(autoscan): hold marker when any resolve attempt fails transiently, not only when all do The transient-failure hold was gated inside the nothing-resolved branch, so a mixed window (some paths resolved+enqueued, others failed on an internal resolver fault) still advanced the marker past the failed paths. Hoist the gate: any internal resolve failure now records the error and holds the marker regardless of how many paths resolved. The resolved targets were already enqueued; re-reading the window at worst re-scans them, which is safe. Adds a mixed-window regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fe06312b06 |
fix(settings): register player.dolby_vision_enabled and player.seek_cache_enabled
Keys absent from settingsRegistry resolve through the user-scope path and return an empty effective_value, which Apple clients interpreted as false — flipping these default-ON toggles off on first sync. Register both device-scoped keys with default "true" so defaults resolve correctly and device overrides round-trip. seek_cache_enabled had been syncing unregistered since it shipped; dolby_vision_enabled is new (Apple client Dolby Vision toggle, silo-apple e9bd775). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c29212b2c5 |
docs: handoff for account.capabilities_changed events
Design doc for a user-scoped capability-invalidation event on the existing events WebSocket, so clients refresh cached capability payloads (e.g. /downloads/capability) when admin permission or server settings change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8e9e2c91d4 |
feat(web): auto-run split dry-run preview instead of a manual Preview button
The Split Versions dialog now debounces a dry run whenever the plan is valid, shows the preview automatically (with a stale-response token guard), and tolerates null override arrays from older servers. Split still requires a rendered preview before enabling. Part of #319 follow-up. AI-use: implemented with Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
80aff39921 |
fix(matching): return empty override arrays from split so dry-run preview JSON has [] not null
A whole-folder split only populated root overrides, leaving file_overrides marshaled as null, which crashed the Split Versions preview render in the web UI and kept the Split button disabled. Part of #319 follow-up. AI-use: implemented with Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
885354e573 |
fix(scanner): repair invalid CTE that made UpdateEpisodeLink fail on every call (#321)
The statement ended with SELECT COUNT(*) FROM inserted, but the inserted CTE has no RETURNING clause — PostgreSQL rejects referencing such a CTE (SQLSTATE 0A000), so every UpdateEpisodeLink call has errored since #283. The sole caller logs a warning and continues, so the metadata-side single-file episode relink silently did nothing: episode_id stayed stale, episode_libraries rows were not inserted, latest_episode_added_at was not bumped. Bulk linking uses the correct pattern and masked the breakage. The INSERT is now the top-level statement; the count was unused. Verified against a migrated PostgreSQL scratch database: TestEpisodeLinkMaintainsLatestEpisodeAdded fails on main with the exact error and passes with this change. Fixes #320 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0fb5afe479 |
feat(matching): split wrongly merged versions with watch-state reattribution; anchor group keys on provider tags (#319)
* feat(matching): split wrongly merged versions, reattribute watch state, anchor group keys on provider tags
Wrong merges (two titles normalizing to the same title+year key) stacked
different films as fake "versions" of one item with no in-app repair, and
explicit {tmdb-…}/[imdb-…] folder tags could not prevent it because the
content-group key ignored provider IDs entirely. Merges also silently
orphaned all per-user watch state.
- Anchor group keys on structured provider tags: same tag always groups,
different tags can never merge; untagged files keep title+year keys.
- media_identity_overrides: path-scoped (root/file) forced identities applied
during group inference, so admin splits survive rescans.
- internal/catalog/reattribute: shared user-state mover — exact moves for
file-linked rows, evidence-based user_watch_history classification via the
playback session log, newest-wins progress conflicts; wired into
rebindItemToExistingItem to stop merge orphaning (with S/E episode mapping).
- POST /admin/items/{id}/split (dry-run = full transaction + rollback, so
previews are exact), POST /admin/items/{id}/merge, GET /admin/items/{id}/files.
- Web admin: Split Versions dialog (files by folder → candidate search →
preview → split), Resolve link from ambiguous-roots diagnostics.
Part of #318
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(reattribute): classify history before moving session log; cover managed downloads and series-scoped preferences
Review findings on #319, all reproduced against a migrated scratch database:
- moveFileSubset re-pointed playback_history_admin before the history
evidence query ran, erasing exactly the evidence proving a profile's plays
were all on moved files — their history stayed behind as ambiguous.
History classification now runs first; the pre-fix code demonstrably fails
TestRun_HistoryEvidenceClassification.
- Managed offline downloads (downloads.content_id/episode_id) were not
remapped on split or merge, stranding rows on the old id. Now moved per
file on splits and swept per id pair on merges/episode re-anchoring.
- Series merges left user_audio_preferences, user_subtitle_preferences,
user_series_playback_preferences (series_id-keyed) and the denormalized
user_home_item_dismissals.series_id behind. All four now move, mirroring
the provider-merge remap.
All five reattribute DB tests now verified green against PostgreSQL, with
new coverage for managed downloads, subtitle preferences, and dismissal
series ids.
Part of #318
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d08a4f232f |
feat(audiobooks): emit non-null imagePath for photo-bearing ABS authors (#316)
* fix(audiobooks): select photo_path in ABS author detail query
GetAuthorByID selected poster_path from the people table, but the
person-image column is photo_path, so every GET /api/authors/{id}
request failed with SQLSTATE 42703 and a 500. Since photo_path is
NOT NULL DEFAULT '', the nullable scan is also dropped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(audiobooks): emit non-null imagePath for photo-bearing ABS authors
Author responses always emitted imagePath: null, so ABS clients never
fetched /api/authors/{id}/image even though it works and ~7% of
audiobook authors have a people.photo_path. Real ABS puts a server-local
filesystem path in imagePath and clients treat any non-null value as the
cue to fetch the image endpoint, so a synthetic ABS-shaped path is
emitted whenever the person has a photo.
The detail path reads the flag from Author.PosterPath. The list path
carries a new AuthorSummary.HasPhoto: the MV query LEFT JOINs people at
read time (photo presence stays current instead of stale until the next
REFRESH) and the live fallback selects photo_path alongside its existing
people join.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|