Commit Graph
82 Commits
Author SHA1 Message Date
RXWatcherandClaude Opus 4.7 eeb22e47d3 feat(audiobooks): render Also-by-author / In-series rails when API exposes them
Frontend gains a feature-detected display path for two relationship
rails. Detail responses may now carry also_by_author and in_series.
When present, the detail page mounts Also-by-author followed by
In-series, with the current book highlighted inside the series strip.
When the backend omits them — which is the case today — the rails are
not rendered, so this lands as zero visible change until the backend
ships the data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:42:53 +02:00
RXWatcherandClaude Opus 4.7 07da12f8f4 feat(audiobooks): add narrator card to detail page
Surfaces the narrator as a first-class card with a circular initials
avatar. Rendered only when data.narrator is present, so audiobooks
without narrator metadata don't introduce empty space.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:41:11 +02:00
RXWatcherandClaude Opus 4.7 9013718b2d feat(audiobooks): chapters expanded by default with current-row highlight
Replaces the collapsed-by-default ChapterList disclosure with
ChaptersSection: always-expanded, current-chapter highlighted with a
left-edge accent + 'playing' badge, and a small sort menu (By position
/ Longest first). currentPositionSeconds is plumbed from the player's
seek start when open, otherwise from the resume position, so the
highlight tracks the user's most recent listening point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:40:43 +02:00
RXWatcherandClaude Opus 4.7 de0bb87a79 feat(audiobooks): hero shows progress + chapter-aware Resume label
Progress bar moves above the Resume / Play-from-Start row. Caption
becomes \"Xh Ym listened · NN%\" (was just the listened time). Resume
button now reads \"Resume · <chapter name>\" derived from
findChapterAt so the user sees where they'll land without opening the
player. No progress → no bar, no chapter; the Play button keeps its
existing label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:39:24 +02:00
RXWatcherandClaude Opus 4.7 c71a0a88a2 feat(audiobooks): add sleep timer with duration + end-of-chapter modes
SleepTimerMenu is a new shared player primitive (moon icon, popover
with 5/15/30/45/60-minute presets plus End-of-chapter). Hook state in
useAudiobookPlayback drives both the countdown label and the auto-pause:
duration timers track wall-clock so they survive tab inactivity, and
end-of-chapter watches currentChapter.end_seconds. Both modes pause the
audio element and reset the setting when they fire. Wired into the
right rail of MiniBar and into the utility row of NowListening.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:38:35 +02:00
RXWatcherandClaude Opus 4.7 7529c8e37a feat(audiobooks): wire mini ↔ Now Listening mode swap
AudiobookPlayer now tracks 'mini' | 'now-listening' locally. The
CoverExpandTile flips to Now Listening, the chevron flips back. The
hidden audio element is owned by the shared parent, so swapping chrome
does not remount it and currentTime advances continuously across the
transition. MiniBar now owns its fixed-bottom positioning so the
AudiobookDetail wrapper can drop. Author + narrator now flow into the
overlay's header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:36:27 +02:00
RXWatcherandClaude Opus 4.7 b02e6ed84c feat(audiobooks): build NowListening overlay component
Full-overlay chrome that mirrors the mini bar's controls scaled up
for a focused listening view: large cover (with view-transition anchor
matching CoverExpandTile), title/author/narrator block, current-chapter
heading, large SeekBar with toggleable remaining-time label, big play
disc, and the same chapter + speed menus. Not yet reachable — wired in
Task 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:35:41 +02:00
RXWatcherandClaude Opus 4.7 62cbe942c6 feat(audiobooks): add CoverExpandTile to MiniBar (no expand wiring yet)
The tile sits at the far left of the bar; clicking it fires onExpand,
which is a no-op for now (mini ↔ Now Listening swap lands in the next
task). The tile sets viewTransitionName: audiobook-cover-\${contentId}
so the upcoming overlay can morph from it via the View Transitions API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:34:44 +02:00
RXWatcherandClaude Opus 4.7 d8e03efda0 feat(audiobooks): show current chapter in MiniBar left column
Renders playback.currentChapter.title between the book title and the
time row. Truncates with a tooltip; absent when the chapter list is
empty or the current position falls outside any chapter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:33:09 +02:00
RXWatcherandClaude Opus 4.7 5d1534f794 refactor(audiobooks): split MiniBar out of AudiobookPlayer shell
The bar chrome moves into MiniBar.tsx; AudiobookPlayer becomes a thin
shell that owns the audio element and renders MiniBar. Same DOM, same
behavior — sets up the NowListening swap (Task 9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:32:38 +02:00
RXWatcherandClaude Opus 4.7 4d40118cba refactor(audiobooks): extract useAudiobookPlayback hook
All audio-element wiring, periodic progress reporting, and seek/skip/
toggle/rate logic move into a reusable hook. The player component now
consumes playback.* and switches to the extracted CircleButton
primitive shared with the video player.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:31:24 +02:00
RXWatcherandClaude Opus 4.7 813ebfeb64 refactor(audiobooks): move AudiobookPlayer into player/ subfolder
Pure file move. Sets up the directory for the upcoming hook + chrome
split (useAudiobookPlayback, MiniBar, NowListening).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:29:26 +02:00
RXWatcherandClaude Opus 4.7 025fdba565 feat(player): SpeedMenu primitive replaces audiobook select
Introduces a shared SpeedMenu component (button trigger + popover with
arrow-key navigation and escape-to-close) used by the audiobook player.
Replaces the previous native <select> with a menu that matches the
in-design dark surface treatment and the planned shared player chrome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:21:40 +02:00
RXWatcherandClaude Opus 4.7 63e00a498b refactor(player): extract CircleButton shared primitive
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:54:36 +02:00
RXWatcherandClaude Opus 4.7 50be1d4243 docs(audiobooks): add embedding-based Similar audiobooks rail
Adds a third feature-detected rail on the detail page powered by
server-side embedding similarity. Frontend has no embedding logic —
it just renders the ordered list the API returns. Plan gains Task
14b implementing it via an optional subtitle prop on RelatedRail
and an optional similar_audiobooks field on the detail response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:42:31 +02:00
RXWatcherandClaude Opus 4.7 ac31c9d17e chore(audiobooks): snapshot in-progress work as pre-redesign baseline
Bundles 21 in-progress files spanning scanner, image cache, ABS API
handlers, podcast/audiobook scan, cover enrichment, metadata sync
task, and an interim pass at the audiobook detail page (DetailHero
adoption + fixed-bottom player). Committed as a single baseline so
the upcoming UI redesign (per the 2026-05-24 plan) starts from a
clean working tree. Not split into logical commits because this
captures pre-existing local work, not a curated change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:24:11 +02:00
RXWatcherandClaude Opus 4.7 7a374c9b8f docs(audiobooks): UI redesign implementation plan (14 tasks)
Tasks 1–10 cover player work: extract shared CircleButton + SpeedMenu
primitives, split AudiobookPlayer into a state hook plus MiniBar /
NowListening chrome, add cover-tile expand affordance, and ship a
sleep timer (duration presets + end-of-chapter). Tasks 11–14 cover
detail-page work: chapter-aware Resume action, expanded chapters
section with current-row highlight, narrator card, feature-detected
"Also by author" / "In series" rails.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:21:40 +02:00
RXWatcherandClaude Opus 4.7 024c51f616 docs(audiobooks): UI redesign spec — player two-mode + detail page
Bring the audiobook detail page and player to parity with the video
player, translated for audio: shared glass-disc primitives, a polished
mini bar with cover-tile expand affordance, and a Now Listening
full-overlay mode. Restructure the detail page so progress, current
chapter, narrator, and cross-book context are foregrounded. Defers
bookmarks, car mode, and cross-book rails to v1.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:08:41 +02:00
RXWatcherandClaude Opus 4.7 454be6da74 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>
2026-05-24 16:12:49 +02:00
RXWatcherandClaude Opus 4.7 412defb939 feat(audiobooks): port podcast RSS feed refresher
Sub-plan 5 of the audiobooks absorption. Ports the plugin's
podcastfeed.Refresher into internal/audiobooks/podcastfeed and
registers it as a 10-minute scheduled task in silo's task manager.

For each row in podcast_feeds (sub-plan 1 mig 140), the refresher:
  - Fetches the RSS feed with a 30-second HTTP timeout
  - Parses entries via gofeed (github.com/mmcdole/gofeed)
  - UPSERTs new entries into episodes (joined to the podcast's
    media_items row via podcast_feeds.media_item_id = series_id)
  - Updates podcast_feeds.last_refreshed_at + last_refresh_error

Migration 144 adds podcast_guid and podcast_audio_url columns to
episodes, plus a partial unique index on (series_id, podcast_guid)
WHERE podcast_guid IS NOT NULL so the refresher can ON CONFLICT upsert
without touching non-podcast episode rows.

Enclosure file download is deferred; this stage upserts episode
metadata only. Existing content_ids are reused when a feed re-emits a
known guid so per-user progress rows stay bound to the same FK target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:06:27 +02:00
RXWatcherandClaude Opus 4.7 8de72bf424 docs(audiobooks): implementation plan sub-plan 5 (podcasts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:58:59 +02:00
RXWatcherandClaude Opus 4.7 64e30997cb feat(audiobooks): wire ABS routes into main router
Stage 7 of the ABS-compat port (final). Implements MediaStore, ConfigProvider,
and event/recommender stub adapters, constructs the abs.Handler in
cmd/silo/main.go, and mounts it under /abs and /abs/api on silo's main
router. ABS-compatible Audiobookshelf clients can now hit silo
directly for login, library + items browse, file streaming, progress,
and Socket.io.

End-to-end smoke verified: /login returns 401, /abs/api/login returns
401, /abs/api/me returns 401, /abs/api/libraries returns 401,
/abs/socket.io/ returns 200 on polling handshake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:58:02 +02:00
RXWatcherandClaude Opus 4.7 7785f286e7 feat(audiobooks): port ABS Socket.io endpoint
Stage 6 of the ABS-compat port. Lifts the plugin's abssocket package
into internal/audiobooks/abssocket/, mounted under /abs/socket.io/ on
silo's main HTTP listener. The plugin's standalone-listener
workaround is no longer needed because the host plugin-proxy WS-bridge
constraint disappears once the code runs in-process.

Redis pub/sub adapter behavior is preserved when REDIS_URL is set;
single-replica in-memory fan-out otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:45:11 +02:00
RXWatcherandClaude Opus 4.7 d73b1b1c6b feat(audiobooks): port ABS library + items browse
Stage 5 of the ABS-compat port. Adds:
  - GET /abs/api/me (and /api/me)
  - GET /abs/api/libraries (and /api/libraries)
  - GET /abs/api/libraries/{id}
  - GET /abs/api/libraries/{id}/items
  - GET /abs/api/items/{id}
  - GET /abs/api/items/{id}/similar
  - GET /abs/api/me/items-in-progress (Continue Listening)
  - Stub 200 endpoints for authors, series, search, personalized

Extends MediaStore interface: ListAudiobookLibraries and
LibraryID-filtered ListAudiobooks. Adds AudiobookLibrary struct.

Also ports collapse.go (CollapseBySeries) from the plugin — identical
logic, needed for collapseseries=1 query param support.

Author/series detail and filter-data population stubbed (empty lists);
ABS clients fall back to flat item browse when those endpoints return
empty rather than crashing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:41:21 +02:00
RXWatcherandClaude Opus 4.7 8c60e48817 feat(audiobooks): port ABS progress + continue listening
Stage 4 of the ABS-compat port. Five handlers:
  - GET  /abs/api/me/progress
  - GET  /abs/api/me/progress/{libraryItemId}
  - POST /abs/api/me/progress/{libraryItemId}
  - PATCH /abs/api/session/{sid}
  - POST /abs/api/session/{sid}/close

Reads/writes hit silo's user_watch_progress (keyed by user_id,
profile_id, content_id) for resume positions and the new
abs_playback_sessions table (migration 143) for per-session
listening-time tracking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:35:34 +02:00
RXWatcherandClaude Opus 4.7 db138450ac feat(audiobooks): migration 143 add abs_playback_sessions table
Tracks active /abs/api/items/{id}/play sessions for Audiobookshelf
clients so the /session/{sid}/sync and /session/{sid}/close
endpoints can record per-session listening time. Distinct from
abs_sessions (which tracks auth tokens) and user_watch_progress
(which tracks resume position).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:35:25 +02:00
RXWatcherandClaude Opus 4.7 1930e7867e feat(audiobooks): port ABS file handler + play session
Stage 3 of the ABS-compat port. handleFileStream serves a single
audio file by ino (which we map to silo's media_files.id);
handlePlayStart issues a play-session manifest pointing at silo's
existing /api/v1/direct-download stream URL. Plugin's bookref/
streaming/mediatoken abstractions are replaced with direct
MediaStore lookups against silo's tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:22:51 +02:00
RXWatcherandClaude Opus 4.7 7619e84c8d feat(audiobooks): port ABS auth (JWT + login bridge)
Stage 2 of the ABS-compat port. Adds the handleLogin handler issuing
JWT access + refresh tokens, a SiloCredValidator adapter that calls
silo's auth.Service directly (in-process — the plugin previously
HTTP-POSTed to /api/v1/auth/login), and an ABSSessionStore adapter
backed by the abs_sessions table from migration 139.

POST /abs/login and POST /login are mounted but not yet reachable
until Stage 7 wires the audiobook service into the main router.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:17:48 +02:00
RXWatcherandClaude Opus 4.7 226d721ea3 feat(audiobooks): port ABS handler scaffolding (mount/handler/filter)
Stage 1 of the audiobookshelf-compatible REST + Socket.io port from
silo-plugin-audiobooks. Lands the package layout, Handler struct with
interface stubs for silo-side dependencies (MediaStore, TokenStore,
ProfileCredentialValidator, ConfigProvider, EventPublisher, Recommender),
and an empty Mount() method. Real route handlers arrive in subsequent stages
(auth, file serving, progress, browse).

Plugin imports of store/backend/streaming/mediatoken/bookref/runtimehost are
replaced with minimal interfaces; real silo implementations come when those
interfaces are needed by specific route handlers.

Also ports filter.go (ABS filter query parsing/matching), access_log.go
(request logging via log/slog), minified.go (minified=1 response shaping),
login_ratelimit.go (per-IP token bucket), jwt.go (JWT mint/parse), and
types.go (shared ABS wire-format types and constants) — all clean of
plugin-specific imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:14:12 +02:00
RXWatcherandClaude Opus 4.7 7ea7609931 docs(audiobooks): implementation plan sub-plan 4 (ABS-compat)
Stage-based port of silo-plugin-audiobooks' abs/ + abssocket/
packages. Preserves the plugin's file layout but rewrites every SQL
query against silo's existing tables (media_items, media_files,
episodes, user_watch_progress, user_playback_sessions, abs_sessions,
people, item_people) — no new tables. Out-of-scope plugin features
(bookmarks, smart collections, share links, playlists, podcasts/RSS
in this sub-plan) are dropped. 8 stages from support files through
Socket.io, router wiring, and final smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:06:09 +02:00
RXWatcherandClaude Opus 4.7 9ed283d2ef feat(audiobooks): wire navigation and routes
Adds an Audiobooks entry to the sidebar and registers the two new
routes (/audiobooks for the library grid, /audiobooks/book/:id for
detail). The player renders inline inside the detail page; no
dedicated player route is required for MVP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:00:14 +02:00
RXWatcherandClaude Opus 4.7 7094ad3760 feat(audiobooks): HTML5 audio player with chapter navigation
Single-file audiobook playback for MVP. Multi-file queuing arrives in
a follow-up. Streams via the existing /api/v1/direct-download GET
endpoint. Position is reported to /api/v1/audiobooks/{id}/progress
every 10s while playing plus on pause/seek/end. Skip-30s, playback
rate select, chapter list panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:00:09 +02:00
RXWatcherandClaude Opus 4.7 8696259103 feat(audiobooks): detail page with chapter list
Renders cover, title, author, narrator, year, and overview alongside a
chapter list. Clicking a chapter opens an inline sticky
AudiobookPlayer at that chapter's start. A "Resume" button restarts
playback at the saved progress position if present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:00:04 +02:00
RXWatcherandClaude Opus 4.7 4fa8895556 feat(audiobooks): library grid page at /audiobooks
Renders a paginated grid of audiobook cards using the
useAudiobookLibrary hook. Each card links to /audiobooks/book/{id}.
Cards show poster, title, and year; falls back to a "No cover"
placeholder when the audiobook has no poster_url. Empty state hints
to operators that they need to set a library's type to 'audiobooks'.

Routes themselves are wired in Task 8 (navigation integration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:51:56 +02:00
RXWatcherandClaude Opus 4.7 cfd682470b feat(audiobooks): frontend types and TanStack Query hooks
TypeScript types match the JSON shapes from the new
/api/v1/audiobooks endpoints (list, detail, progress). Three hooks:
useAudiobookLibrary (list), useAudiobook (detail), and
useReportAudiobookProgress (mutation that invalidates the detail
query on success so progress updates reflect immediately).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:44:57 +02:00
RXWatcherandClaude Opus 4.7 75cff9f441 feat(audiobooks): progress endpoint at POST /api/v1/audiobooks/{id}/progress
UPSERTs user_watch_progress for the caller's (user_id, profile_id,
content_id). Body carries position_seconds; clients are expected to
post every 5-10s during playback plus on pause/seek (matching silo's
existing video progress cadence).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:40:50 +02:00
RXWatcherandClaude Opus 4.7 1ce29ac60d feat(audiobooks): detail endpoint at GET /api/v1/audiobooks/{id}
Returns the media_items row, its media_files (with chapters JSONB),
author/narrator extracted from item_people (kinds 7/8), and the
caller's per-profile listening progress from user_watch_progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:38:17 +02:00
RXWatcherandClaude Opus 4.7 cc12ab5d5d feat(audiobooks): list endpoint at GET /api/v1/audiobooks
Paginated list of media_items with type='audiobook' scoped to the
caller's accessible libraries via the existing access filter.
Mirrors silo's existing list-style handlers for movies and series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:33:21 +02:00
RXWatcherandClaude Opus 4.7 bf86b031b3 docs(audiobooks): implementation plan sub-plan 3 (API + frontend MVP)
Third of six sub-plans. 9 tasks: three REST endpoints (list/detail/
progress), TanStack Query hooks + types, three React pages
(Library/Detail/Player), and navigation integration. Scoped to MVP —
author/series indices, smart collections, share links, and other
nice-to-haves from the spec are deferred. Streaming reuses silo's
existing /api/v1/stream/{session_id}; no new transcode code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:30:52 +02:00
RXWatcherandClaude Opus 4.7 42205e0e8b 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>
2026-05-24 14:27:12 +02:00
RXWatcherandClaude Opus 4.7 2cbb8514df 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>
2026-05-24 14:21:21 +02:00
RXWatcherandClaude Opus 4.7 9b50f0a88d 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>
2026-05-24 14:11:31 +02:00
RXWatcherandClaude Opus 4.7 51dc8ef53a 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>
2026-05-24 14:07:09 +02:00
RXWatcherandClaude Opus 4.7 5e5e6c18b7 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>
2026-05-24 14:04:44 +02:00
RXWatcherandClaude Opus 4.7 90792cb4e0 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>
2026-05-24 13:52:48 +02:00
RXWatcherandClaude Opus 4.7 fab95e291f 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>
2026-05-24 13:50:46 +02:00
RXWatcherandClaude Opus 4.7 5f1ca3f825 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>
2026-05-24 13:28:40 +02:00
RXWatcherandClaude Opus 4.7 da3d3198c8 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>
2026-05-24 13:26:38 +02:00
RXWatcherandClaude Opus 4.7 31704a2855 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>
2026-05-24 13:25:49 +02:00
RXWatcherandClaude Opus 4.7 bc19ce34f6 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>
2026-05-24 12:56:09 +02:00