codex/bound-transcode-segments
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
71ca2012c5 | feat(sections): show episode context in cards | ||
|
|
c085b12fd1 | Initial Silo migration |