1dbcf2cd9ae26b176a4b90d8eb4ec1dfd2a73dd3
43
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
443445f0fe |
fix(catalog): make media_items scalar columns NOT NULL to stop NULL-scan crashes (#228)
media_items had many columns the Go model (models.MediaItem) declares as non-pointer string/int fields, but migration 001 left them nullable. Five scanners (catalog item_repo, catalog browse, jellycompat, sections, and the catalog API handler) read these straight into the non-pointer fields, so a NULL row panics with "cannot scan NULL into *string" (or *int). item_repo papers over a subset (poster/backdrop/logo/metadata paths) with COALESCE in its SELECT, but the other four scanners read the same columns raw and crash; sort_title/original_title/etc. are not coalesced anywhere. No writer stores a meaningful NULL (every insert/upsert passes the Go field, '' or 0 at worst) and all sort/filter SQL already collapses NULL and '' (e.g. COALESCE(NULLIF(BTRIM(sort_title), ''), title); "poster_path IS NULL OR poster_path = ''"). Enforce the invariant the code already assumes at the schema level rather than scattering COALESCE across every current and future scanner. Mirrors what later migrations already did for original_language, show_status, default_metadata_language, and the *_source_path columns (all NOT NULL DEFAULT ''). - Migration: backfill existing NULLs, then NOT NULL DEFAULT '' on 15 text columns (sort_title, original_title, content_rating, overview, tagline, imdb_id/tmdb_id/tvdb_id, poster_path, poster_thumbhash, backdrop_path, backdrop_thumbhash, logo_path, metadata_s3_path, metadata_etag) and NOT NULL DEFAULT 0 on year/runtime. - item_repo: SetLocalPoster and UpdateArtworkIfSourceMatches now store '' for empty thumbhashes (was NULLIF($,'')) — the only deliberate NULL writers — matching the upsert path. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
02e62767a1 |
feat(watchsync): sync watchlists with Trakt/Simkl/MDBList (#227)
* feat(watchsync): sync watchlists with Trakt/Simkl/MDBList Extend the watch-providers feature to sync a user's watchlist, generalizing the existing favorites pipeline rather than duplicating it. What changed - Generalize the favorites sync into one ListKind-parameterized pipeline (internal/watchsync/lists.go) driving both favorites and watchlist; the per-favorites service methods are replaced by kind-generic ones. The shadow table watch_provider_favorite_items becomes watch_provider_list_items with a list_kind discriminator. - Providers: Trakt gains watchlist sync (/sync/watchlist, distinct from favorites); Simkl gains plan-to-watch sync; MDBList is re-mapped from favorites to watchlist (its only list is a watchlist) — its capabilities now report import_favorites=false / import_watchlist=true, and the migration re-binds existing MDBList connections. - Auto-remove watched items from the watchlist: a standalone, default-on profile preference (user_profiles.remove_watched_from_watchlist) removes a movie when watched and a series once every episode is watched. Implemented as watchstate.CompletionObserver (internal/watchlist.Maintainer), wired into the manual mark-watched, playback-stop, and jellycompat mark-played paths. - Optional MDBList sort-order mirroring: an opt-in, capability-gated toggle mirrors MDBList's watchlist order into Silo via user_watchlist.sort_index; ListWatchlist orders by sort_index then added_at, so both /api/v1/watchlist and the catalog watchlist view inherit it. - Real-time + scheduled: local add/remove pushes to connected providers immediately (removals gated by the opt-in removals toggle); the hourly job is the inbound/import + retry/reconcile path. - Web: watch-provider settings gain watchlist import/export/removals and "mirror watchlist order" toggles plus watchlist sync stats. Why - The favorites and watchlist pipelines are ~90% identical; generalizing keeps one code path (per CLAUDE.md's anti-duplication guidance) instead of cloning. API/compat - All new fields on ConnectionStatus/Capabilities/ConnectionUpdate/SyncRun and the web types are additive (Silo v1 additive-only rule). No existing field is renamed, removed, or retyped. Risks / follow-up - MDBList capability flip is intentional and client-visible: silo-android / silo-apple may need to surface MDBList under the watchlist (not favorites) UI. - MDBList existing users: their MDBList list previously mirrored Silo favorites and now mirrors Silo watchlist; the first post-migration sync is a union (removals default off), so nothing is destructively purged. - Order mirroring reflects the order MDBList returns from /watchlist/items (couldn't confirm against their docs — Cloudflare-blocked); if it ever diverges from the UI sort, a sort param is the small follow-up. Tests: new maintainer (auto-remove) and watchlist-order unit tests; provider + service tests updated. go build, go test (affected pkgs), migrate-validate, verify-local-paths, web prettier/eslint/tsc all pass. AI-use disclosure: implemented with Claude Code (Claude Opus 4.8). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(watchsync): update list shadow table references * fix(watchsync): address review — retry/progress + error propagation Addresses CodeRabbit review on #227: - maintainer: propagate transient catalog lookup errors instead of silently treating every items.GetByID failure as "maybe an episode". - exportList: mark every queued item not confirmed sent (not_found, failed, or omitted) so the pending loop always advances; the next run's upsert clears the error and re-attempts, so transient failures still retry. - removePendingListItems + realtime removal: treat Sent and NotFound as reconciled; leave true failures pending (no last_error, which would strand them from the removal query) so the scheduled run retries, using in-memory dedupe to terminate the loop. - exportLocalListItems: send the normalized items (with computed ProviderItemKey), not the original event slice. - UpdateConnection: clear mirrored watchlist order before persisting the disable and propagate failures, so a failed clear can't report "disabled" while sort_index ordering is still active. - web: include favorite + watchlist removal counts in the exported "sent" total. - test: align serviceFakeRepo list-state with Postgres (clear last_error on successful transitions); add maintainer error-propagation test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7f67bb80ad |
Fix semantic-disabled Meilisearch settings
- Omit Meilisearch embedders unless semantic search is enabled - Default semantic ratio setting to 0.50 |
||
|
|
927d7764a4 |
feat(search): model-filtered embed-eligible coverage counts
Establish how semantic vector "coverage" is counted: current-model embeddings over embed-eligible items, per media type, from Postgres. - Move the embed-eligibility predicate to a single source of truth in embeddingvectors.ItemEligibilityWhereClause; recommendations now delegates to it (output byte-identical). - Add catalogSemanticCoverageByType (per-type eligible/vectorized) and define the coverageQuerier interface in catalog. Both numerator and denominator apply the eligibility predicate, so vectorized never exceeds eligible (C1 guarantee) even with a stale embedding on a now-unmatched item. - countCatalogSearchVectorDocuments now takes a coverageQuerier and a model, applies the eligibility + model filter, and is the per-type numerator summed across types. Update all four callers (pass ""). - Add idx_media_item_embeddings_model (CONCURRENTLY, self-healing guard) so the model filter does not scan the embeddings table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
07f2dd5a8f | fix(admin): bound autoscan activity backlog | ||
|
|
40329f616d | perf(search): speed up catalog query results | ||
|
|
6ca427096b | Add catalog search provider support | ||
|
|
5c0027b54b | fix(metadata): dedupe availability during re-id | ||
|
|
87159b0a38 |
feat(collections): add profile-scoped display filters (#191)
* feat(collections): add profile-scoped display filters
* refactor(collections): dedup display-filter helpers per review
Address code-review feedback on the profile-scoped display filters
without changing behavior:
- Widen CompletedHistoryItemMap to accept ProgressCompletionStore and
drop the duplicate completedHistoryItemMapForProgress copy.
- Extract the duplicated MDBList candidate retry loop into a generic
collectionutil.FetchMDBListWithFallback helper, used by both the user
and library collection syncers, and cover it with unit tests.
- Reuse validateOptionalLibraryIDs in HandleUpdateCollection instead of
an inline positive-ID loop.
- Import the shared COLLECTION_{WATCH,MEDIA}_FILTER_OPTIONS in the
template config form rather than redefining them locally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(collections): sanitize query_definition library_ids fallback
readSourceConfigLibraryIDs validated source_config.library_ids (finite,
positive, truncated, deduplicated) but returned the query_definition
fallback raw, so legacy rows could surface zero/negative/duplicate IDs
that the backend now rejects on save. Extract a shared sanitizer and
apply it to both paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(docs): This makes the agents annoying to work with
* Improve playback session handling
* Support collection source order in catalog filters
* fix(collections): address display filter review feedback
* refactor(catalog): remove duplicate collection query params
* Hide episode media scope for collection overlays
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
227986094b | Propagate playback client metadata through session sync | ||
|
|
729468be30 | fix: wrap episodes season index migration block | ||
|
|
7e532ea982 |
[codex] fix season page loading (#178)
* fix(catalog): speed up season page loads * fix: address season loading review feedback |
||
|
|
14ffc91dfb |
[codex] Expand provider image cache queue (#176)
* feat(metadata): expand provider image cache queue * fix(metadata): harden provider image cache queue Addresses bug-review feedback from Codex/CodeRabbit on the metadata image cache pipeline. All findings validated against the code before fixing; false positives (rows/connection deadlock, PhotoSourcePath merge coupling) were confirmed non-issues and left unchanged. - Honor metadata.cache_images for the background processor. The cache_metadata_images task was registered whenever S3 was configured, so merely enabling object storage downloaded the entire provider-artwork catalog even with caching disabled. Add ImageCacheProcessor.SetEnabled, gate RunOnce/RunUntilIdle on it, and wire it (with hot reload) from cfg.Metadata.CacheImages in main.go. - Guard terminal job updates with lease ownership. EnqueueBatch can repurpose a running row with a new source; MarkSucceeded/MarkFailed keyed on id alone let a stale worker finalize the replacement job and drop the new artwork. Thread locked_by through and add status='running' AND locked_by=$n guards. - Avoid uploading stale jobs onto the live artwork key. Verify the target still references the job's source (CurrentTargetSourcePath) before CacheImage, so a job whose source an admin/refresh already replaced cannot overwrite the deterministic storage object. - COALESCE nullable external IDs in EnqueueExistingProviderArtwork. A NULL tmdb_id/tvdb_id/imdb_id on any candidate failed the scan and aborted the whole cache run; matches the existing item_repo pattern. - Stop re-downloading the catalog every 30 days. Discovery now skips targets whose *_path is already a cached relative path, making the cached row the durable dedup marker instead of the prunable job row. - Decouple catalog sweeps from queue draining. RunOnce no longer runs discovery per batch; RunUntilIdle sweeps only when the queue drains and throttles full sweeps to every 15m, so idle installs stop full-scanning every entity table each minute. - Requeue claimed-but-unstarted jobs on cancellation. Acquire the semaphore before spawning workers and RequeueClaimed any jobs not yet started, instead of leaving them locked until the 15m lease expires. - Skip the backoff sleep after the final upload attempt in putObjectWithRetry (saves ~1.5s on permanent failures). - Add the s3/file/local/upload/generated exclusion to the seasons and episodes backfill in migration 20260617184537 for consistency with the later migration (the bad backfill was inert downstream, but the asymmetry is removed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
562ae635d7 | feat: improve audiobook groups and notification refresh | ||
|
|
20e0dd7fe2 |
Fix Jellyfin parent browsing and release dedupe
- List seasons or episodes correctly for Jellyfin parent item queries - Deduplicate episode availability by logical episode identity - Improve audiobook title fallbacks and existing item updates |
||
|
|
9b111649f3 |
perf+fix(audiobooks): detail page, browse, sessions & scanner (#169)
* perf(catalog): fix audiobook detail N+1 + slow people facets Audiobook detail pages were slow in proportion to track count (up to 433 files/book). Root causes, found by EXPLAIN ANALYZE on the live DB: - effectiveAudioSelection ran 3-4 user-store queries (profile, audio pref, library pref) per file inside buildPlaybackInfo's loop, though the results are invariant across a request. Introduce a request-scoped audioPrefResolver that memoizes the store lookups (library prefs keyed by folder); a 400-file audiobook now issues each query once instead of per file. Selection logic is unchanged (audioPreference returns a copy so the original-language sentinel is still resolved per file). - buildAudiobookExtension ran its four independent related-content queries serially; run them concurrently so latency is the slowest, not the sum. - author/narrator browse facets did a full people-table scan; add a (kind, content_id, person_id) index so the facet resolves from an index-only scan of just that kind's credits (~112ms -> ~49ms on the live library). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(catalog): cache audiobook author/narrator group browse The Authors/Narrators audiobook pages were slow on cold load and slow again after a hard refresh (fast only while the React Query client cache was warm). Root cause (EXPLAIN ANALYZE on live, 31K-audiobook library): the grouped browse query is ~234ms/page, there are ~13K distinct authors, and the client pages through the entire list on every load (sequential 500-row requests). With no server-side cache each of the ~20 pages re-ran the full aggregation (COUNT(*) OVER() forces it), so a cold load was ~20x234ms. The client's 60s staleTime was the only thing making a warm revisit fast; a refresh wiped it. Fix: AudiobookGroupsCache caches the full sorted group list per (library, group_by, sort, viewer) for 60s (matching the client staleTime, so no extra staleness) and serves every page as an in-memory slice — one aggregation per window instead of one per page, and a refresh is a cache hit. Also raise the client page size 500->2000 so fewer sequential round-trips are needed now that a larger page is a cheap slice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(settings): throttle per-request device last_seen upserts Device-setting reads (HandleGetDeviceSetting, HandleGetEffectiveSettings, HandleGetEffectiveSubtitleAppearance) each registered the request's device — an INSERT ... ON CONFLICT upsert of last_seen_at on a single per-device row. A page that fetches many settings fired hundreds of these concurrently; they serialized on that row's lock (observed 100-237ms each, ~250 per page load in the slow query log), taxing every settings fetch. Throttle device registration to one upsert per (profile, device) per 5 minutes via an in-process TTL cache, marking the device seen before the upsert so a concurrent burst collapses to a single write. last_seen_at stays fresh to within the window. Reads no longer issue a contended write on the hot path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(audiobooks): probe-repair, resume position, cache storm, groups reveal, hot-row + stats index From the full audiobook code review (EXPLAIN + slow-query trace on live): - #1 (P0, detail-page killer): NeedsCriticalProbeRepair required video codec/ resolution/tracks, which audio-only files never have, so PlaybackProbeEnsurer re-ran ffprobe per file on every detail/watch load (up to N serial spawns for an N-track book) and never converged. Gate video-field checks on the file actually having a video stream. TDD. - #3 (P0): abs session-sync rewound the resume cursor — UpdateProgressPosition did an unconditional SET with no monotonic guard, ignored its error, and no-op'd when no row existed (first-listen resume lost). Now a finish-preserving GREATEST upsert; caller logs failures. - #4 (P0 perf): progress reports fired every ~10s invalidated all of catalogKeys.all → refetched every active browse/detail query incl the 13k audiobook group lists. Scope invalidation to the reported item's detail. - #6 (P1 perf): Authors/Narrators page rendered all ~13k groups + cover images at once (main-thread freeze). Incremental reveal: render a capped window, grow on scroll via IntersectionObserver. - #10: throttle abs TouchToken last_seen upsert (one per token per 5min) — same hot-row contention class as the device fix. - #8: index abs_playback_sessions (user_id, profile_id, started_at) for the listening-stats aggregations. Deferred (need contract/validation): listening-time idempotency (client delta-vs- cumulative), scanner deleted-file reconcile, abs session retention job, abs list- handler batch fetch, scanner-output P2s (need re-backfill). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(audiobooks): batch-fetch abs list/shelf handlers (kill N+1) handleSimilarItems, handleItemsInProgress, and handleGetMyProgress called MediaStore.GetAudiobookByID once per row — up to ~500 single fetches (each a few queries) on app open. Add GetAudiobooksByIDs (one access-scoped fetch + people/series hydrated once for the whole set) and look results up from the returned map, preserving order. Underlying primitives (GetByIDsWithAccess, hydratePeople/Series) were already batch-capable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audiobooks): reconcile deleted files on scan + prune session history #5: ScanAudiobookFolder only ever upserted — deleted/renamed books leaked media_items/media_files/memberships forever. Mirror the ebook reconcile: collect seenPaths during the walk, MarkMissing files no longer on disk, then reconcileLibraryMemberships. Safety mirrors ebooks/video: an inaccessible root (unmounted source) is skipped entirely, and a walk that saw zero files while the DB has rows only reconciles after operator cleanup confirmation (ebookEmptyCleanupAllowed) — so a flapping mount can't wipe the catalog. Soft mark only; the existing grace-period purge hard-deletes later. Reconcile runs only on a fully-completed (non-cancelled) scan. (#9 coarse case already handled: audiobookFolderShouldSkip skips unchanged folders; per-file reuse deferred.) #8-retention: abs_playback_sessions grew unbounded (one row per play-start, never deleted) and fed every listening-stats scan. Add an hourly sweep in SessionCleaner: close abandoned open sessions (no /close, stopped syncing >24h) and delete closed sessions older than 90 days. Mirrors the recommendation_cache / missing-files prune pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audiobooks): address max-effort code-review findings From /code-review max on the pre-PR diff: - DATA RACE (P0): SessionCleaner.lastABSSessionPrune is read+written by both the 15s ticker goroutine and the shutdown-path CleanStale call (main.go defers Stop() to after that call). Guard the prune-due gate with a mutex. (CleanStale was stateless before this branch, so concurrent calls were previously safe.) - ScanAudiobookFolder hardcoded fullScan=true into the empty-walk cleanup guard, but it's also called from ScanSubtree (incremental scans). An empty subtree scan would wrongly consume the operator's one-shot empty-cleanup allowance and warn. Thread a real fullScan flag (true from ScanFolder, false from the two subtree call sites), mirroring the ebook path. - Revert UpdateProgressPosition to UPDATE-only (drop the INSERT-on-missing): keep the monotonic GREATEST + finish guard that fixes the resume rewind, but restore the no-op-on-missing contract so a stray sync tick can't resurrect just-cleared progress or create a zero-duration continue-listening row. - Clamp the audiobook-groups handler limit (paging moved into the cache, leaving the old 500/page bound stranded); also gofmt the Scanner struct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(audiobooks): address review feedback for scanner and stats * fix(audiobooks): address review feedback * fix(audiobooks): retry failed session prune --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
c4cbcddeae |
feat(manga): manga library type — series grouping, reading loop, AniList/MangaDex metadata + status badge (#138)
* docs: design spec for manga library type (host sub-project) Forks the ebooks library type into a 'manga' type: series detected from the folder tree as a first-class type='manga' item, .cbz/.cbr chapters stay readable ebook items linked via a new manga_chapters table, browse shows series cards, enrichment targets the series item at content level 'manga'. Hands off to a follow-on plugin spec for the manga metadata source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for manga library type (host) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(scanner): manga filename index/volume parser * feat(scanner): manga series-name-from-folder detection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(plan): align manga DB/scanner tasks to scanner pure-planner pattern (no test-DB) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(scanner): manga parser corpus regression Add TestParseMangaIndexCorpus — 36 real-world scanlation filenames covering bare chapter, decimal chapter, v/vol-prefix volume, and c/ch-prefix chapter patterns; asserts <5% miss rate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(db): manga_chapters link table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scanner): manga_chapters repository + pure chapter-write mapping Adds mangaChapterWrite (pure, unit-tested), upsertMangaChapter, and listMangaChapters following the ebook/audiobook thin-SQL pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scanner): recognize manga library type Add isMangaLibraryType helper (unexported, matching the style of isEbookLibraryType / isAudiobookLibraryType) with a corresponding TestIsMangaLibraryType unit test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(api): manga library content level Map library type "manga" to content level ["manga"] in metadataContentLevelsForLibraryType so that seedDefaultChain seeds a manga-level metadata provider chain when a manga library is created. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scanner): route manga libraries to a manga scan path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(scanner): group manga chapters under a manga series item Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scanner): give manga series item a library membership so it browses Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(catalog): browse manga libraries as series Accept "manga" as a valid media_scope so a manga library browses only its type='manga' series items; the per-chapter type='ebook' items are naturally excluded because MediaScopeItemTypes("manga") expands to {"manga"}. Add the manga default library sections (scoped to media_scope='manga') so the library feed shows series cards. Refresh the two media_scope validation error messages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(catalog): manga series detail lists chapters For a type='manga' item, attach its chapters to the detail response via a new MangaDetailExtension. fetchMangaChapters joins manga_chapters to media_items on the chapter content ID, scopes to the series, and orders by chapter_index (NULLS LAST) then sort_title — matching the scanner's chapter ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): manga detail types + library browse scoping Add MangaChapter/MangaDetailExtension TS types mirroring the host catalog structs, wire manga? onto ItemDetail, and admit "manga" as a QueryDefinition.media_scope. Scope manga libraries to media_scope=manga in browse (host expands it to type=manga series items) while reusing the ebook sort universe via getLibrarySortRelevanceScope. Add isMangaLibraryType. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): manga series detail with volume-grouped chapter list Add MangaContent detail view: a DetailHero series header plus a chapter list grouped by volume. groupMangaChapters (pure, unit-tested) buckets chapters by their volume token, orders chapters within a group by chapter_index (nulls last) and orders groups by their minimum index; loose (volume-less) chapters collapse into a trailing "Chapters" group. Each chapter links to the existing ebook reader by content_id alone (file_id is optional — the reader resolves the file server-side), reusing buildMediaPlayHref. Admit "manga" into ItemDetail.type and wire the detail switch. Continue-reading is deferred (needs per-chapter progress fan-out / a last-read timestamp not in the current payload). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): handle manga in playable-type + collection filter-scope unions Adding "manga" to the shared ItemDetail["type"] and QueryDefinition["media_scope"] unions leaked into consumers with narrower local types, breaking the production tsc build. Fixes: - mediaNavigation: admit "manga" into PlayableMediaType. Manga series are not directly playable (you open the detail page and read a chapter, itself an ebook item), so buildMediaPlayHref falls through to the item href for them, like series/season. - FilterRuleEditor: add "manga" to FilterRuleMediaScope and relabel "watched" -> "Read" for manga as well as ebook (manga is read). - CollectionGuidedRulesEditor: add "manga" to GuidedFormState.mediaScope, a "Manga" media-type option, ebook-like "Read Status" labels, and map manga -> ebook sort-relevance scope (manga has no dedicated sort scope). - CatalogFilterBar (cascading leak surfaced after the above): add a "Manga" scope option and map manga -> ebook sort-relevance scope in both scope handlers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): offer manga as a library type in the create dialog Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(scanner): strip scene-release junk from manga series names Add cleanMangaSeriesName which repeatedly strips trailing parenthetical groups (year, year-range, Digital, release-group tags) then trims any dangling dash, so folder names like "404 Demons (Digital) (Oak)" resolve to "404 Demons". Wire it into mangaSeriesFromPath so both the series title and the mangaSeriesGroupKey identity key use the cleaned value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): flat volume/chapter manga list; nest only multi-chapter volumes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scanner): parse manga index after stripping series-name prefix Numbers inside a series title (e.g. "404 Demons", "365 Days to the Wedding") were wrongly grabbed as the chapter number because parseMangaIndex matched the first bare number in the full filename. mangaIndexForFile now strips the series-name prefix before delegating to parseMangaIndex, so only the number that follows the title is used. reconcileMangaFile in manga_scan.go is updated to call mangaIndexForFile instead of parseMangaIndex directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scanner): stop missing-file reconcile from deleting manga series items Manga series items are file-less virtual parents; the shared ReconcileFolderMembership swept them every scan because they have no media_file. Exclude type='manga' from file-presence membership reconciliation, and add a manga-scan step that deletes only series with zero remaining chapters. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ebooks): exclude manga chapters from individual ebook enrichment Manga chapters are type='ebook' parts of a series; the ebook enrichment sweep was searching each one against book sources (Gutenberg/Anna's/etc.) and failing in a pointless storm. Exclude items with a manga_chapters link; series-level enrichment is handled separately. * docs: design spec for manga metadata plugin + series enrichment (sub-project 2) New silo-plugin-manga-metadata (AniList, high-confidence matching) + a host MangaEnricher for type='manga' series; default-enabled metadata source for manga libraries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: implementation plan for manga metadata plugin + series enrichment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(db): manga_enrichment_state table Mirrors ebook_enrichment_state: dedicated failure counter for the manga enrichment sweep so it does not contend with media_items.refresh_failures. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(manga): series enricher (claims type='manga', resolves manga chain) * feat(manga): sync_manga_metadata task + enricher wiring * feat(catalog): expose manga chapter/volume counts in browse Add manga_chapter_count and manga_volume_count to browse cards so the frontend can render a Vols N / Ch N chip on manga series. The counts come from two index-backed correlated subqueries over manga_chapters in the browse SELECT (mangaCountColumns), scanned positionally before added_at and nilled out for non-manga rows. Threaded through models.MediaItem and exposed on the itemListResponse JSON card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sections): scope manga home recent sections to type=manga series A manga library mixes type='manga' series with type='ebook' chapters, so the auto-generated home 'Recently Added/Released in <Library>' rows surfaced the junk chapter filenames. Add GeneratedHomeLibraryRecentConfigScoped which emits the modern QueryDefinition shape (library_ids + media_scope) so a manga library's generated home rows filter to type='manga' only. Other library types are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(catalog): exclude manga chapters from browse/section/search surfaces Manga CHAPTER items (type='ebook' rows linked into a type='manga' series via manga_chapters) were leaking into catalog browse, section resolution, and search as standalone items showing junk filenames. They are internal sub-units of the series and only the series should appear. There is no single shared item-listing chokepoint: browse, the query/preview executor, and search each build their own WHERE. Add a shared, index-backed anti-join predicate (manga_chapters.chapter_content_id is the PK) via mangaChapterExclusionWhere and wire it into all three builders. By-id fetch paths that legitimately resolve chapters (ebook reader, continue-reading, series detail chapter list) use separate queries and are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scanner): use #NN as the manga volume for Vol.YYYY #NN releases mangaVolYearIssue early-return was returning the year token (e.g. "Vol.2003") as the volume label, which the frontend couldn't prettify to "Volume N". Now returns "v<issue>" (e.g. "v04") so the existing frontend regex ^v?(\d+)$ renders it as "Volume 4" correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(web): manga count chip on posters Add an optional manga_chapter_count / manga_volume_count to the browse item type and render a top-right "Vols N" / "Ch N" chip on ItemCard, strictly gated on type==='manga'. The label prefers "Vols" when the volume count dominates, "Ch" otherwise; the chip is hidden when the chapter count is missing or non-positive. No other card type renders it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): manga reader back returns to series (no loop) The ebook reader's back action defaulted to the chapter's own item detail (/item/<chapter>), whose back returned to the reader — an infinite loop for manga chapters. The reader now honors an explicit backTo search param when present, navigating there instead. Absent for normal ebooks, so their back behavior is unchanged. Only manga chapter rows pass backTo, keeping the fix manga-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): manga chapter row actions (read/mark-read/download) Each manga chapter/volume row now offers Read (the existing reader link, now carrying a backTo to the series), Mark-read (the shared watched-state mutation per chapter content_id), and Download (lazily fetches the chapter's file versions on demand and opens the shared DownloadVersionPicker, gated on user.download_allowed). The volume-unit / loose-chapter / section structure from buildMangaList is unchanged. Scoped to MangaContent only; EbookContent is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): validate reader backTo param is a safe in-app relative path Prevents open-redirect / javascript:-URI XSS from a crafted ?backTo= URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(catalog): include per-chapter read state in manga detail Manga chapters are ebook items, so a chapter is "read" when the viewer's ebook_reader_progress row crosses the finished threshold. fetchMangaChapters now LEFT JOINs that table scoped to the AccessFilter's user_id/profile_id and exposes a per-chapter Read bool on MangaChapter, threaded through buildMangaExtension. The detail payload previously carried no read state, so the row toggle always started unread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): manga rows reflect read state on load MangaChapter now carries an optional read flag from the detail payload, and MangaRow seeds its mark-read toggle from chapter.read instead of always starting unread. The optimistic toggle + shared watched mutation are unchanged; only the initial value is seeded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sections): exclude manga chapters from recently-added/released/random + other library-listing sections Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sections): manga recently-added/released cards show the latest volume's cover * fix(manga): keep enrichment honest about no-match vs enriched, batch 50->200 - sweep stats now separate enriched / no_match / failed: a stamped no-match was counted (and logged) as an enrichment, which masked a collapse of the real match rate during the backfill - batch size 50 -> 200 (SILO_MANGA_ENRICH_BATCH overrides): with the plugin serving GetMetadata from its search cache an item costs one rate-limited AniList request, so a sweep still fits the 5-minute task interval Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(manga): size enrich batch to the 5-minute interval at AniList's real budget 140 items x ~2.1s/request fits the interval; an overlong sweep makes the task manager drop the next trigger and the effective rate falls below the AniList budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(catalog): manga count chip data missing from library browse manga_chapter_count/manga_volume_count were only added to BrowseRepository, but /library/{id}?tab=library flows through previewQuerySource -> QueryExecutor.PreviewPage, which selects qualifiedListItemColumns and scans with scanItems - so manga cards never carried the counts and the Vols/Ch poster chip stayed hidden. Append mangaCountColumns to the preview-page SELECT and scan them via a new scanItemsWithMangaCounts (nil for non-manga rows, mirroring scanBrowseItems). Extract listItemScanDests so the three scan variants share one destination list instead of duplicating the 48-column scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): manga chip reads 'X Volumes · X Chapters', menu verbs say Read - chip: show distinct-volume and loose-chapter counts side by side instead of the single 'Vols N'/'Ch N' heuristic; mangaCountColumns now counts DISTINCT volume tokens (rows sharing a volume are one volume) and only un-volumed rows as chapters - watched-state labels: type='manga' fell through to the video default, so the card dot menu and detail page said 'Mark Watched' - manga now uses the ebook reading verbs (Mark Read / Mark Unread, 'Marked as read' toast) - format MangaContent.test.tsx (pre-existing prettier miss) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(manga): backdrop enrichment - banner hero art + backdrop-only backfill - cache remote backdrops like posters (cacheRemoteImages generalizes the poster-only path; failures keep the provider URL, which still renders) - claim arm for enriched items missing a backdrop: fetched by stored provider ID (search skipped - no rate spend, no re-match risk) and only the backdrop is written; stamping after the attempt keeps banner-less series from being re-claimed every sweep - backfill = one-time SQL clearing last_refreshed for poster-set/ backdrop-empty manga Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(manga): reading-loop UX - continue CTA, next chapter, series-aware cards, file details Fixes the four high-priority findings from the manga UX review plus a file-inspector request: - H1: series hero gets a Continue / Start Reading / Read Again CTA targeting the first unread chapter (firstUnreadChapter over the ordered list), plus an overflow menu (View Details, admin Refresh Metadata) - H2: the reader resolves its owning manga series (chapter detail now carries series_id/series_title) and offers next-chapter navigation: a header next button and an end-of-book floating CTA at >=99.5% progress; back defaults to the series even without a backTo param - H3: chapter rows show a persistent read check + muted title, and the mark-read mutation carries series_id so the series detail cache invalidates (read states no longer revert on revisit) - H4: continue-reading cards for manga chapters present the series: sections payload resolves chapter->series linkage, the card heading/image link to the series, and meta lines launch the reader - View Details: manga series menus (card dot menu + detail overflow) open a file inspector showing folder paths and per-chapter file names/sizes via GET /catalog/items/{id}/manga-files; paths are stripped for viewers without file-path visibility (item-versions policy) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(manga): UX mediums - richer detail page, smarter list, manga sort scope Second batch from the manga UX review (M1-M7): - M1: multi-chapter volume sections are collapsible (fully read sections start collapsed) with sticky headers, and long series get a 'Jump to <next unread>' anchor above the list - M2: the series hero shows the author line (HeroCrewLine learns Author credits with person links; DetailHero now renders crewLine and genre chips independently) and Volumes/Chapters badges - M3: browse-card count chip abbreviates to '12 Vol - 3 Ch' so it fits narrow cards without occluding covers - M4: manga gets its own sort scope: Duration/Bitrate (meaningless for file-less series rows) disappear, reading labels (Date Read / Reads) apply, Author stays - M5: global search labels manga results 'Manga' instead of the raw type - M6: chapters carry the viewer's reading fraction; part-read rows show an inline progress bar + percent - M7: chapter rows show the extracted cover thumbnail (presigned poster_url on the chapters payload) instead of a generic icon Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(manga): UX lows - volume token dedupe, comic reader chrome, empty-state hint - buildMangaList buckets volumes by canonical numeric token so mixed release naming (v01 + 1) yields one Volume 1 instead of duplicates - cbz/cbr readers start with the side panel closed and hide prose-only chrome (reading ruler, TTS, typography/font controls, hyphenation, writing mode) while keeping comic-relevant settings (theme, brightness, margin, right-to-left, spread, flow) - manga empty state mentions chapters appear after the library scan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(manga): publication status badge via new SDK status field - vendor the unpublished plugin SDK (adds MetadataItem.status) under internal/compat/ with a relative go.mod replace, following the zishang520-webtransport-go convention; swap to the published module before the upstream PR - map plugin status into MetadataResult.ShowStatus, persist it during manga enrichment, and show it as the hero status badge (show_status was already on the detail payload and MetadataBadges) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(manga): generalize backdrop pass to secondary fields (backdrop + status) The backdrop-only claim arm becomes a secondary-fields pass: enriched items missing a backdrop and/or publication status are claimed, fetched by stored provider ID, and only the missing secondary fields are written. Lets the new status field backfill across the already-enriched library instead of applying only to future enrichments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(metadata): merge ShowStatus through MergeMetadata/MergeGlobalMetadata The new MetadataResult.ShowStatus never reached the accumulated result the manga enricher persists from - the field-by-field merges didn't know it, so the status backfill pass obtained nothing. Regression-tested on both paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(manga): keep scanner identity IDs out of the metadata flow filterMangaProviderIDs passed the scanner's manga_series identity row through, so the search-skip-when-already-matched guard saw provider IDs on every item and never searched: unmatched items went straight to a by-ID fetch with no usable ID and were stamped as terminal no-match without a single provider request (and the MangaDex fallback was never consulted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: gitignore docker-compose.override.yml (local deployment override) The override unpublishes the bundled redis/postgres host ports (ports: !override []). It is a per-deployment, local-only file: ignoring it keeps a rebase from main and git clean -fd from disturbing it, and keeps it out of any PR. Its accidental absence once exposed Redis to the internet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(manga): code-review fixes — no-match guard, sort comparator, volume-count consistency - enrichWithProviders: set accumulator.HasMetadata after a provider result merges (MergeMetadata doesn't propagate it). Without this, a confident match carrying only genres/authors/status/year but no cover and no overview failed the no-match check and was discarded + terminally stamped. - byChapterIndex: both un-indexed chapters yield POSITIVE_INFINITY, so the subtraction was Infinity-Infinity=NaN (Array.sort treats NaN as 0, leaving order undefined). Compare explicitly for a stable order. - MangaContent volume/chapter badges: derive counts from the rendered buildMangaList entries (which canonicalize v01 ≡ 1) instead of raw distinct volume tokens, so the badge can no longer say '2 Volumes' over one row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(manga): clarify the enrichment claim's secondary arm is admin-reset-only The secondary arm (poster present, backdrop/status missing) requires last_refreshed IS NULL, so it is only reachable when an operator resets last_refreshed to backfill a newly-added field — not an automatic periodic re-check (which would re-fetch banner-less series every sweep). Documents the intent so it does not read as dead code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(manga): collapse continue-reading chapters per series; batch provider-id lookup - Continue Reading now collapses multiple in-progress chapters of the same manga into one card (most recently read kept), mirroring the episode→series collapse. The reading section resolves chapter→series linkage into itemMeta (applyMangaChapterSeriesMeta) and runs the shared collapseContinueWatchingSeriesCandidates, which the reading path previously skipped. - claimBatch resolves provider IDs for the whole batch in one query via the new ProviderIDRepository.GetByContentIDs (content_id = ANY), replacing the per-item GetByContentID N+1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): manga publication-status chip on browse cards + more legible chips - Color-coded publication status pill (Ongoing/Completed/Hiatus/Cancelled/ Upcoming) in the manga card's top-left corner, mirroring the vol/chapter count chip top-right. Strictly manga-gated; show_status was already on the browse payload. - New .glass-chip (78% surface vs glass-subtle's 40%) for the manga count + status pills so the labels stay legible over busy cover art. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * build(manga): depend on published silo-plugin-sdk v0.7.0 Replace the vendored internal/compat/silo-plugin-sdk copy with a normal dependency on the published SDK module at v0.7.0, which adds MetadataItem.status (publication/airing status) consumed by the manga status badge at internal/metadata/plugin_provider.go. - go.mod: pin v0.7.0, drop the local-path replace directive - remove the vendored internal/compat/silo-plugin-sdk tree - Dockerfile: drop the vendored-SDK COPY - strip the manga design docs/plans from docs/superpowers (internal) Requires Silo-Server/silo-plugin-sdk#4 merged and tagged v0.7.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(manga): exclude chapters from the matcher's unmatched-item lister Manga chapters are type='ebook' items that stay status='pending' by design - provider metadata lives on the type='manga' series item. The scan-final RetryUnmatchedItemsByFolderAndPathPrefix listed all of them and ran a rate-limited ebook-plugin search per chapter: 31,564 chapters x ~1s = 8h46m appended to a 2-minute manga library scan (observed live), every one a guaranteed no-match. Earlier runs never survived to completion, so the library's last_scanned_at stayed NULL forever. Add the same manga_chapters NOT EXISTS guard the ebook enricher's claim query already uses. Verified live: the same library now scans in 27s with retried_items=0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scanner): never probe-repair ebook/comic files (ebook+manga detail-page killer) NeedsCriticalProbeRepair was always true for BaseType 'ebook' files (epub, pdf, cbz, cbr — incl. manga chapters): buildEbookMediaFile leaves ProbeUpdatedAt nil and they have no audio/video, so probeEnsurer.Ensure spawned ffprobe per file on every detail/watch load and never converged (ffprobe errors on zip/rar, result never persisted). Short-circuit probe-repair for ebook base type — they're read directly and never use the transcode/playback probe pipeline. SHARED fix: benefits both the ebooks and manga library types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(ebooks): parallelize detail extension + preserve finished read-state - buildEbookExtension ran its 3 related-content queries (series, also-by-author, similar) sequentially; run them concurrently like buildAudiobookExtension so ebook detail latency is the slowest query, not their sum. - PGEbookReaderProgressStore.Upsert did an unconditional SET progress=EXCLUDED; a routine autosave (e.g. reopening a finished book) could drop it below the 0.9 finished threshold and silently un-mark it read (and clear the manga chapter checkmark, which rides on the same row). Guard: once finished, progress only moves on an explicit unread (row delete); below threshold it tracks freely. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(manga): batch chapter presign, index volume counts, quiet scan log - fetchMangaChapters presigned each chapter poster individually; a long-running series has hundreds of chapters. Batch them in one PresignImageURLs call, and add the missing rows.Err() check (was silently returning partial lists). - The browse manga count chip's count(DISTINCT volume) subquery wasn't covered by manga_chapters_series (series_content_id, chapter_index); add idx_manga_chapters_series_volume (series_content_id, volume) so both count subqueries are index-only. - Downgrade the per-chapter "manga scan: indexed" log from Info to Debug (one line per .cbz; the 500-file progress log already covers operator visibility). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(manga): address PR #138 code-review findings Folds PR #142 into the manga branch (already done via fast-forward) and remediates the issues surfaced in the #138 code review. Correctness: - Preserve the scanner's manga_series identity anchor through enrichment. ReplaceByContentID's DELETE was unconditional, so the first successful enrichment wiped the manga_series provider-id row the scanner relies on for idempotency, causing duplicate series + metadata loss on the next scan. excludedProviderIDs now also means "not deleted", and the DELETE preserves those rows. (internal/catalog/provider_id_repo.go) - Fall back to the series cover when the latest chapter has no poster. Poster columns default to '' (not NULL), so the manga series-card poster override blanked cards via a plain COALESCE; wrap operands in NULLIF. (internal/sections/fetcher.go) - Keep backTo a real query param on reader links when libraryId is absent. It was string-concatenated with '&', producing a malformed URL on deep-links; route it through the query helper instead. (web/src/lib/mediaNavigation.ts, EbookReader.tsx, MangaContent.tsx) Quality: - Hide manga chapters from favorites/watchlist browse, matching the exclusion enforced on every other listing surface. (internal/catalog/favorites_browse.go) - Centralize the manga chapter exclusion predicate into a single exported catalog.MangaChapterExclusionWhere, removing four duplicated copies. (catalog, sections, ebooks) - Skip the two manga count subqueries on browse scopes that cannot contain manga (non-manga type filters), substituting NULL placeholders. (internal/catalog/browse.go) - Normalize provider publication status (AniList/MangaDex/SDK variants) into a stable label set so show_status carries one manga value-domain. (internal/manga/enrichment.go) Adds unit tests for the poster NULLIF contract, browse gating, and status normalization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: regenerate go.sum after rebase onto main Drops stale silo-plugin-sdk v0.6.0 and other leftover hashes from the intermediate rebased states; go.mod is now on the published v0.7.0 tag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scanner): adapt manga scan to ebookFileShouldSkip 3-value signature main changed ebookFileShouldSkip to also return the existing content ID; the manga scan path only needs the unchanged flag, so discard the new return. Resolves a silent semantic conflict from the rebase onto main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Silo Server Developer <warmasterx555@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e084cdd1d6 |
Add unified literary works for ebooks and audiobooks (#107)
* docs: add literary works design and plan * feat(literary): add work link schema * feat(literary): add work domain primitives * feat(literary): persist work links * feat(literary): score work matches * feat(catalog): include literary work summary on item detail * feat(literary): expose work detail API * feat(literary): assemble work detail * feat(literary): add admin work linking primitives * feat(catalog): group literary items by work * feat(literary): auto-link works during book scans * fix(literary): narrow work match candidates * fix(literary): address work merge blockers --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
bf54f040f9 |
fix(audiobooks): extract scan covers and harden dedupe (#103)
* fix(audiobooks): extract scan covers and harden dedupe * fix(audiobooks): address scan cover review --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> |
||
|
|
13c5e0ba2f |
feat(catalog): deterministic cross-server content_id (#155)
* feat(catalog): deterministic cross-server content_id Replace per-server Sonyflake content_id with a structured natural key derived from provider IDs (movie:tmdb:…, series:tvdb:…, episode:…, local:… fallback), so two servers holding the same title share one anchor for artwork, watch history, progress, favorites and ratings. - internal/contentid: derivation core, SeriesIDFromContentID transform, frozen precedence, SchemeVersion=1, embedded-series-anchor invariant. - internal/metadata/service.go: deterministic id at every mint site. - internal/catalog/history_source.go: resolve show via string transform for anchored episode ids; skip the episodes_pkey probe. - migrations/sql/20260612130000: collision-safe value remap across the 65-column reference graph + COLLATE "C", FK/trigger handling, audit map, working down. Benchmarked against an exact-cardinality copy of cprod-postgres (1.93M episodes, 775k history rows): 2.57x faster history page, 1.7x throughput at 100 concurrent users, 2.7x cheaper per content_id probe. * feat(catalog): re-ID untagged items to deterministic content_id at first match Untagged libraries get a path-derived local: content_id at scan time and only learn their provider IDs later, when the match worker confirms a result. Previously that id was never folded back in, so untagged-then-matched items kept a per-server local: placeholder forever and never converged across servers (re-ID was deferred to a migration rerun). mergeAndPersist now promotes a local: skeleton to its deterministic provider-anchored id at the moment of first confirmed match, via a single new gate (canonicalizeLocalContentID): - target id already taken -> merge onto it (existing rebind machinery) - target id free -> rename in place The rename is a single SQL function (silo_rename_content_id); FK children follow via ON UPDATE CASCADE added to the content_id family, so a fresh skeleton moves a handful of rows rather than the full-table remap the bulk migration does. The guard is one IsLocal prefix check, so tagged content and all refreshes pay nothing, and the move is self-healing under retry. Verified: gofmt/vet/build clean; migrate-validate passes; migration applies on the real schema (up/down/up), FKs gain ON UPDATE CASCADE while keeping ON DELETE; functional test confirms series PK move + series_id cascade + provider-id sweep, and movie rename. Follow-ups (noted in docs): recomposeSeriesChildIDs for a series that accumulated episodes before matching; a lockstep test for the soft-ref list. * fix(catalog): harden content_id parsing and merge per review Address review feedback on the deterministic content_id work: - history_source.go: gate the anchored-episode display-id transform on the full five-part episode shape (split_part parts 2-5 non-empty), not just the 'episode:' prefix, so a malformed id can't transform to 'series:broken:' and vanish at the media_items join. Shared anchoredEpisodePredicate drives both the null-poisoned join key and the series-recovery expression. - contentid.go: unexport the provider-precedence slices so no package can mutate the frozen SchemeVersion ordering at runtime. - contentid.go: add parseAnchored to validate the exact per-kind arity and numeric season/episode suffixes; SeriesIDFromContentID and IsProviderAnchored now fail closed on truncated/malformed ids (e.g. "episode:tvdb:296762"). - canonicalize.go: distinguish catalog.ErrItemNotFound from transient lookup errors (a real error no longer masquerades as "target free"), and allow a matched local source to be consolidated onto the canonical row instead of orphaning a duplicate. * refactor(contentid): URL-safe "-" separator in content_id Use "-" instead of ":" to join content_id components (movie-tmdb-228064, episode-tvdb-296762-1-5, local-<hex>). "-" is an RFC 3986 unreserved character, so a content_id is URL-safe verbatim: encodeURIComponent is a no-op and the id is its own tidy path segment (/item/series-tvdb-296762) with no %3A escaping. The stored value equals the URL value, so there is no encode/decode boundary and an operator can grep the id straight out of a URL or log. Every component is [a-z0-9]+ (or "tt"+digits), so "-" is unambiguous. Pre-release format finalization: this branch is unmerged, so no deployed data carries ":" ids — the migration mints the "-" form fresh and no re-migration is needed. Still SchemeVersion 1. - contentid.go: single `sep` constant drives construction and parsing so the two can never drift; all constructors/parsers and doc examples updated. - history_source.go: split_part transform and the anchored-episode predicate use '-'; kept in lockstep with the package via a code comment. - 20260612130000_deterministic_content_id.sql: derivation and season/episode composition emit '-'; LIKE filters match 'series-%'. - docs/architecture/deterministic-content-id.md: format spec + rationale for the separator choice; this is the design doc the change is derived from. Client-side: the web frontend treats content_id as an opaque string (no splitting/regex), so no client changes are required; existing encodeURIComponent call sites simply stop emitting %3A. * docs(contentid): show why hash/bigint rejected in probe-cost table Add Cross-server deterministic / Zero-join show transform / Human-readable columns to the index-probe-cost comparison so the trade-off is legible at a glance: the 128-bit hash and bigint surrogate are faster but each give up a load-bearing property, and the structured key is the only all-checkmark row. * docs(contentid): order probe-cost table to end on the structured key * docs(contentid): label fenced blocks and drop stray EOF tags Per CodeRabbit review: add 'text' language to three fenced code blocks (MD040) and remove accidental </content></invoke> artifacts at EOF. * fix(catalog): remap array-valued content_id soft references in deterministic id migration The value-remap migration (20260612130000) enumerates the reference graph by FK plus a scalar name+type sweep (text/varchar/bpchar). That misses trending_discover_snapshots.content_ids: it is text[] (excluded by the type filter), named content_ids not content_id (excluded by the name list), and cannot carry an FK — so the bulk remap left those arrays holding stale Sonyflake ids that resolve to nothing until the snapshot regenerates. A counterexample to the migration's "self-protecting, cannot orphan" invariant. Remap the array element-wise in both directions (Up old->new, Down new->old), preserving order and leaving collision/unmatched elements untouched; a WHERE EXISTS guard skips empty/unaffected arrays so array_agg never collapses the NOT NULL column to NULL. Mirror the gap in silo_rename_content_id (20260614120000) with array_replace for the single-value runtime rename so the two stay in lockstep. Verified on PG18: mixed/collision/empty arrays remap correctly and round-trip clean; runtime array_replace preserves order. Surfaced reviewing #155. The jellycompat restart-decode regression and the atomicity-wording nit are posted as review comments, not addressed here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(jellycompat): pack content_id into compat UUID reversibly so item ids survive restarts Addresses the restart-decode regression raised in review of #155. With content_id now a structured string instead of a numeric Sonyflake, EncodeStringID sent every item/season id down the one-way SHA1 path, making decode depend on an in-memory reverse map. That map is cold after a process restart (the codec is a process-lifetime singleton), so a client presenting a previously-issued item UUID — resume-from-home, deep link, detail page, image, userdata — got "unknown compat id" until the item was re-listed. Make the encoding reversible instead of stateful: - internal/contentid: add Pack/Unpack, a bit-packed, fixed-budget (<=15 byte) binary form of a structured or local content_id. digitCount preserves provider-id leading zeros (e.g. imdb tt0944947); structured forms are self-delimiting; the local form fills the budget exactly. Provider ids that overflow uint64 return ok=false. - Shrink ForLocal to a 112-bit (sha256(path)[:14]) hash so a local id packs losslessly into the 15-byte UUID payload. 112 bits is far beyond any single server's local-item count. No other code assumed the old width. - internal/jellycompat: EncodeStringID packs item/season content_ids into the UUID (byte 0 = kind, bytes 1..15 = packed, non-zero tag distinguishes it from the numeric encoding); DecodeStringID unpacks first and re-packs to confirm, so an opaque id whose bytes merely parse is rejected and falls through to the map. Numeric ids and arbitrary names (genres, studios) are unchanged. Net: item/season ids decode by pure computation — stable across restarts and across instances — with no lookup table. Only the rare unpackable content_id and non-content names still use the in-memory map. TDD: round-trip property tests in contentid (all kinds, leading zeros, reject cases) and a cross-instance decode test in jellycompat that fails on the old hash+map path. Full contentid + jellycompat suites green; production code golangci-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(migrate): make the migration run timeout configurable (SILO_MIGRATE_TIMEOUT) The boot-path migration runner hardcoded a 5-minute context timeout. The deterministic-content-id value-remap (20260612130000) does a full-table COLLATE rewrite + 65-column remap that needs ~20 min on a real dataset (615k items / 2M episodes), so it was cancelled at 5 min. Worse, Postgres keeps the orphaned backend running (holding AccessExclusive locks) until it notices the dead client at a statement boundary, while the goose session advisory lock releases on disconnect — so each 5-min boot retry piled a new attempt behind the previous one's locks. The migration never applied; the server boot-looped. Make the timeout configurable via SILO_MIGRATE_TIMEOUT (a Go duration like "60m"); 0 or negative disables the deadline for a one-off heavy migration. Default stays 5m. All three entry points (migrate-status, --migrate-only, boot) honor it. Required for the deterministic-content-id migration to apply on any real-sized database, not just dev — the 5m cap made the PR undeployable at scale. Follow-up (not here): on cancellation the runner should actively terminate its backend so a future timeout cannot orphan a lock-holding statement. TDD: MigrationTimeout parsing (default/override/zero/invalid) + MigrationContext deadline behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(contentid): require exact length for local ids in Unpack Tighten the tagLocal branch of Unpack from `len(body) < localHashLen` to an equality check. The local form fills the compat-UUID payload exactly (no padding), so a body of any other length is non-canonical; matching it exactly keeps Unpack a strict fail-closed inverse of Pack for the fixed-length branch, which decodes client-supplied UUIDs. Not applied to the structured branch (a review suggestion proposed the same change there): structured ids are self-delimiting and the compat layer pads them with trailing zeros to fill the 15-byte UUID payload, so ignoring trailing bytes is intentional and documented. Rejecting them would make every structured id fail to decode — the jellycompat cross-instance test guards against that. Not a live bug today (the only caller passes u[1:] from a 16-byte UUID, so body is always exactly localHashLen, and idcodec re-packs to verify), but it is the correct contract and zero-risk. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
c7f65bf351 | fix(audiobooks): normalize ABS session token hash | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
339ed20074 |
fix(audiobooks): improve poster enrichment throughput (#94)
* fix(audiobooks): improve poster enrichment throughput * fix(audiobooks): enable ABS compat by default * chore(audiobooks): remove stale feature flag setting * fix(audiobooks): validate scoped scan roots --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> |
||
|
|
96bae2f346 |
fix(library): clean orphaned provisional items (#76)
* fix(library): clean orphaned provisional items * test(metadata): use item not found sentinel in fake repo * fix(library): preserve abs references during orphan cleanup * fix(library): avoid dropped abs cleanup tables * fix(library): harden provisional orphan cleanup * fix(library): preserve home dismissal series orphans * fix(library): Delete matched items created after the first orphan sweep |
||
|
|
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 |
||
|
|
31b2716089 | feat(database): adopt goose migrations (#62) |