9cd00a877bda129b4ded01e863a89495fcc680ef
157
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
88aa769fe6 |
feat(collections): surface server collections on the user Collections tab (#156)
* feat(collections): surface server collections on the user Collections tab The user-facing Collections tab only showed personal collections, which are usually empty — leaving most users with a confusingly blank page. Server (admin-curated) collections were reachable only inside each individual library's tab. Add a new GET /collections/server endpoint that aggregates visible library collections across every accessible library (honoring access scope, capped per library with a total_count for a See all link), and restructure Collections.tsx into two titled sections: Your collections (personal) and Server collections (horizontal teaser rows per library, linking into each library's Collections tab). Extract the shared CollectionPosterCard so the per-library grid and the new rows share one implementation. * fix(collections): match server-collections loading skeleton to row layout The Server collections section renders as one horizontal teaser row per library, but the loading skeleton showed a poster grid — so data arriving visibly reflowed the page from a grid into rows. Mirror the final layout (section header + per-library rows of poster cards) in the skeleton, and drop the now-unused COLLECTION_POSTER_GRID_CLASSES import. Addresses CodeRabbit review comment on PR #156. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Align server collections with shared carousel behavior - Add opt-out edge padding to reusable media carousels - Render server collection rows with shared carousel controls and spacing --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5afe56cfc0 |
feat(jellycompat): add runtime-managed Jellyfin Web compatibility (#77)
* feat(jellycompat): install web assets at runtime * fix(jellycompat): recover stale web operation locks * fix(jellycompat): harden web component management * feat(admin): refine compat settings and restart status * chore(dev): add hot-reload docker compose stack * fix(dev): include npm in hot-reload backend * feat(admin): refine Jellyfin compatibility settings * feat(settings): improve jellyfin proxy summary * feat(settings): improve jellyfin web controls * fix(settings): update jellyfin web removal status * fix(settings): enable jellyfin web after install * feat(jellycompat): auto-select web ui version * test(api): update rate limit handler setup * feat(jellycompat): refine web ui install onboarding * fix(jellycompat): address web ui install review issues * fix(onboarding): mirror jellyfin api runtime status * fix(admin): remove global restart banner * fix(settings): gate restart required tracking * fix(jellyfin): ignore live settings for restart status * fix(jellyfin): avoid restart for live compat settings * fix(subtitles): normalize AI language codes * fix(catalog): support partial title search tokens * feat(branding): add white-label customization * Add push relay engineering plan - Document relay API contracts, APNs/FCM behavior, auth, storage, and ops - Capture implementation plan, provider references, decisions, and README --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
bcf0253c09 |
feat(notifications): notify requesters of request status changes (#143)
Requests previously only notified the community server channels for submitted/approved/declined and the requester personally for fulfilled. This closes the gap and makes request posts addressable: - New request.approved / request.declined delivery types ride the operational dispatch path to the requesting profile: inbox, websocket toast, email, Discord DM, personal webhooks (gated by the existing notify_requests flag), and web push. Submitted stays broadcast-only (the requester performed the action themselves). Title/year/decline reason travel in reason_flags since no catalog item exists yet. - Request status notices are transactional: digest-mode recipients get an off-schedule early send (watermark-durable, last_digest_at left alone) instead of waiting for the digest hour. Per-episode recipients were already immediate via the dispatch nudge. - At-most-once per (profile, request, type) via a partial unique index (migration 20260612100000), mirroring the fulfilled dedupe. - Server-channel Discord request posts can @mention the requester via their OAuth-linked identity (notifications.server_channels. mention_requesters, default off). Resolved lazily in the sweep worker only when a Discord destination is about to receive the event; the ping uses content-level mention with pinned allowed_mentions, and the Discord identity never leaks into generic webhook payloads. Android/Apple clients render the new inbox types with their generic fallback until they add them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9508449859 | feat(web): add settings search command palette | ||
|
|
91e987bcda |
fix(web): add padding and hierarchy to notification preferences popover
The shared PopoverContent primitive ships with p-0, but the preferences popover never added its own padding, leaving toggle rows flush against the border. Add p-4, separate the master switch from category toggles with a divider, and dim dependent rows while the master switch is off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22af261786 |
feat(web): redesign admin notifications settings around pipeline and channel cards
Replace the flat wall of setting groups with three zones that mirror how notifications actually flow: a pipeline card framing the master switches as Record events -> Fan out -> Deliver (with paused-stage explanations), expandable per-channel cards with collapsed status chips (digest hour, Discord credential state, failing server channels, SSRF warning), and an Advanced zone for fanout tuning and retention. Discord's setup wall becomes a collapsible guide; settings stay editable while a channel is off so admins can configure before enabling. Same setting keys and form plumbing throughout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
799754dde7 |
feat(notifications): rich Discord embeds with posters, provider links, and ratings
Upgrade all outbound Discord surfaces (personal webhooks, bot DMs, server channels, request events) from bare title/description embeds to rich ones: poster thumbnail, overview teaser, TMDB/IMDb/TVDB links, rating and genre fields, content-rating footer, and a clickable title URL. Artwork respects the v1 privacy contract via a new admin poster mode (notifications.discord.poster_mode): "provider" (default) only emits public provider-CDN URLs, "server" additionally presigns locally cached posters from this server's image storage, "off" drops images entirely. Builders never derive artwork URLs themselves; the sender layer resolves PosterURL through System.discordPosterURL. To keep provider-CDN URLs derivable after image caching rewrites poster_path to a local storage key, media_items gains poster_source_path, captured during cacheItemImages, preserved across refreshes that keep the cached poster, and cleared on explicit poster overrides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d2ccbcb1a6 |
fix(web): type server channel event field tables explicitly
tsc -b (the production build) collapses flatMap over the as-const union of two differently shaped readonly field tuples to unknown, failing the Docker frontend stage; a bare tsc --noEmit accepted it. Give the field entries one explicit shared type so flatMap and channel[field.key] resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3d2368aed7 |
feat(notifications): admin server channels broadcasting new content and request activity
Add admin-owned broadcast destinations ("community channels"): Discord or
generic webhooks fed straight from release_events by a per-channel watermark
sweep, announcing newly added movies/episodes as grouped digest posts plus
configurable media request lifecycle events (submitted/approved/declined/
fulfilled).
- Extend release_events with a kind discriminator and add a movie
availability spine (movie_availability + kind-keyed
notification_content_seed_state; first full scan seeds silently so
upgrades never flood the movie back catalog)
- Sweep worker reads events by (created_at, id) cursor with batch-window
grouping, per-channel backoff, and auto-disable; request events post
best-effort via new requests.LifecycleNotifier hooks
- Reuse the webhook stack throughout: URL encryption (new AAD namespace),
SSRF guard, embed limits, HMAC signing; shared type/name validation
extracted for both services
- Admin CRUD API under /admin/notifications/server-channels and a Server
Channels section in the notifications admin settings UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ebf3352bda |
feat(notifications): per-profile email channel with verified addresses
Re-key the email notification channel from login accounts to profiles. Each profile owns its mode, dispatch watermark, and destination address; there is deliberately no fallback to the account email, so the account holder no longer receives mail for every household profile. A profile receives nothing until its own address is verified. - Genericize the watermark-sweep engine over a recipient key (accountChannel[K]): email keys by profile_id, Discord stays on user_id. Delivery reads move into the channel adapters. - Custom addresses verify via single-use SHA-256-hashed token links served by a public endpoint; enabling the channel requires a verified address, and clearing the address switches the channel off. - Addresses are globally unique (case-insensitive): rejected when verified for another profile or matching another account's email or username. Checked at request time, re-checked at verify time (first-to-verify wins), backstopped by a partial unique index. - Every email carries an RFC 8058 one-click unsubscribe link backed by a per-profile capability token, minted lazily under the claim tx. - Child profiles cannot set addresses (and so receive no email in v1). - Verification sends are rate limited (1/min, 10/day per profile); mail.Message gains custom header support for List-Unsubscribe. - Migration drops the account-level prefs table without carrying opt-ins over, so nobody gets surprise emails post-upgrade. Android/Apple notification settings need follow-up for the new profile-scoped response shape and address-management endpoints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5f05374a1d |
feat(notifications): Discord bot DM channel with account linking
Adds Discord direct messages as a notification channel. Users link their Discord account via OAuth2 (identify scope only, one-time server-side state rows); a bot delivers their inbox notifications as DMs. - Extract the email channel's watermark sweep into a generic account-channel engine; email and Discord are now thin adapters, so the SKIP LOCKED claim / watermark-after-send durability logic exists once. - New internal/discord REST client (token exchange, identity, open DM, send message) — no Gateway connection, no new dependencies. - Opt-in master switch (notifications.discord_enabled, default off) gates delivery, linking, capability, and the admin settings reveal. - Admin UI: credentials (secret + bot token encrypted at rest), dev portal setup checklist, bot invite link buttons, and a test button that bypasses the settings read cache and is disabled while credential edits are unsaved. - DM failures from missing shared guild (Discord 50007) surface as link health in user settings and self-heal via capped backoff. - New combined mode (per_episode_and_digest) for email and Discord: instant sends all day plus a daily digest recapping the whole window since the previous digest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
27b9006ef5 |
feat(notifications): gate user webhooks behind admin opt-in
Letting users point server-originated HTTP at arbitrary destinations is an admin decision, so notifications.webhooks_enabled now defaults to off instead of acting as a default-on kill switch. The flag is also enforced at webhook creation and test sends (delivery was already gated at enqueue and dispatch); existing webhooks stay manageable while disabled so rows are never stranded. The admin toggle moves into the Webhook Guards group with an off default, and the user settings page hides the Webhooks section entirely when the capability is unavailable, matching the other channel sections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
df95e3cb95 |
feat(notifications): email notification channel
Adds email as a notification channel built on the shared SMTP core (mail.Sender). Email mode is a per-account preference (off, daily digest, or per-episode) stored in notification_email_prefs; delivery is an account-watermark sweep over notification_deliveries that dedupes cross-profile duplicates, advancing the watermark only after a successful send. Admin controls cover the channel kill switch, the per-episode allowance (off coerces those accounts to the digest), digest hour, and an external URL for deep links inside emails. Availability is advertised through /notifications/capability and the user settings page gains an Email section for opt-in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e5b210589d |
fix(notifications): address PR #136 review findings
Codex + CodeRabbit review fixes, all verified against current behavior: - Web Push: single-writer VAPID provisioning via a new conditional SetIfAbsent settings write (no split-brain identity across nodes), and read/decode failures now surface instead of silently rotating the keypair; the eager-provisioning goroutine joins the shutdown WaitGroup - Web Push: endpoint reassignment purges the previous owner's pending attempts inside the upsert transaction, with an ownership re-check at send time - Webhooks: per-profile cap enforced atomically (advisory-locked count+insert), typed pgconn unique-violation mapping, create-time type/URL mismatch rejection, send-time HTTPS re-check, and Retry-After HTTP-date support (shared, clamped parser also used by web push) - Delivery workers: transient delivery-row lookup errors leave the claim to lease expiry instead of permanently failing the attempt - Interest: history-only imports now feed the index (userstore history hooks + completed-history folding in recompute/rebuild), rebuild also recomputes existing interest rows so removed sources get cleaned up, and failed flush mutations requeue (bounded) instead of dropping - Retention: read notifications age from read_at, not created_at - Startup: scan queue workers start only after the availability detector is wired, so resumed scans cannot skip availability recording - mail: settings-store read failures propagate instead of reading as "not configured" - DB: new migration adds episode ordinal/key CHECK constraints - Web: service worker restricts notification clicks to same-origin URLs, preferences popover gets an error+retry state, and the realtime profile-rebind backoff grows to 5 minutes to keep shared channels stable through notifications-only outages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4274220266 |
feat(web): show web push delivery health in notification settings
Surface per-subscription delivery health on the Notifications settings page: this browser and each other device now show last-delivered / last-failed status (failures newer than the last success render in amber, mirroring webhook health), plus a device count and an explicit disabled-after-failures state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d9e27da59e |
feat(notifications): request-fulfilled notifications across all channels
Notify the requesting profile once its media request is actually present in the catalog (roadmap 06, item 2). Completion transitions stay notification-agnostic; a presence-gated pass at the end of each reconcile run fires the notice, so it means "watchable in Silo", not "download finished". - New System.DispatchOperational: delivery insert + webhook/web-push outbox enqueue in one transaction, post-commit multi-dispatch. The webhook auto-disable notice now rides the same path (replacing its hand-rolled hub publish and the now-removed InsertOperational), which also delivers auto-disable notices over web push. - At-most-once delivery: partial unique index on (profile_id, reason_flags->>'request_id') plus a fulfilled_notified_at marker on media_requests, backfilled for pre-existing completed requests so deploys never flood. - Per-webhook notify_requests toggle (default on) through repo, service, API, and settings UI; gated independently of the episode reason flags. - request.fulfilled rendering in web inbox, realtime toast, web push payload, and Discord/generic webhook payloads, deep-linking to the matched catalog item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4fd21c7aee |
feat(web): admin settings page for notification controls
Expose every live notifications.* server setting on a new admin Notifications tab (next to Email): kill switches, fanout tuning, webhook guards, and retention. Unset kill switches render as enabled to match the backend defaults, numeric fields surface their effective defaults, and enabling private webhook destinations shows an SSRF warning. v1.5 roadmap item 1; no backend changes — all keys are live-read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b091f0c6c1 |
feat(notifications): in-app inbox, realtime, webhooks, web push + shared SMTP core
Implements the notification system foundation and all v1 delivery channels that need no external infrastructure (specs 00/01/04/05 in docs/superpowers/plans/notifications/): Foundation (spec 01): - episode_availability seeding + per-library seed markers: "newly available" means newly released to this server, so back-catalog imports and first scans never flood (verified on dev: 1.13M episodes seeded silently) - release_events -> profile_series_interest fanout worker with settling delay, per-series burst caps, FOR UPDATE SKIP LOCKED multi-node claims, and a guarded last-notified cursor - interest index maintained via a userstore provider decorator so every favorites/watchlist/progress mutation path (REST, jellycompat, imports, playback) feeds it; progress writes only recompute on state transitions - durable per-profile inbox + read state, forward-sync cursor API, websocket channel with short-lived single-use handshake tickets - web UI: sidebar badge, inbox page, toasts, per-profile preferences - startup/daily tasks: availability seeding, interest rebuild, retention Outbound webhooks (spec 04): - Discord embeds (text-only per the v1 privacy contract) and generic JSON signed Stripe-style with per-webhook secrets - HTTPS-only + private-destination guard enforced at registration and at connect time (DNS-rebinding mitigation); URLs/secrets encrypted at rest - durable per-target outbox enqueued in the fanout transaction, lease-based claims, 24h exponential retry, 3x-consecutive-4xx auto-disable with an in-app notice (loop-guarded) Web push (spec 05): - VAPID keypair self-provisioned at startup (single atomic JSON setting, private half encrypted at rest) — no third-party accounts needed - payloads E2E-encrypted (RFC 8291); 404/410 treated as unsubscribe - service worker + subscribe flow in Settings -> Notifications Shared SMTP core (internal/mail): - feature-agnostic mail.Sender over live email.* settings, STARTTLS or implicit TLS, encrypted password, admin Email settings page with synchronous test send; no consumer yet by design (digest is v1.5) APNs/FCM (specs 02/03) are deferred to v2; the capability endpoint reports them unavailable so clients render truthfully. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a0f7810481 |
fix(web): show admin chrome only on the admin account's primary profile (#131)
* fix(web): show admin chrome only on the admin account's primary profile The top-right ServerActivity indicator and the sidebar Admin section were gated on the account-level role alone, so every profile on an admin account — including child profiles — saw admin system notifications and the indicator polled four admin endpoints on their behalf. Gate both on the active profile being the household primary, matching the existing is_primary idiom in SettingsLayout and the server-side quota exemption. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): resolve active profile via useCurrentProfile in admin route gates RequireAdmin/RequirePrimaryOrAdmin read the profile from useAuth(), but the admin chrome (AppSidebar, Layout) gates on useCurrentProfile(), which resolves the selected profile. Use the same source in the route gates so the redirect and the visible admin UI can never disagree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web,api): centralize acting-admin policy and enforce it server-side Address code-review findings on the primary-profile admin gate: - Add isActingAdmin to web/src/lib/permissions.ts as the single client-side definition of the policy (admin role + primary or no profile), with a useIsActingAdmin hook on top. Route gates, sidebar, Layout, and realtime channel gating all use it now, so the gate and the chrome can no longer disagree on null-profile handling. - Convert the admin-gated surfaces the original change missed (MediaItemMenu, EditMetadataDialog images tab, AddToCollectionDialog, MarkerEditor, theme CatalogBrowser, PersonDetail, SettingsLayout, ItemDetail content pages) so an admin on a non-primary profile is a regular viewer everywhere, not just in the sidebar. - Make the role-derived permission bypass (metadata curation, marker edit) follow the same policy on both client and server. - Enforce the policy server-side: RequireActingAdmin middleware refuses admin routes when the request declares a non-primary profile via X-Profile-Id, and the metadata-curation middleware holds admins on non-primary profiles to explicitly assigned permissions. - Stop spreading the profiles query result from useCurrentProfile so route gates only re-render when the resolved profile changes, and make it safe outside AuthProvider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web,api): fail closed on unresolved profiles in acting-admin policy Address review feedback on the acting-admin gate: - Server: actingAdminAllowed now denies when the declared profile cannot be resolved to one of the caller's profiles, so a bogus X-Profile-Id can no longer restore admin powers to a non-primary session. - Client: useIsActingAdmin returns false while a selected profile id has not yet resolved (e.g. hard refresh before the profiles query returns), instead of briefly treating it as "no profile selected". useCurrentProfile exposes hasSelectedProfile to make that state distinguishable. - hasPermission/canCurateMetadata/canEditMarkers now require the profile argument (resolved profile or explicit null), so a missed call site fails the typecheck instead of silently restoring the admin bypass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
0eb87fa0f5 |
fix(player): flip ASS subtitle delay sign to match VTT semantics (#130)
JASSUB renders the ASS event matching video.currentTime + timeOffset, so an event at source time S appears at video time S - timeOffset. Adding the user delay to the offset therefore showed ASS subtitles EARLIER on positive delay, while the VTT path (and the subtitle menu's "later" label) shifts cues later. At +2000ms an ASS track diverged from an SRT track by a full 4 seconds. Subtract the delay instead, correct the comment to state the actual JASSUB render equation, and add tests covering the constructed timeOffset for positive/negative delays plus the live-instance update path. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fadd8ff456 |
feat(player): native PGS subtitle rendering via libpgs (#129)
* feat(playback): add IsPGS helper and sup streaming extract path PGS (Blu-ray bitmap) subtitle tracks can be copied losslessly into a .sup elementary stream for client-side rendering, so they no longer have to be burned in. streamExtractOutput maps PGS to (copy, sup), and the seek/-t windowing now skips PGS like ASS: both formats are fetched once and consumed whole by their client-side renderers. This also fixes a pre-existing truncation bug: the -t duration cap was applied unconditionally, cutting embedded ASS extracts off at the default 600s window even though the ASS client fetches the full track. Extract the ffmpeg argument construction into streamExtractArgs for testability, following the buildFFmpegArgs pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(api): expose PGS subtitle tracks as .sup stream URLs PGS tracks were filtered out of /playback/start subtitle_urls entirely, so the web player showed no subtitles for PGS-only files (#34). Include them with a .sup URL extension; DVD/DVB bitmap tracks stay hidden since they still have no non-burn-in delivery path. HandleSubtitle streams the full PGS track as application/octet-stream. The seek/duration window is forced to zero for sup: subtitleSeekPosition falls back to the session's last reported position even without a ?position= query, which would otherwise start the extract mid-file. The proxy-node subtitle handler gets the same sup branch, streaming ffmpeg output directly instead of buffering like its text paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(player): consolidate subtitle codec helpers into subtitleCodecs.ts Rename assSubtitles.ts to subtitleCodecs.ts — the module already labeled every codec, not just ASS — and add isPGSCodec/isBitmapCodec. Replace the duplicated BITMAP_CODECS set in SubtitleTranslateModal with the shared helper so codec lists live in one place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(player): native PGS subtitle rendering via libpgs Render PGS subtitle tracks client-side instead of leaving them unavailable (#34). usePGSSubtitles mirrors the JASSUB hook: when a PGS track is active it lazy-loads libpgs, which fetches the .sup stream in a worker, decodes display sets progressively as bytes arrive, and draws them onto a canvas positioned over the video. The renderer looks up the display set at currentTime + timeOffset, so the HLS stream origin adds and the user-facing delay subtracts — a positive delay shows subtitles later, matching VTT semantics. Offset changes apply through the timeOffset setter without recreating the renderer; track switches, PiP detach, and unmount dispose it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(player): prefer text over bitmap tracks in subtitle auto-select With PGS tracks now listed, an earlier PGS track would win auto-select over a later same-language SRT/ASS track. Deprioritize bitmap codecs within the same source tier — text is lighter to render and styleable — while a PGS track still wins when it is the only language match, and forced-PGS auto-select now works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a17529fd6f |
feat(config): live admin settings + truthful restart-required banner (#128)
* feat(nodeconfig): harden config watcher for integrated-mode use - RequestReload(): non-blocking, coalescing reload nudge that runs on the poll goroutine, so concurrent requests can never swap a stale snapshot over a newer one (unlike ForceReload from request handlers) - Skip OnChange callbacks when the reloaded config is deep-equal to the previous one, so the 60s poll doesn't fire rebuild/log callbacks on no-op reloads - Add RedisURL to BootstrapOverrides; previously a reload clobbered an env-provided Redis URL in the live config - Split reload into fetchSettings/applySettings and add unit tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(config): hot-reload config watcher in integrated mode Start nodeconfig.Watcher in integrated/api mode (previously only proxy/ transcode worker modes hot-reloaded). Expose the live config to the API and jellycompat routers via func-typed LiveConfig/OnConfigChange fields with nil fallbacks to the startup snapshot, and wire the admin settings update hook to RequestReload so same-process changes apply immediately even without Redis. No consumer reads the live config yet — conversions land separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(admin): truthful restart-required banner for settings saves The settings UI showed 'restart required' after every save regardless of the key. The backend now classifies each key via a central registry (internal/config/restart_keys.go) and PUT /admin/settings/{key} reports restart_required per key; useSettingsForm only raises the banner when a saved key actually needs a restart (and keeps it raised until restart). The registry is conservative: every currently startup-frozen key is marked restart-required; subsequent hot-reload conversions shrink it. Settings read live from the settings repo (branding, overlays, markers, download.*, ...) default to no-restart. DownloadSettings/OverlaySettings drop their hardcoded restartRequired={false} special-casing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(logging): hot-reload server.log_level and server.log_quiet Share one slog.LevelVar across the handler chain and make logfilter.Handler's quiet-prefix list an atomic pointer shared with WithAttrs/WithGroup clones (New previously returned the inner handler unwrapped when the quiet list was empty, leaving nothing to update). The integrated-mode config watcher now applies both settings live; their keys leave the restart-required registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(auth): hot-reload access/refresh token expiries JWTService stores expiries as atomics with a SetExpiries hook; all three instances (main API, ABS compat, jellycompat) re-apply them on config reload. Applies to newly issued tokens; outstanding tokens keep their original expiry. The JWT secret stays fixed for the process lifetime. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(playback): read transcode config live at session start The playback and stream handlers pull ffmpeg path / hwaccel / transcode dir from the live config when starting a transcode or extracting subtitles, instead of values frozen at router construction. Each session snapshots the config once so its output dir and binary stay consistent. Also fixes a real bug: playback.hw_device was parsed into the config but never wired into the integrated-mode handler, so local transcodes always ran with an empty HWDevice while transcode nodes honored it. playback.transcode_dir leaves the restart-required registry (the handler is its only consumer); ffmpeg_path/hw_accel stay restart-required until scanner/chapterthumbs/audiobook consumers convert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(jellycompat): read compat identity settings live per request System/Auth handlers take a config provider instead of the startup snapshot, so jellyfin_compat.public_url, .server_name, and .emulated_server_version apply without restart. server_id stays restart-required (generate-once, baked into the resource mapper), as do the session-store TTLs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scanner,metadata,mdblist): hot-reload worker pools and API key scanner.workers, matcher.workers/batch_size, metadata.cache_images, and mdblist.api_key convert to atomic fields with setters wired to the config watcher. Worker counts apply on the next scan/match cycle (the loops read them per cycle); the MDBList key applies to the next request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ai): hot-reload AI connection, models, toggles, and quotas The shared llm.Client holds its config behind an atomic pointer (UpdateConfig; each request snapshots once), and the subtitle/metadata AI services gain UpdateConfig plus setters on the translator (batching) and Whisper transcriber (ffmpeg path, chunk seconds). The router derives their configs from shared helpers used both at construction and in OnConfigChange callbacks, re-evaluating the chat-only-gateway transcribe guard on each reload and warning only when it newly fires. Everything on the AI Services page now applies without restart except ai.max_concurrent_jobs (fixed-capacity dispatch semaphore). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): wire transcode_enabled; remove dead playback/scanner knobs playback.transcode_enabled was parsed into the config but the resolver always received a hardcoded true — the admin toggle did nothing. It now reads the live config per playback start, so disabling transcodes applies without restart. Remove settings that were wired to nothing so 'save + restart' stops pretending: playback.allow_hevc_encoding (resolver field never assigned), playback.transcode_ahead_segments and playback.segment_duration (parsed, never consumed — segment duration is per-session from the client), scanner.file_removal_grace (DeleteMissing is never called). UI fields removed and the config struct fields pruned so they don't resurrect; YAML import still tolerates the legacy keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7958f0bbf0 |
feat(nodepool): node groups, per-node caps, and local transcode fallback control (#126)
* feat(nodepool): node groups, per-node caps, and local transcode fallback control Group co-located transcode and proxy nodes so transcoded streams are served by a proxy on the same host/LAN instead of bouncing across the internal network (fixes #93): - New nodepool.Planner is the single selection entry point: it picks the transcode node and its group's proxy together (round-robin within the group), replacing the independent ProxyPool.Pick/TranscodePool.Acquire calls scattered across the native and jellycompat handlers, and absorbs the duplicated soft-affinity pick logic. - A group is only eligible while all of its enabled members are healthy; ungrouped nodes keep the historical behavior. - New per-node max_jobs cap (transcodes for transcode nodes, streams for proxies; NULL = unlimited), enforced via health-reported job counts plus short-lived reservations that expire once fresher health data arrives. Proxy health now reports real stream counts, including HLS sessions via idle-expiry tracking. - New playback.local_transcode_fallback setting (default on) lets admins refuse API-server transcoding when no eligible node exists. - Health checks now publish updated node copies under the pool lock instead of mutating shared structs in place, fixing a data race. - Admin UI: group + cap fields on the node form, group/cap columns, and the new fallback toggle in playback settings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(nodepool): proxy bandwidth measurement and egress caps Proxy nodes now measure their stream egress (rolling 60s average over everything under /stream) and report it via the health endpoint. A new per-proxy max_bandwidth_kbps cap lets the planner route new streams away from saturated proxies: - Admission combines the measured egress with the estimated bitrate of the new stream (transcode target bitrate, or source bitrate for direct play/remux) so a stream is only admitted where it fits. - Recently admitted streams are bridged as bandwidth reservations for the meter window, since the rolling average only converges on a new stream's rate gradually. - A group whose proxies lack bandwidth headroom is treated as full: its transcode nodes are skipped, same as the job cap. - Admin UI: per-proxy "Max Egress Bandwidth (Mbps)" field and a live egress column; manual health checks return the measured rate. Active streams are never interrupted - the cap only gates new admissions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(playback): trim node-mode time-to-stream-start Distributed playback paid several avoidable costs before the first frame that integrated mode doesn't have. This trims the safe ones: - Web player preconnects to the stream origin (the proxy node) as soon as /playback/start returns, overlapping DNS/TCP/TLS handshakes with the transcode dispatch instead of paying them at the first manifest fetch. - The transcode node no longer blocks its 202 on monitoring work: the Redis session-track write moves off the request path, and a replaced session's segment directory is renamed aside and deleted in the background instead of synchronously (RemoveAll of a long session can take seconds on slow disks during quality switches). - The proxy's node-facing HTTP client gets a tuned transport: a larger idle-connection pool (Go's default of 2 per host causes connection churn and TLS re-handshakes when many viewers stream through one proxy->node pair) and a response-header timeout so a hung transcode node can no longer hang client requests indefinitely. - jellycompat's remote transcode dispatch gains the same 10s timeout the native path has had; an unreachable node previously hung the compat manifest request until the OS gave up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
52dd5dff7f |
perf(web): prefetch season detail and episodes from series page
Season cards now warm the react-query cache for the season's item detail and episodes on hover/focus/touch, so navigating from a series page to a season renders the episode grid without a request waterfall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ed4cebf3ba |
feat(ai): per-user transcription quota for subtitle ASR jobs
Cap how many Whisper transcription jobs each user account can start per rolling window (day/week/month), configurable from admin settings. The player modal shows remaining usage and the server returns 429 with details when the limit is hit. Enforcement is atomic with the job insert (per-user advisory lock, same pattern as media-request quotas), so concurrent requests cannot race past the limit. Failed/cancelled jobs that never produced transcription work are refunded. Exemption applies to the admin account's primary profile only; other profiles on an admin account stay subject to the quota. A partial index covers the quota count, a malformed quota setting row degrades to "no quota" instead of blocking startup, and the period vocabulary and admin-role predicate are each defined once (ai.ValidQuotaPeriod, apimw.IsAdmin). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
39ba284c9d |
feat(ai): shared AI core — metadata translation, Whisper ASR, per-profile language, on-view translation (#127)
* docs: design + plan for shared AI core, metadata translation, Whisper ASR Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(ai): shared LLM client, segment translator, and job runner packages internal/ai/llm: OpenAI-compatible chat client moved out of subtitles/ai, plus /v1/audio/transcriptions (verbose_json) for the ASR work; one shared retry/backoff loop for both. internal/ai/translate: the batched indexed-JSON translation protocol generalized to text segments. internal/ai/jobrunner: dispatch/heartbeat/reaper/cancel lifecycle extracted behind a minimal store interface, with a semaphore shareable across job services. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(subtitles): consume shared AI core LLMTranslator becomes a thin cue<->segment adapter over aitranslate; the service delegates dispatch/heartbeat/reaper/cancel to jobrunner; the local OpenAI client is gone in favor of internal/ai/llm. Behavior (prompts, wire protocol, job rows, recovery semantics) is unchanged. NewService now takes the dispatch semaphore so all AI job services can share one bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(config): shared ai.* settings, metadata translation job table, localization provenance columns ai.* connection keys (chat + optional separate ASR endpoint) load with a fallback to the legacy subtitle_ai.* rows — those are never renamed in SQL because encrypted values are GCM-bound to their setting key. New toggles: subtitle_ai.transcribe_enabled, metadata_ai.enabled. Migration adds metadata_translation_jobs, per-field provenance (provider|ai|manual) on the localization tables, and media_folders.auto_translate_metadata. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(catalog): localization field provenance with provider/ai/manual precedence Provider upserts keep manual values and never blank a field with an empty incoming value; new UpsertAITranslation/UpsertAIOverview methods write AI fields only over empty or ai-sourced values (force adds provider, never manual) — all enforced in single-statement SQL. Serving now merges only non-empty localized fields onto the base item, since localization rows are legitimately partial (AI rows carry no titles/artwork). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(metadata): AI translation service, refresh auto-fallback, and admin API internal/metadata/translation: job service over the shared AI core that expands an item to its season/episode overviews, skips already-localized fields (zero model calls on repeat runs), batches paragraphs through the generic translator, and persists per batch with provenance-aware upserts. MetadataService gains an AutoTranslator seam invoked after each refresh for libraries with auto_translate_metadata. Admin endpoints under the metadata curation guard: enqueue, list (poll), cancel; plus a status probe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(subtitles): Whisper ASR transcribe and transcribe_translate jobs New WhisperTranscriber: one ffmpeg pass extracts the audio track to 10-min 16kHz mono WAV chunks (temp dir cleaned on every exit path), each chunk goes to the OpenAI-compatible /v1/audio/transcriptions endpoint (verbose_json, per-request timeout sized to 3x chunk duration), segment timestamps are offset and built into wrapped cues. Chunks process playhead-first and stream live to the requesting session. The transcript is stored as an ordinary downloaded subtitle (provider 'transcribed'); transcribe_translate chains the existing translator and stores the translated track as the job result. Enqueue accepts an optional kind; status reports transcribe_enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): AI services settings, metadata translate action, library auto-translate, generate-from-audio New AI Services admin page hosts the shared endpoint config (reads fall back to legacy subtitle_ai.* values, writes target ai.*) and the three feature toggles; the AI card moves out of Subtitles settings. The metadata editor gains a Translate-with-AI panel with job polling and force/re-translate. The library form gains the auto-translate toggle (threaded through the libraries API). The player translate modal gains a From-audio mode that lists audio tracks and submits transcribe / transcribe_translate jobs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: gofmt import grouping in router and translation tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(catalog): per-profile metadata language and viewer-triggered description translation user_profiles.preferred_metadata_language threads through the access scope into catalog serving: presentation language now resolves explicit param -> profile preference -> library metadata language (native API and jellycompat). ItemDetail gains pending_translation_language when the viewer's language is missing a localized overview. New metadata_ai.on_view setting (off|button| auto) gates POST /items/{id}/translate-description: any profile with item access may request its language, with in-flight dedup and a 15-minute failure cooldown so page views never hammer a broken endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): on-view description translation with per-profile metadata language Profile playback settings gain a Metadata language picker (library default inherit). Detail pages: when the server reports pending_translation_language and metadata_ai.on_view is 'auto', the description translates on view with a pulse animation until the refetched detail comes back localized (45s timeout); in 'button' mode a small Translate chip triggers the same flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): expose metadata_ai.on_view in AI Services settings The on-view translation mode had no UI control, so it could only ever be 'off' — viewers got neither the auto translation nor the fallback button. Adds the off/button/auto selector to the Features card, and the config loader now warns and falls back to 'off' on a bad row instead of refusing to start. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): clear configuration hint when the transcription endpoint is chat-only A blank Transcription base URL falls back to the chat endpoint; chat-only gateways reject the multipart upload with an opaque 400 that reads like a pipeline bug. 400/404/405 transcription failures now carry a hint to set a Whisper-compatible endpoint in AI Services. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(subtitles): wrap ASR cue text by rune count, not bytes Arabic/Cyrillic/Greek text is 2+ bytes per character in UTF-8, so byte-based wrapping broke lines at roughly half the intended visual width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(web): steer transcription base URL hint away from chat-only gateways Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ai): block chat-only gateways for transcription, add endpoint presets llm.IsChatOnlyGateway (OpenRouter et al — no timestamped transcription API) is enforced in three layers: the settings API rejects ai.asr_base_url values pointing at one, the router disables ASR with a warning when the blank-URL fallback would land on one, and llm.Transcribe refuses outright. The AI Services page gains one-click transcription presets (Groq turbo/accurate, OpenAI, self-hosted speaches) plus the mirrored client-side check, and the settings API now also validates metadata_ai.on_view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(subtitles): tighten ASR subtitle sync Three systematic timing-error sources addressed: cue offsets now use the segment muxer's exact per-chunk start times (segment_list CSV) instead of assuming index*chunk_seconds; the audio stream's start delay relative to the container timeline (common in TS remuxes) is probed via ffprobe and added to every cue; and the chunk length is now operator-tunable via subtitle_ai.asr_chunk_seconds (60-600s, default 600) since shorter chunks bound Whisper's within-chunk timestamp drift. Playhead-first ordering now pivots on real chunk starts, and a beyond-end playhead starts at the final chunk instead of restarting from zero. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): tolerate base URLs that already include the /v1 segment Providers like DeepInfra expose their OpenAI-compatible API under a base that contains the version segment (api.deepinfra.com/v1/openai); always appending /v1/... mangled those. endpointURL now appends bare paths when the base already carries /v1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): prefer self-hosted transcription in presets and hints Preset order becomes self-hosted (recommended) -> Groq turbo -> Groq large-v3 -> OpenAI, and the settings hint plus the job-error hint lead with the self-hosted option. The self-hosted preset now fills the turbo CT2 model to match the recommended speaches setup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(subtitles): request VAD and word timestamps for ASR cue accuracy Without vad_filter, faster-whisper servers report wall-to-wall segment times: cues linger on screen through silence (verified up to 91s) and paragraph-length segments become single 400+ char cues. Request vad_filter=true (skipped for hosted providers that reject non-OpenAI fields and run VAD server-side) plus timestamp_granularities word+segment, and rebuild cues from word timings: split at speech pauses, sentence ends, text capacity, and a 7s max duration; cap word-less segments instead of trusting their reported end; stretch sub-second cues to a readable minimum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
54b20f57d7 |
feat(audiobooks): player QoL — shortcuts, smart rewind, per-book speed, volume, chapter nav (#125)
Brings the web audiobook player to parity with best-in-class audiobook apps (Audiobookshelf, Audible, BookPlayer) on the quality-of-life axis, per docs/superpowers/plans/audiobook-player-qol.md: - Keyboard shortcuts active on every route while the player is mounted (space/K, arrow skips, volume, M, N/P chapters, shift+./, speed, E expand, Esc collapse), inert while a video session exists. - Configurable skip intervals with asymmetric defaults (back 10s, forward 30s) via a new player settings popover. - Speed control expanded to 0.5x-3x with a 0.05 stepper, presets, and per-book memory (LRU-capped, device-local). - Chapter prev/next transport buttons (prev restarts the chapter when more than 3s in, the universal player convention). - Volume/mute via the shared VolumeControl with a new theme-token "surface" tone; persistence shared with the video player. - Smart rewind: backs up 3-30s on resume scaled by pause length, plus a flat 10s on cold resume from the detail page; explicit seeks are never second-guessed. Toggleable in player settings. - Now Listening time label cycles total / remaining / remaining at current speed. Chapter flattening is deduplicated into lib/audiobooks/chapters.ts, the SkipIcon is shared between both player views, and new pure modules (smartRewind, chapter nav, prefs) come with unit tests. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8b70357703 |
feat(ebooks): first-class ebook libraries, scanner, and reader (#124)
* docs: define ebook architecture matching audiobooks * docs: plan ebook audiobook-parity implementation * feat: add ebook scanner parser foundation * fix: harden ebook scanner foundation * fix: handle ebook isbn labels * fix: guard ebook subtree scans * feat: scan ebook libraries in core * fix: preserve ebook scan people credits * fix: refresh ebook scan metadata safely * feat: persist ebook series membership * test: cover ebook series persistence decisions * fix: address ebook scanner PR review * docs: clarify ebook foundation PR scope * feat: add ebook metadata enricher * fix: harden ebook poster cache * feat: wire ebook metadata sync task * feat: expose ebook library metadata setup * feat: add ebook catalog scope support * feat: add ebook detail view * feat: label ebook file versions by format * feat: use file-size copy for downloads * feat: use file language in download dialog * test: cover ebook detail authors and downloads * fix: drop narrator credits from ebook scanner merges * fix: align ebook collection filters with book media * fix: drop asin provider ids from ebook enrichment * fix: force ebook people refresh for stale narrators * chore: omit ebook planning docs from branch * feat: add ebook detail related content * feat: add ebook reader file entrypoint * feat: render ebooks with foliate reader * feat: persist ebook reader progress * feat: add ebook reader controls * feat: extract ebook pdf metadata * feat: favor scanner isbn during ebook enrichment * feat: extract fbz ebook metadata * feat: count cbz ebook pages * feat: show ebook file page counts * feat: show ebook download summaries * feat: switch ebook reader files * feat: prefer epub for ebook read action * feat: surface ebook reader progress * feat: sync ebook reader progress cache * feat: hide ebook read action for unsupported files * feat: filter ebook reader file selector * fix: serve fbz ebook archives with reader mime type * fix: detect fbz ebooks from compound filename * fix: authorize fbz ebooks from compound filename * fix: scope ebook catalog facets * fix: reject narrator queries for ebooks * fix: build ebook recommendation text from authors * fix: include ebooks in embedding eligibility * fix: include ebooks in recommendation media mix * fix: include ebooks in recently added recommendations * feat: include ebook progress in recommendation signals * feat: include ebooks in continue watching sections * feat: include ebooks in catalog progress metrics * fix: read ebook isbn from epub metadata * fix: filter ebook asin provider aliases * fix: fall back from unsupported ebook reader files * fix: sort ebook catalogs by reader progress * fix: filter ebook catalogs by reader progress * fix: include ebooks in last watched catalog filters * feat: reflect ebook reader progress in item user state * feat: share ebook progress state across item surfaces * feat: report ebook scan progress * fix: include ebook activity in recommendations * fix: expose ebook reader progress on item detail * fix: support ebook subtree scans * fix: honor profile header for ebook item progress * fix: add ebook library default sections * fix: route ebook continue cards to reader * fix: hide watched toggle for ebooks * fix: route ebook watch tonight cards to reader * fix: route ebook hero actions to reader * fix: detect archive ebook reader formats by filename * feat: cache embedded ebook covers during scan * fix: encode ebook hero reader links * fix: persist non-epub ebook reader progress * fix: scope narrator catalog badges to audiobooks * fix: merge ebook reader progress during item repair * fix: label ebook progress filters as read * fix: show ebook related rails as book covers * fix: remove txt ebook reader support * fix: reject txt ebook reader files * fix: label ebook advanced filters as read * fix: label ebook personalized sorts as read * fix: remove plain text reader loader path * test: cover ebook unread catalog rules * fix: preserve ebook reader library context * fix: link ebook genres with library scope * fix: encode related rail item links * fix: encode catalog card item links * fix: encode hero and continue item links * fix: encode watch tonight item links * fix: encode recommendation and search item links * test: cover ebook scan format set * fix: label ebook search results clearly * fix: make global search prompt media neutral * fix: encode catalog read API ids * fix: encode item API ids * fix: include ebook reader vendor in docker build * fix: make ebook reader build clean * fix: clean ebook embedded descriptions * docs: plan ebook reader shell parity * feat: add ebook reader shell controls * fix: widen ebook scrolled reader flow * fix: remove scrolled reader content width cap * docs: plan ebook reader full parity * feat: persist ebook reader config * feat: add ebook annotations and bookmarks * feat: add ebook reader tools and aids * feat: add ebook advanced reader settings * fix: keep ebook reader panel in viewport * fix: use foliate sizing units for ebook scroll flow * fix: keep ebook settings controls readable * fix: simplify ebook reader settings controls * feat(ebooks): extract local covers during scan (#98) * feat(ebooks): extract local covers during scan * fix(ebooks): read nullable poster paths during cover scan * fix(catalog): coalesce nullable media artwork fields * fix(ebooks): group sibling formats by book identity * fix(ebooks): tolerate legacy ebook metadata encodings * fix(ebooks): decode PDF hex metadata strings * fix(ebooks): harden local cover extraction and format grouping Address review findings on the local cover scan: - Restrict generic sidecar covers (cover.jpg, folder.png, ...) to single-book directories, always accept images named after the book file, and apply exactly one cover per reconcile with sidecar taking precedence over the embedded cover. - Replace the read-then-write poster update with an atomic conditional UPDATE (ItemRepository.SetLocalPoster) so provider/admin artwork is never clobbered by concurrent writers, and refresh locally owned posters when the extracted cover bytes change (thumbhash compare). - Preserve UTF-8 PDF Info strings (including a UTF-8 BOM) instead of forcing everything through Windows-1252; the cp1252 fallback now only applies to non-UTF-8 bytes. - Select EPUB covers by manifest media-type with properties="cover-image" outranking the EPUB2 meta name="cover" id, so XHTML cover pages no longer shadow the real image. - Order CBZ pages naturally (2.jpg before 10.jpg, ch2/ before ch10/) when picking the cover page, via a single O(n) min-scan. - Bump the ebook content group key scheme to version 2 and reprocess rows written under older versions so pre-existing libraries gain sibling-format grouping instead of accumulating duplicates. - Group different formats only (a same-format sibling with colliding sparse metadata stays a separate item) and stop a joining sibling's embedded metadata from overwriting a provider-matched item. - Decode any IANA-labelled OPF/FB2 XML charset (windows-1251, koi8-r, shift_jis, ...) via x/net/html/charset, and wire the charset reader into FB2 parsing which previously had none. - Strip the full .fb2.zip double extension from filename-derived titles and group keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(ebooks): add reader profiles and ruler (#99) * feat(ebooks): extract local covers during scan * fix(ebooks): read nullable poster paths during cover scan * fix(catalog): coalesce nullable media artwork fields * fix(ebooks): group sibling formats by book identity * fix(ebooks): tolerate legacy ebook metadata encodings * fix(ebooks): decode PDF hex metadata strings * feat(ebooks): add reader profiles and ruler * fix(ebooks): address reader ruler and profile review findings - skip renderer setStyles/render when computed styles and attributes are unchanged, so ruler position updates no longer re-style the book view - drag the ruler via a local draft that commits on release, with the surface rect cached at pointer-down - migrate font values persisted before the generic stacks (Inter, Georgia, Merriweather, legacy serif) so the font select never renders blank, with a Custom fallback option for unknown values - make the ruler band click-through and move dragging to a dedicated keyboard-accessible slider handle so links and text selection keep working under the band - share font stacks between options and profiles via READER_FONT_STACKS - surface the active reading profile, move presets to the top of the settings panel, and drop the redundant profile button aria-labels Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ebooks): resolve prefer-const lint error in readest document lib `pnpm run lint` failed on the branch because `direction` is never reassigned in getDirection; split the destructure so only the reassigned `writingMode` stays mutable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Merge branch 'main' into work/ebooks-reader-base Brings the ebook integration branch up to date with main (audiobook library redesign, continue-watching rework and card affordances, quic-go bump, jellycompat fixes). Conflict resolutions favor main's generalized mechanisms and register ebooks with them: - media scope validation goes through IsValidMediaScope (now including "ebook" alongside main's "video" group scope), in Go and in the web filter/search types - continue-watching uses main's typed rails; reading-type sections pull resume points from ebook_reader_progress and the ebook library default section is wired to ContinueTypeConfig(ContinueTypeReading) - item_repo keeps main's derived select-list machinery (itemColumnExpr) and both poster accessors (GetPoster/SetLocalPoster for ebook covers, GetPosterPath for audiobook covers) - web cards/hero/watch-tonight adopt main's buildMediaPlayHref helpers, which now route ebooks to /reader/ebook and encode content ids; ebook affordances (BookOpen icon, Read verb, percent-read subtitle) carry over onto main's reworked components - LibraryForm ebook support ported into main's refactored useLibraryForm/libraryTypes modules Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docker): copy foliate-js vendor into Dockerfile.dev frontend stage foliate-js is a file:vendor/foliate-js dependency, so pnpm install needs the vendor directory before the lockfile install layer. The production Dockerfile already copies it; the dev image was missed, breaking make dev-deploy with ENOENT on /app/web/vendor/foliate-js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ebooks): render Continue Reading sections as upright poster cards All-ebook continue sections previously fell through to the horizontal 16:9 wide card; include ebooks in the poster-variant check so book covers render in their natural 2:3 framing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): stop related-rail highlight ring clipping on detail pages Move the current-item ring onto the cover artwork with a themed ring-offset color (matching the sidebar profile highlight) and give the scroll container top headroom so the ring is not cut off by overflow-x-auto. Applies to both ebook and audiobook detail rails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scanner): harden ebook scanning against data loss and bad metadata - Reconcile missing ebook files like video/audio, with real per-root walk failure tracking (failed/unmounted roots are excluded from deletion), symlinked-root support via the shared logical walker, and the empty-root cleanup allowance before any destructive reconciliation. - Create ebook items as 'pending' so enrichment can promote them to 'matched' (backfill migration included), and protect matched items from re-scan clobbering: title/year skipped, people/series fill-empty only. - PDF metadata: scan head + tail windows (non-linearized PDFs keep the Info dict at the end), require proper key delimiters, head values win. - Cap plain .fb2 reads like .fbz entries; drop .md as an ebook format. - gofmt internal/scanner/audiobook.go (pre-existing drift). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ebooks): make enrichment failures non-terminal with dedicated backoff state - Provider errors now record a failure (capped retries) instead of stamping last_refreshed, which permanently excluded items after transient outages. - Unconfigured metadata chains and the scan-window membership race skip the item without stamping or burning a retry. - Failure tracking moves to a new ebook_enrichment_state table, decoupling it from media_items.refresh_failures (shared with metadata refresh debt). - Preserve non-author people credits when persisting enrichment results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(catalog): gate ebook progress on hidden history and centralize threshold - Apply user_history_hidden_items gating (video semantics) to the ebook watched/in-progress filters, progress sort plan, and Continue Reading. - Continue Reading pages past dismissed items via the shared collector and dedupes items across pages (also fixes the video path's latent exposure). - Centralize the 0.9 finished threshold as models.EbookFinishedProgressThreshold with a single SQL-interpolated mirror in catalog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(recommendations): correct watcher counting and wire ebook taste signals - itemWatchersQuery dedupes to distinct (watcher, item) rows so one binge-watcher can no longer satisfy minWatchers; the eligibility floor now counts distinct accounts rather than profiles. - Hidden-history gating on GetEbookReaderProgressForUser (signal reader). - Ebook reading produces canonical implicit taste signals (weighted like the equivalent movie progress ratio); ebooks join taste-seed candidates. - Stale GetRecentlyAddedItems doc comment corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): harden ebook reader endpoints and serve a Content-Security-Policy - Serve a CSP on all SPA HTML responses: blob/srcdoc book iframes inherit it, so script-src 'self' 'wasm-unsafe-eval' blocks script execution from malicious book content (sandbox alone is defeated by the WebKit allow-scripts requirement). Threat model documented on the constant. - X-Content-Type-Options: nosniff on frontend, jellycompat, and ebook file responses; MIME resolution can no longer fall through to octet-stream for an admitted ebook file. - Annotation PATCH: presence-aware field semantics (absent keeps, present sets/clears), invariant re-validation on the merged row, and an atomic SELECT ... FOR UPDATE read-merge-write. - Request size caps (413) on progress/config/annotation writes; Content-Disposition via mime.FormatMediaType; hidden-history gating in the shared ebook progress lister; FK-cascade indexes for reader tables. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(api): native read-state endpoints for ebooks - POST/DELETE /watched/{id} accepts ebook content IDs: mark read upserts progress 1.0 preserving the reader's file/location (or picks the preferred reader file for never-opened books); mark unread mirrors video unwatch semantics and deletes the progress row. - /history/remove accepts ebooks: hides via user_history_hidden_items without touching the reading position (hidden != unread; next reading activity resurfaces the book, mirroring video re-watch). - Access-filter checks match the video branch; shared logic lives in ebook_read_state.go. Sort metrics/user-state thresholds use the shared constant; profile-header fallback deduplicated. Clients: response is {type: "ebook", affected_count: 1, played: bool}; the existing watched SSE event fires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): harden the ebook reader UI - Open-flow race: cancellation checked after every await with full stale-run teardown (no wrong-file progress saves, no leaked views/blob URLs); book.destroy() on cleanup. - Progress: monotonic stale-response guard; visibilitychange flush uses the refresh-capable client, pagehide uses keepalive; per-book cross-format progress documented as deliberate. - Settings: side effects out of the setState updater; local edits no longer clobbered by late server config; pending saves flushed on unmount/pagehide. - TTS: generation token so Stop actually stops (Chromium/Firefox synthetic events); Media Session uninstalled on unmount. - External book links: http(s) only, opened with noopener,noreferrer. - apiBlob 512 MiB guard with a user-facing error; fraction bookmarks navigable; search-result key collisions fixed; dead e-ink code removed; getLibrarySortRelevanceScope deduplicated; md format dropped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): mark read/unread affordances for ebooks - Item detail gets a Mark Read/Unread button; card menus drop the ebook gate and share type-aware labels/toasts (also dedupes audiobook wording). - Watched-state invalidation includes the reader progress query key so the Continue button and percent refresh after toggling. - Continue Reading dismiss copy for ebooks; dismissal path now URL-encodes item IDs (ebook content IDs can contain reserved characters). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record the PR #124 review and hardening pass 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> |
||
|
|
79c749b9a7 |
Feat/continue watching card affordance (#120)
* feat(web): improve Continue Watching card affordances Add hover/active feedback to the play button (scale, brightness, shadow) and make the episode meta and time-left lines link to the item page like the heading. The blank time-left placeholder stays a plain div so card heights align without an invisible link. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(web): cover audiobook-only continue sections using poster cards Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
46540dfec3 |
fix(progress): track resume points independently of watched state (#117)
* fix(progress): track resume points independently of watched state
Re-watching a finished item never re-entered Continue Watching: completion
latched completed = TRUE one-way, pinned position_seconds to the duration,
and the resume query filtered on completed = FALSE — so a rewatch heartbeat
could never surface the item again (and releasing the latch would have
erased the watched state clients display).
Adopt the Jellyfin invariant instead of guard heuristics:
- Completion resets position_seconds to 0 (UpdateProgress, SetProgress,
SetProgressAt, SetProgressIfNewer, MarkWatched, MarkProgressBatch), so
position_seconds > 0 now means "live resume point".
- completed stays a pure one-way watched latch; rewatch heartbeats re-enter
Continue Watching through plain GREATEST/MAX while the watched flag and
PlayCount survive (matching Plex and Jellyfin master).
- ListProgress("in_progress") keys on position_seconds > 0 in both stores;
the SQLite store also gains the min-resume floor the Postgres store had.
- jellycompat reports Played=true with live PositionTicks during a rewatch
(resumePositionTicks no longer zeroes played items) — the DTO shape real
Jellyfin emits since jellyfin/jellyfin#15762.
- Web mirrors the latch (playbackProgressCache), resumes rewatches at their
stored position, and shows progress bars on rewatched episodes.
- ABS audiobook surfaces keep today's behavior: finished books report 100%
via the completed flag and Continue Listening still excludes them.
- Migrations reset legacy completed rows (position pinned to duration) to
0: a Goose migration for Postgres and a user_version-gated one-time fix
for the per-user SQLite DBs.
Replaces the guard-based approach of #109, whose restart detection
(50% fraction + 60s time gap) could never release the latch for immediate
rewatches (blocked heartbeats refreshed updated_at, re-arming the gap) and
un-watched items on position-0 heartbeats.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(progress): address review — migration gate, one-way latch, missed writers/readers
Review fixes for the position-based watch-progress model:
- The per-user SQLite data fix is now migrateToV11 in the existing
versioned runMigrations chain (schemaVersion 11). The previous
standalone PRAGMA gate compared against 1, but existing DBs already
sit at user_version 10, so the reset never ran for them — and the
gate would have rewound the version. Fresh DBs short-circuit to the
current version as before.
- `completed` is now one-way across every playback/sync writer:
SetProgress (the RecordPlaybackStop path — stopping a rewatch below
the watched threshold no longer clears the watched state),
SetProgressAt, SetProgressIfNewer (both stores), and the history
import upsert, which also stops pinning completed imports to
position = duration. Mark-unwatched still releases the latch via
ClearProgress/ClearProgressBatch.
- MarkProgressBatch regains its freshness guard: a delayed batch mark
carrying an old timestamp can no longer zero a newer rewatch resume
point (the position-reset now rides the original updated_at check).
- Catalog read paths align with the new in-progress definition
(position_seconds > 0, completed-agnostic): smart-collection
in_progress filter, progress sort ratio, episode progress CTE, and
both next-up predicates.
- jellycompat derives PlayedPercentage and PlaybackPositionTicks from
the same clamped position; a played item at rest reports 100 (as the
old model did) while a rewatch reports its live fraction.
- ABS audiobook UpsertProgress stores position 0 on finish so finished
books can't surface as phantom resume entries; re-listens still move
position forward from 0 with the latch intact.
- The web optimistic cache zeroes the resume point on completion,
mirroring the server invariant until the refetch lands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
162e0cc449 |
feat(audiobooks): redesign audiobook library around resume and series progression (#116)
* feat(audiobooks): redesign audiobook library around resume and series progression Audiobook libraries previously reused the video-shaped library page: a backdrop carousel hero (audiobooks have square covers and no backdrops), movie-style default sections, and a browse grid whose primary audiobook axes (author, narrator, series) were buried as filters. Backend: - New next_in_series section type: surfaces the next unstarted book, by series_index, in series the profile has finished a book of, ordered by most recent finish. Registered as a library-staple recipe. - New GET /api/v1/catalog/audiobook-groups endpoint: grouped browse by author/narrator/series with book count, total duration, per-profile progress counts, and poster URLs for cover stacks. - Audiobook library defaults: continue-listening is featured (renders as the Now Listening hero) with next-in-series directly after it. A data migration upgrades existing audiobook libraries, skipping layouts where an admin already featured a section. Frontend: - NowListeningHero replaces HeroBanner for audiobook libraries: resume deck with chapter position, hours left, ambient color from the cover, and one-click resume; remaining in-progress books render as the Continue Listening row. - Library tab gains Books/Series/Authors/Narrators browse axes persisted via the type param; selecting a group drops into the Books grid with the matching filter applied. - "Recommended" tab is labeled "Home" for audiobook libraries; audiobook continue cards use square covers and hr/min time-left formatting. - Shared audiobook chapter/file/duration helpers extracted to web/src/lib/audiobooks (deduplicated from AudiobookContent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(audiobooks): address review feedback on library redesign - Push library scoping into the next-in-series candidate SQL so finished series whose next book lives in another library can't consume the candidate limit and starve a library-scoped section (Codex P2). - Paginate the audiobook groups fetch until the server-reported total is reached (500/page, 20-page bound) so client-side filtering sees the complete author/narrator/series list (Codex P2, CodeRabbit). - Make the redesign migration rollback-safe: rows the Up touches carry config markers (featured_by_migration / seeded_by_migration) and the Down reverts only marked rows, leaving admin-set featured state and hand-created next_in_series sections alone (Codex P2, CodeRabbit). - Gate NowListeningHero's detail-derived files/credits on the detail matching the deck item, so Resume can't start the new book with the previous book's files while keepPreviousData shows stale detail (Codex P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7f8dc95eca |
feat(web): make Continue Watching posters open the item page (#118)
* feat(web): make Continue Watching posters open the item page Clicking a Continue Watching / Next Up poster previously started playback immediately, which made it hard to reach an item's detail page from the home screen. Posters now navigate to the item page and playback moves to an explicit play button layered over the artwork (hover/focus-revealed on desktop, always visible on touch, matching the existing card menu pattern). The card title is now its own link: for episodes it goes to the series page via series_id, for movies to the movie page. Also mocks the playback context in SectionRow.test.tsx, fixing two pre-existing failures unrelated to this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(web): gate hidden play overlay on hover capability, not viewport width Review feedback on the Continue Watching play button: md:opacity-0 hid the button on touch tablets at desktop widths where no hover exists to reveal it, and the visually hidden link still captured clicks at the poster center on desktop. Gate the hidden state on pointer-fine instead of md, and pair it with pointer-events-none so the hidden button cannot intercept poster clicks; hover and keyboard focus restore both visibility and interactivity together. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2933e0581f |
fix(web): make pre-merge frontend checks pass at HEAD (prettier drift + 15 stale tests) (#119)
* style(web): run prettier over drifted files prettier --check was failing on 19 files at HEAD. The pinned prettier version (3.8.1) has not changed since the initial migration, so the drift came from unformatted commits, not a toolchain bump. Formatting changes only, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(web): sync stale test harnesses with current components 15 tests across 6 files were failing at HEAD; the components were committed already out of sync with their tests in the initial migration. All fixes are in test files only: - SectionRow / SeriesContent: mock the watch playback controller required by ContinueWatchingCard and MediaItemMenu, mock the new useSimilarItems hook, add useOptionalAuth to the auth mock, and wrap renders in QueryClientProvider for MediaItemMenu query hooks - SeriesContent: read rating from item.user_rating (the useRating hook no longer exists) - ProfilesSettings: add useHouseholdSessions to the profiles mock for the new HouseholdStreamsPanel - ImpersonationBanner: match the redesigned banner ("Viewing as", sticky full-width bar instead of sidebar offset) - EpisodeRow: watched indicator uses the text-success token now - profile-management: use a valid DiceBear preset ref; "adventurer" no longer resolves and round-trips to "" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2fe2c918f4 |
feat(search): add Media/Audiobooks/All scope with remembered per-user default
Search previously mixed audiobooks into movie/series results with no way to separate them beyond single-type filters. Backend: accept a new "video" group media scope (movies + series) anywhere a media_scope is valid, expanded centrally via MediaScopeItemTypes into the search item-type list, browse comma-list Type filter, and a type = ANY(...) condition in the query executor. Register a user-scoped search.media_scope setting (all|video|audiobook, default video). Frontend: Media / Audiobooks / All chips on the search results page that filter results and persist the choice as the user's default; the global search typeahead follows the same preference. An explicit URL ?type= always wins (with type=all as an unscoped sentinel), and the filter-bar dropdown gains a Movies & Series option. The API surface is additive, so Android/Apple clients are unaffected until they adopt the new scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b9a9613fd0 |
feat(web): redesign library create/edit dialog with sectioned navigation
Replace the single scrolling library form dialog with a Plex-style editor: a left-hand navigation rail with General, Folders, Metadata, and Advanced panes, inline validation that jumps to the offending section, a visual icon-card library type picker, and a sticky footer with Cancel/Save actions. Form state, provider-chain logic, and save mutations move into a shared useLibraryForm hook with reusable field-group components, so the setup wizard keeps its inline layout without duplicating logic. LibraryPosterSection moves out of AdminLibraries.tsx into its own component, and the Advanced toggle cards now use semantic theme tokens instead of hardcoded white overlays that broke light themes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cb6272ef25 |
fix(ratelimit): keep rate limit settings usable when limiter is disabled
The /admin/rate-limits/config routes were only mounted when the limiter was running, and the limiter is only constructed at boot when ratelimit.enabled is true. Disabling rate limiting from the UI and restarting therefore 404'd the settings page permanently, with no way to re-enable it without editing the database. Mount the routes whenever the settings store exists and make the handler tolerate a nil limiter: saves always persist, hot-reload is skipped when nothing is running, and the PUT response reports restart_required (enabling with no limiter built, or switching backend on a running one). GET now exposes active/active_backend so the UI can tell saved config apart from what the process is enforcing. The settings page shows a persistent restart banner driven by that server state, with the same restart flow as other settings pages via a RestartServerButton extracted from SaveBar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
89f005dfa8 |
feat(web): regroup admin navigation and settings into intuitive sections
Reorganize the admin sidebar into Overview / Content / Automation / Users / System so media pipeline tools no longer hide under Users and Settings is not buried in a seven-item Server grab-bag. Group the flat 13-tab admin settings page into Server / Media / Connections / Data sections, and split the Integrations tab into dedicated Subtitles (search providers + AI translation) and Watch Providers (Trakt/Simkl OAuth) pages, leaving Integrations for one-off keys like MDBList. All routes and ?tab= ids are unchanged so deep links keep working. Extract the shared grouped-rail markup used by the admin sidebar, admin settings nav, and user settings nav into a SideNav component, and remove the orphaned admin-settings PluginsSettings page (superseded by /admin/plugins). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2c714e4ef2 |
feat(requests): pluginize request fulfillment behind request_router.v1 (#104)
* docs: design spec for pluginizing requests fulfillment Pluginize the requests fulfillment backend behind an agnostic request_router.v1 capability (high seam: whole-request fulfiller). Host keeps lifecycle/quota/policy/quality-governance and a generic two-tier connection registry; plugins own routing+submission+status. First plugin extracts multi-instance Sonarr/Radarr; Seerr follows in a separate spec. Preserves autoscan reuse of arr connection rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for requests pluginization Three-phase plan: (1) request_router.v1 SDK capability, (2) new silo-plugin-requests-arr plugin extracting multi-instance Sonarr/Radarr, (3) host refactor routing fulfillment through the plugin while keeping quality governance, target records, and autoscan connection reuse host-side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(db): generalize request_integrations into a two-tier connection registry Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): add generic connection fields to Integration + repo mapping * feat(pluginhost): typed RequestRouter capability client + resolver Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): plugin-backed RequestRouterProvider seam Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): route fulfillment through RequestRouterProvider; host keeps quality governance Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): base auto-approve gate on router connection model Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): wire plugin-backed request router at both service sites Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(requests): remove in-host Sonarr/Radarr fulfillment code * test(autoscan): lock request-integration reuse after connection generalization Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): plugin-driven request integration config form Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): echo router connection fields in integration response * fix(requests): retry dropped qualities, contain to one router installation, dedupe targets Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): harden plugin trust boundary (validate targets, contain bad connections, media-type routing) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): tighten auto-approve gate, restore default/4k validation, propagate config-encode error, drop itoa wrapper, test status/options translation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(requests): resolve integrations/settings/secrets once per reconcile cycle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): dedupe config helpers, preserve zero profile id, stabilize installation default, drop redundant options write Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: design spec for schema-driven plugin config form Extends AdminFormDescriptor into a full form-description language (dynamic options, multi-select, conditional visibility, sections, validation) + a plugin Validate RPC, rendered by one reusable SchemaForm engine. Retires the bespoke arr connection form and integrationOptionsFromRouter so any request_router backend renders its config UI from manifest data with zero host changes. Addresses code-review finding #9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for schema-driven plugin config form Six phases: SDK AdminFormDescriptor extensions + Validate RPC; reusable SchemaForm renderer (refactor PluginConfigForm onto it); host Validate plumbing + generic options + legacy-column derivation + retire integrationOptionsFromRouter; requests admin page swap to SchemaForm with per-plugin grouping; arr manifest enrichment + Validate impl; verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): extend plugin admin-form TS types (sections, conditions, validation, multi-select) * feat(web): schema-form pure utils (show_when, validation, value coercion) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): SchemaForm renderer (controls, sections, show_when, dynamic options, errors) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): render PluginConfigForm via the shared SchemaForm engine Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): RequestRouter Validate client + provider seam Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): plugin Validate on save, generic options, derive legacy columns from plugin_config Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): generic options response + 400 field_errors on plugin validation failure * feat(web): generic request-integration options type + surface validation field_errors Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): render request connections via SchemaForm; per-plugin grouping; retire bespoke arr form Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): silent connection-options probe with inline failure status (no toast spam) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): serialize admin_form sections/show_when/dynamic_options/validation to the client Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): drop show_when-hidden fields from buildSchemaValues payload Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): pass requester user id as int64 (no truncation) * refactor(requests): drop legacy arr columns; plugin_config is sole source of truth Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): backfill api key in plugin validate; centralize validation 400; drop duplicate host cross-field check; guard admin-form serializer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): refuse stored api key reuse when base_url changes (security hardening) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): SchemaForm regex-guard, default_value, type-driven coercion, validity callback Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): connection-options latest-wins + narrowed deps + clear stale errors; auto-select; type-driven persist; reuse types Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: design spec for silo-plugin-requests-seerr (request_router.v1 backend) * docs: implementation plan for silo-plugin-requests-seerr * docs(spec): FindExistingRequest uses /api/v1/request (carries request id) * docs(spec): seerr hardening — id-recovery, 404 terminal, media-status, sort pin, single missing-tmdb message * docs: design spec for shared plugin-platform SDK helpers (code-review #10) * docs: plan for plugin-platform SDK helpers (#10) + spec fix (inline broker wiring, no import cycle) * docs: design spec for typed 4K quality-tier signal (code-review #9) * docs: implementation plan for typed 4K quality-tier signal (#9) * feat(requests): stamp is4k per quality (host owns the 4K-tier fact) * fix(requests): store capability sub-id, not the type, in request_integrations request_integrations.capability_id carried the capability TYPE ("request_router.v1") instead of the capability sub-id ("arr"/"seerr"). The host resolves a router plugin via requireCapability("request_router.v1", id), which keys on (type, id), so storing the type resolved to no capability: every save/options/fulfill 500'd ("Request operation failed" / "no fulfillment backend configured") in ~1ms, before the arr/Seerr API was ever contacted. The path was internally split-brained (the fulfillment filter matched the type while the dispatcher needed the sub-id), so it never worked end-to-end; the unit tests hid it behind a fake provider that skips requireCapability. Align capability_id with the scan_source/metadata convention (sub-id): - validateInstance: require a non-empty sub-id; drop the default-to-type and the "!= request_router.v1" reject. - resolveRouterConnections / integrationConfigured / unbound-guidance: match on a non-empty capability, not type equality. - repository: persist capability_id verbatim (never default to the type). - web AdminRequests: send the selected plugin's capability.id in both the options probe and the save payload (was a hardcoded type constant). - migration 20260608131649: backfill capability_id from each bound installation's request_router.v1 capability and drop the column's misleading default. Unbound legacy rows are left for admin re-save. Tests: validateInstance now requires the sub-id, and the selected sub-id must reach the plugin Validate RPC (fakeRouterProvider records it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): polish request connection cards (grouped toggles + option loading states) The schema-driven connection cards rendered each boolean as its own bordered, double-labeled box and showed dynamic SELECTs (root folder, quality profile, tags) as empty controls with a single "Loading options…" line while the host probed the service. - Toggles render as a cohesive settings list: consecutive switches collapse into one bordered, divided container; each row is toggle-first with the label + description hugging beside it (no stranded whitespace between a short label and its switch). Honors show_when, so conditional toggles still group correctly. - Dynamic SELECT/MULTI_SELECT fields show a per-field spinner + shimmer skeleton while options load, and only when there's nothing to show yet — a background re-probe never flashes over the operator's current value. - Sections get a softer surface and clearer titles; the card's enable switch is labeled Enabled/Disabled; the options-load failure is a proper inline alert with retry guidance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): treat "Any"/no-cap playback ceiling as 4K-allowed allowedQualities decided whether to also request 2160p with `CompareQuality(ceiling, PlaybackQuality4K) >= 0`. But an "Any" max playback quality resolves to an empty ceiling ("no cap"), and in qualityRank "" is the LOWEST rank (0) — so CompareQuality("", "2160p") returns -1 and 4K was dropped. A requester with unlimited playback quality only got a 1080p request, never the 4K one. Use access.QualityAllowed(PlaybackQuality4K, ceiling), which already encodes "empty ceiling == no cap == allows everything". Now: - "" / "Any" -> 1080p + 2160p - "2160p" -> 1080p + 2160p - "1080p" -> 1080p only - resolver error still fails safe to the HD ceiling. Tests: add an "any/no-cap ceiling adds 2160p" case; the unknown-quality, status-coercion, dedup, and per-quality-idempotency submit tests now pin an explicit HD ceiling (they relied on the old empty-default == HD-only behavior and were not about 4K entitlement). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: design spec for collapsible Library + anime gate/nesting (request card UI, Spec A) Spec A of two for the request connection card UX: Library section becomes collapsible/collapsed (auto-expanding on validation errors) and the anime override fields move into a single gated section below Library instead of popping out as a detached sibling card. Single-default enforcement is Spec B. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for collapsible Library + anime gate/nesting (Spec A) Task-by-task TDD plan: SchemaForm auto-expand-on-error + nested-field affordance (silo-server), arr manifest regroup (collapsible Library, anime gate section), then build/deploy/reinstall + manual verify. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): auto-expand collapsible schema sections that have validation errors SchemaFormSection now accepts a forceOpen prop; when any field in the section has a mergedError (client validation or server error), the section expands automatically so required-field setup can never be hidden behind a collapsed accordion. The operator's manual toggle is preserved via a nullable userOpen state that only takes effect when forceOpen is false. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): indent show_when-revealed schema fields to read as nested Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: design spec for schema-driven single-default exclusivity enforcement (Spec B) At most one connection per service_kind may be the HD default (is_default) or 4K default (is_default_4k). Generic exclusivity: a new AdminFormField exclusive_group_field declares the rule, the plugin Validate enforces it against host-supplied siblings (config only, no creds), and the admin UI auto-clears conflicts as you toggle. Host stays plugin-agnostic. Forward-only; no migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for single-default exclusivity enforcement (Spec B) Five TDD tasks across 3 repos: SDK proto (siblings + exclusive_group_field) + buf regen; arr Validate cross-sibling + manifest; host gathers siblings (config-only) into Validate; frontend generic mutual-exclusion helper; then re-vendor/rebuild/redeploy + plugininstall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(requests): pass sibling connections to plugin Validate for cross-connection rules Adds siblings []ResolvedRouterConnection to RequestRouterProvider.Validate so the plugin can enforce cross-connection invariants (e.g. one default per service_kind) without the host resolving sibling credentials. The new siblingConnections helper gathers other connections on the same installation, carrying only ID + PluginConfig. Vendor updated to the Task 1 SDK version that carries ValidateRequest.Siblings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(web): auto-clear mutually-exclusive defaults across request connection cards Adds generic applyExclusivity helper and wires it into updateCardConfig so turning on a field with exclusive_group_field proactively clears the same field on sibling cards sharing the same group value, matching server-side enforcement with a proactive UX. * docs: design spec for single-flighting plugin client launch (cold-start herd fix) Concurrent ensureClient calls for a cold installation each spawn a redundant plugin process (Host.Start releases its lock during launch). Wrap ensureClient in a per-installation singleflight.Group so concurrent first-use collapses to one launch. Host-only fix; surfaced while testing the request-router feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for single-flighting plugin client launch TDD: concurrency tests (herd collapses to one launch, warm-cache reuse, distinct installations stay parallel, failed launch propagates) + the singleflight wrapper around ensureClient; then rebuild/redeploy + verify. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins): single-flight ensureClient to prevent cold-start launch herd Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(requests): harden capability containment + dedupe eligibility; UI/migration cleanups Addresses /code-review high findings on the previously-unreviewed commits: - resolveRouterConnections contains fulfillment to the first chosen (installation, capability) and locks only after a connection's key resolves, so a plugin exposing >1 request_router capability never mixes connections and a skipped bad-key connection never pins the capability (+ test). - extract eligibleRouterConnection, shared by resolveRouterConnections and integrationConfigured so the auto-approval gate and fulfillment filter can't drift. - SchemaForm: shared FieldDescription helper (field/switch/section); key switch groups by position so a show_when reveal doesn't remount the group (focus loss). - migration backfill uses a deterministic correlated subquery instead of a join cross-product when an installation exposes multiple request_router capabilities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: design spec for opt-in Seerr per-user requester mapping Per-connection requester_mode (admin default | mapped). In mapped mode the host pushes the requester email/username into the Fulfill descriptor and the seerr plugin resolves/creates the matching Seerr user by email with operator-chosen default permissions, attributing the request (and gating Seerr-side approval via the auto-approve permission). Spans SDK (descriptor fields), host (extend UserIdentityLookup with email + a requester resolver), and the seerr plugin (Seerr user API + mapping). Fallback to admin on any failure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for Seerr per-user requester mapping Five TDD tasks across 3 repos: SDK descriptor fields (requester_email/username); host resolves identity (UserIdentityLookup+email, RequesterIdentityResolver, populate descriptor at both Fulfill sites); seerr config+user API (find/create by email, exported PermissionBits); seerr Fulfill mapping + admin_form; then re-vendor/rebuild/redeploy + plugininstall (installation 6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: make Seerr unmapped-requester behavior a toggle (admin fallback | fail request) Per user feedback: require_mapped_user switch (default off = admin fallback, on = fail the request). Updates spec + plan Tasks 3/4 (config field, Fulfill honoring the toggle via a mapFailed signal, a new test, and the manifest switch). * feat(requests): resolve requester email/username into the Fulfill descriptor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: design spec for simplified Seerr mapped-user permissions Reduce the 5 permission toggles to two (request_4k_all + auto_approve); 1080p always granted; remove manage_requests; 4K eligibility per-user from the request's qualities (host-decided, same as arr) with a blanket override toggle. Seerr-plugin-only; permission-only override (host still gates 4K requests). * docs: implementation plan for simplified Seerr mapped-user permissions Two tasks (seerr-plugin-only): replace the 5 perm toggles with request_4k_all + auto_approve (1080p always; 4K from request qualities via userPermissions; remove PermManageRequests/PermissionBits; manifest + json_schema), then rebuild + reinstall (installation 6). No host/SDK change. * docs: design spec for host rebase onto main + #95 credential-model adoption Per-commit rebase of our 68 request-router commits onto the force-pushed origin/main (drops 188 patch-equivalent). At the credential-path conflicts, adopt #95's inline secret.Cipher model: keep our plugin columns + #95's encrypt/decrypt in repository.go; drop our SecretResolver and read in.APIKeyRef directly in service.go; wire NewRepository(pool, dataCipher). #39-area conflicts take ours (our pluginization supersedes it). Security review + SECRET_KEY deploy note. * docs: implementation plan for host rebase + #95 credential adoption Four tasks: (1) guided per-commit rebase onto origin/main, take-ours on credential files so it builds; (2) TDD integration commit adopting #95's secret.Cipher (encrypt/decrypt in repository.go, drop SecretResolver, read APIKeyRef directly, wire NewRepository(pool, cipher)); (3) security review; (4) pin published SDK v0.6.0, push fork, open host PR with SECRET_KEY deploy note. * chore(rebase): restore scan-source service methods + temp requests-repo arity Post-rebase conflict fixups: take-ours on internal/plugins/service.go dropped origin's ScanSourceClientByPluginID (independent upstream capability) — restored. mediarequests.NewRepository temporarily 1-arg to match our pre-#95 repo; Task 2 restores the cipher arg when adopting #95's at-rest credential model. * feat(requests): adopt at-rest credential cipher (#95) for plugin api keys; drop SecretResolver * build: pin published silo-plugin-sdk v0.6.0 (drop local replace) * test(requests): guard at-rest cipher round-trip + empty-key auto-approval (code-review) Max-effort code review of the #95 credential integration. Fixes the actionable findings: - TestEncryptAPIKeyRoundTripAndAAD: pins encryptAPIKey<->DecryptIfEncrypted inversion, the id-bound apiKeyAAD == secret.RowAAD(...) match (so #95's backfill rows decrypt), the blank-key "" sentinel, and row-bound AAD — the security- critical invariants had no automated guard (no DB harness for scanIntegration). - TestCreateRequestAutoApprovalEmptyKeyTreatedAsUnconfigured: pins that a keyless connection reads as unconfigured (request stays pending, never submitted), so integrationConfigured and resolveRouterConnections can't drift. - Fix stale fulfillContext comment (referenced a resolved-API-key cache removed with SecretResolver). Assessed-not-changed (documented): decrypt-error-fails-closed and failed-backfill behaviors are origin/main #95 design we adopt; nil-cipher is unreachable in prod and matches the codebase-wide no-guard pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: drop stale machine-local SDK replace comment from go.mod The replace directive was already removed when v0.6.0 was pinned (3410df7); this leftover comment falsely claimed a local replace still existed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): prettier-format schema-form utils to 100-col width Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: drop internal superpowers specs/plans from PR These design specs and implementation plans are internal development artifacts; keep them out of the upstream PR diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(metadata): exclude providers from content levels they don't declare ResolveChain falls back to every enabled metadata provider when a library + content-level has no enabled chain entry. That fallback was media-type blind: a provider declaring default_priority only for an unrelated level (e.g. an audiobook provider declaring {"audiobook": N}) was kept in the list (merely sorted last) and invoked for video content levels. In production this made silo.audiobook-metadata hammer external audiobook APIs with anime/movie/series titles every scheduled enrichment pass (MatchWorker, 30s) for the season/episode levels that had no enabled chain entry. Disabling the chain entries did not help because the fallback never consults them; only disabling the installation removed it from the global set. Treat a non-empty default_priority map as the provider enumerating the content levels it supports: in resolveEnabledProvidersByPriority, exclude providers whose declared map omits the requested level instead of ranking them last. Providers that declare no default_priority make no claim and stay eligible everywhere (legacy behavior). Fixes #105 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins): isolate singleflight launch from leader ctx cancellation The deduped ensureClient launch ran doEnsureClient under the leader caller's ctx, so if that caller's request was canceled/timed out mid-launch the shared plugin start was torn down and the error propagated to every waiter. Run the launch under context.WithoutCancel so a single caller cannot cancel work the other waiters depend on (values preserved for tracing/auth). (CodeRabbit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): nil-guard request-router wiring RequestRouterClient dereferenced a.Svc unconditionally and AttachRequestRouter called SetRouterProvider even with nil deps, so a build without the plugin service would panic instead of degrading. Guard both: the adapter returns a controlled error and AttachRequestRouter no-ops, leaving fulfillment to fail with the existing "no backend configured" path. (CodeRabbit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): correct value coercion + track capability sub-id in request form - schemaForm: Boolean("false") was true; parse string booleans explicitly. array:num now coerces decimals ("1.5"), array:int stays integer-only. - AdminRequests: track capability_id alongside installation_id (composite <Select> value) so a multi-capability installation resolves the exact backend; reset pluginConfig when the selected plugin changes so plugin A's keys never reach plugin B's options probe/save. (CodeRabbit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(requests): address request-router review findings * fix(requests): handle router review edge cases * fix(web): resolve schema form build casing * fix(requests): skip unconfigured 4k fulfillment targets --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
5855cebe75 |
fix(audiobooks): complete playback and catalog parity (#96)
* fix(audiobooks): complete playback and catalog parity * fix(web): allow podcast continue targets * fix(audiobooks): use folder sidecar covers during scan * fix(audiobooks): read nullable poster paths during cover scan * feat(home): split continue listening sections * fix(catalog): coalesce nullable media artwork fields * feat(playback): surface audiobook sessions in admin activity * fix(audiobooks): address playback review feedback --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
65ea9b651c |
fix(web): drop backdrop-blur from Pill card overlay preset (#89)
The Pill overlay preset was the only one of the five to carry `backdrop-blur-sm` (backdrop-filter: blur(4px)) on its badge class. Each enabled badge renders its own element, so a typical library grid produces hundreds of backdrop-filter layers the browser must re-sample and blur on every repaint — including every scroll frame, since the poster art behind each badge moves while scrolling. This caused severe scroll/interaction jank, most visibly on Chrome on Windows. The blur is also effectively invisible: the Pill background is already rgba(20,20,30,0.7) (70% opaque), so removing it has no meaningful visual impact while eliminating the per-frame GPU cost. The other four presets are unaffected. |
||
|
|
978e1b4954 |
feat(playback): NVENC support for transcoding (#79)
* feat(playback): NVENC support for transcoding * fix(playback): probe nvenc before auto-selecting * fix(playback): use safe NVENC smoke probe dimensions --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
c4e67effc3 |
fix(admin): correct dashboard library counts and scans (#78)
* fix(admin): correct dashboard library counts and scans * fix(admin): handle mixed library dashboard counts * fix(admin): refine dashboard count affordances |
||
|
|
6b1554a86c |
Fix scoped catalog refresh invalidation (#51)
* Fix scoped catalog refresh invalidation * Remove duplicate catalog refetch * fix(web): keep library lists fresh on scoped catalog events --------- Co-authored-by: RXWatcher <14085001+RXWatcher@users.noreply.github.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
480ca44306 |
fix(recommendations): improve taste seed cold start ranking
Closes #66 |
||
|
|
f9bb94a299 |
[codex] Fix autoscan plugin bindings and poll status (#75)
* fix(autoscan): bind sources by plugin id * fix(autoscan): skip overlapping source polls |
||
|
|
eb6024573e |
feat(audiobooks): make audiobook libraries first-class catalog items (#73)
* docs(audiobooks): design spec for plugin absorption Plan to absorb silo-plugin-audiobooks into silo-server as a first-party feature. Audiobooks land in silo's existing SPA; ABS clients connect directly. Hard constraints: reuse existing tables (media_items, media_files, user_watch_progress, user_playback_sessions, people, item_people, library_collections); only two new tables (abs_sessions, podcast_feeds) and at most one column add (media_libraries.kind); silo's main :8080 listener handles ABS Socket.io natively. Out of scope: audiobook requests flow, smart collections, share links, external recommender, custom metadata providers, separate audiobook SPA. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(audiobooks): implementation plan sub-plan 1 (discovery + schema) First of six sub-plans for the absorption. Six tasks: a discovery audit that resolves the spec's Risk questions, four idempotent SQL migrations (abs_sessions, podcast_feeds, media_libraries.kind, audiobooks.enabled feature flag), and an empty-but-compiling internal/audiobooks package scaffolded into cmd/silo. Lands as a strict no-op for users (feature flag defaults to false). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(audiobooks): discovery findings for absorption sub-plan 1 Locks schema/code decisions for migrations 139-142 and downstream sub-plans. Resolves open Risk questions from the absorption design spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): migration 139 add abs_sessions table Parallel of jellycompat_sessions for Audiobookshelf-compatible clients. Lets ABS mobile/desktop apps maintain a device-bound session that silo's audiobooks/abs handlers will validate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(audiobooks): match codebase conventions in migration 139 Lowercases type keywords in the abs_sessions CREATE TABLE body to match neighboring migrations, fixes the client_version column alignment, and replaces the misleading "parallel to jellycompat_sessions" header comment with a more accurate description of the table's role. Cosmetic only — the running schema is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): migration 140 add podcast_feeds table Side table on media_items for RSS-subscribed podcasts. Holds feed URL, ETag/Last-Modified for conditional fetches, last-refresh timestamp, and the per-feed refresh interval consumed by the upcoming podcastfeed.Refresher scheduled task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(audiobooks): uppercase PRIMARY KEY in migration 140 Aligns with the codebase convention (type keywords lowercase, constraint keywords uppercase) established in migration 139's post-style-fix form. Cosmetic only — running schema is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(audiobooks): migration 141 no-op for media_folders.type Sub-plan 1 originally reserved migration 141 to add a 'kind' column to media_libraries discriminating audiobook/podcast libraries. Discovery audit (sub-plan 1 Task 1) found that the actual table is media_folders and it already has a type text NOT NULL column with no CHECK constraint or enum, so 'audiobooks' and 'podcasts' can be added as future values without DDL. Landing this migration as a documented no-op preserves the version numbering audit trail and pins the decision in git history. The matching down migration is also a no-op. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): migration 142 add audiobooks.enabled flag Server-settings row that gates the absorbed audiobooks feature. Defaults to 'false' so sub-plan 1 lands as a strict no-op; subsequent sub-plans branch on this flag and operators flip it to 'true' at cutover. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): scaffold internal/audiobooks package Empty-but-compiling Service that reads the audiobooks.enabled feature flag from server_settings. Wired into cmd/silo so the package is referenced from the binary; no routes mounted, no scheduled tasks registered, no DB writes. Subsequent sub-plans hang scanner branches, ABS handlers, Socket.io, podcast refresher, and SPA pages off this Service. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(audiobooks): cosmetic cleanups in scaffolded package Two pre-emptive cleanups flagged by code review before sub-plan 2 copies the patterns: 1. Sort the internal/audiobooks import after internal/adminjob in cmd/silo/main.go (alphabetical). 2. Drop the redundant "audiobooks: " prefix from the Enabled() error wrap; matches how every other top-level service package (watchstate, scanqueue, metadata, etc.) formats errors. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(audiobooks): implementation plan sub-plan 2 (scanner) Second of six sub-plans. 10 tasks: PersonKind constants for Author and Narrator, audio-extension recognizer, library-type helpers, a walkLogicalTree refactor (movieLibrary bool -> typed walkMode), chapter extraction via ffprobe, single-file and multi-file audiobook parsers, scanner write path producing media_items.type='audiobook', and a filesystem podcast parser (RSS deferred to sub-plan 5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): add Author and Narrator PersonKind constants Discovery audit confirmed item_people.kind is unconstrained smallint with values 1-6 in use. Reserve 7 = Author, 8 = Narrator for audiobook people-links written by the upcoming scanner branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): add audio-extension recognizer for scanner Mirrors the existing videoExtensions/SupportsVideoFile pair. Used by upcoming audiobook and podcast scanner branches to filter directory walks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): library-type recognizers for scanner dispatch isAudiobookLibraryType and isPodcastLibraryType match singular and plural forms case-insensitively, mirroring isMovieLibraryType. Used by upcoming scanner walk branches (Task 4) that filter audio files into audiobook and podcast libraries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(scanner): replace movieLibrary bool with typed walkMode Lets walkLogicalTree dispatch on multiple library shapes (video, movie, audiobook, podcast) without proliferating boolean flags. Behavior for existing video and movie libraries is unchanged; audiobook and podcast modes will be consumed by the upcoming audiobook.go and podcast.go parsers in later tasks of this sub-plan. walkModeFor() derives the mode from a media_folders.type string; unknown types default to walkModeVideo to preserve prior behavior for any caller still passing a raw type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): expose ffprobe format tags on ProbeData The audiobook scanner needs format-level tags (title, artist, album, date) for media_items metadata; ffprobe already parses them in ffprobeFormat.Tags but ProbeData previously discarded them. Add FormatTags map[string]string to ProbeData, populate it in convertProbeData via a new normalizeFormatTags helper that lowercases keys and trims values. Adds a fixture audiobook .m4b with embedded chapters (Intro/Outro) and format tags, and a test that verifies ProbeFile() returns both correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): parser for single-file audiobook folders parseAudiobookFolder reads tags + chapters via the existing ProbeFile (now that Task 5 exposes FormatTags on ProbeData) and produces a parsedAudiobook struct. Title falls back from "title" tag to "album"; author from "artist" -> "album_artist" -> "composer"; series from "album" -> "series" -> "mvnm" (Movement Name, used by some MP4 tools). Year parsed from "date" or "year" tags, tolerating ISO dates and parenthesized forms. Single-file case only; multi-file folders (one audio file per chapter) return a placeholder error and arrive in Task 7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): multi-file audiobook folder support Folders containing N audio files (one per chapter/part) get one parsedAudiobookFile per file; each file's chapter list is synthesized as a single chapter with title = filename stem. Title/author/series/ year come from the first file's tags. Also drops the duplicate pickFirstNonEmpty helper added in Task 6 in favor of the existing firstNonEmpty already in probe.go. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): scanner write path produces audiobook media_items ScanAudiobookFolder walks an audiobooks-typed media folder and treats each immediate subdirectory as one audiobook. For each parsed audiobook it upserts: - one media_items row with type='audiobook' - one media_files row per audio file (with chapters JSONB) - author/narrator links in item_people (kind=7, kind=8) Adds itemRepo and personRepo to the Scanner struct, wired from fileRepo.Pool() in NewScanner — no constructor signature change needed. ScanFolder dispatches to this path when folder.Type='audiobooks', bypassing the per-file movie/TV pipeline because audiobooks are folder-scoped entities. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): filesystem podcast scanner ScanPodcastFolder walks a podcasts-typed media folder, treating each subdirectory as a podcast show and each audio file inside as an episode. Writes media_items.type='podcast' + episodes rows + media_files rows. RSS-subscribed feeds (podcast_feeds table) arrive in sub-plan 5; this task covers filesystem-only ingestion. ScanFolder dispatches to this path when folder.Type='podcasts'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(audiobooks): implementation plan sub-plan 5 (podcasts) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(audiobooks): expose audiobooks/podcasts library types in admin UI Adds 'Audiobooks' and 'Podcasts' options to the library-type dropdown in the admin libraries page so operators can flag a folder as an audiobook or podcast library. Extends contentLevelsForType() so the admin UI's downstream filtering treats those types correctly (audiobook -> ['audiobook'], podcasts -> ['podcast', 'podcast_episode']). Backend scanner branches for these types were already wired in sub-plan 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(migrations): renumber 139_abs_sessions to 147 for origin/main merge origin/main adds 139_media_requests at the same number our local audiobook branch had used for abs_sessions. Renumber ours to 147 to free up 139 for the upstream migration. The schema_versions row is updated in lockstep on the running database so the migrator sees the abs_sessions migration as already applied at its new version. Migrations 140-146 (podcast feeds, media_folders kind noop, audiobook feature flag, abs playback sessions, podcast episode guid, audiobook series, audiobook title cleanup) stay where they are — they don't collide with anything on origin/main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(migrations): renumber 140_podcast_feeds to 157 for origin/main merge origin/main added 140_user_permissions at the same version this branch had used for podcast_feeds. Renumber ours to 157 (next free above the collections-unify migration at 156) so 140 is free for the upstream migration. schema_versions on the running database is updated in lockstep so the migrator sees podcast_feeds as already applied at its new version. Same pattern as |
||
|
|
c469550fa0 |
fix(libraries): collapse admin library diagnostics (#61)
* Fix troubleshooting visibility on admin libraries * Remove metadata matcher badge from admin libraries * test(libraries): cover hidden matcher queue badge |
||
|
|
09204687fd | feat(requests): link available library items | ||
|
|
1fb09efc9b | fix(requests): remove progress-like card bars | ||
|
|
064963f45f | fix(home): remove watch tonight button |