feat(filters+nav): author navigation end-to-end — arrays on every media response, deep-linkable bookshelf filters, clickable authors

Completes the A2c acceptance scenario: click an author on a book
detail page -> land on the bookshelf filtered to everything that
author wrote (including their share of co-authored books).

- Media detail + unscoped list resolved maps expose subtitle,
  explicit, authors, publishers (the library-scoped raw branch and
  the search endpoint already did — raw rows follow the SELECT).
- SearchMediaItemsUnified SELECT carries the new columns; the search
  endpoint serializes the row directly, so clients get them there too.
- book_detail.templ renders each author as a link to
  /bookshelf?author_filter=<name>.
- Bookshelf honors filter params in the URL on load (author_filter,
  series_filter, genre_filter, publisher_filter, tags_filter,
  narrator_filter, language_filter): pre-fills the form, applies it to
  the first grid load. Comma-joined values are multi-select via the
  server-side string_to_array split. Filtered views are shareable.

Pins widened: search rows (+7 keys), resolved keys (+4); changelog
entries in docs/contract_pins.md. Android follow-up plan written to
~/.opencode/plan/bookhoard-app-v2-compat.md.
This commit is contained in:
John O'Keefe committed 2026-10-19 16:16:58 -04:00
1 parent f94e7472b4
commit 7537063cb7
8 files changed
+851 -728

No files matched your search

+21
View File
@@ -13201,7 +13201,14 @@ SELECT
mi.tags_search,
mi.contributors_search,
mi.narrators,
mi.narrators_search,
mi.abridged,
mi.subtitle,
mi.explicit,
mi.authors,
mi.authors_search,
mi.publishers,
mi.publishers_search,
mi.audio_metadata,
mi.file_sha256,
mi.opf_identifier,
@@ -13429,7 +13436,14 @@ type SearchMediaItemsUnifiedRow struct {
TagsSearch []string `db:"tags_search" json:"tags_search"`
ContributorsSearch []string `db:"contributors_search" json:"contributors_search"`
Narrators []string `db:"narrators" json:"narrators"`
NarratorsSearch []string `db:"narrators_search" json:"narrators_search"`
Abridged bool `db:"abridged" json:"abridged"`
Subtitle pgtype.Text `db:"subtitle" json:"subtitle"`
Explicit bool `db:"explicit" json:"explicit"`
Authors []string `db:"authors" json:"authors"`
AuthorsSearch []string `db:"authors_search" json:"authors_search"`
Publishers []string `db:"publishers" json:"publishers"`
PublishersSearch []string `db:"publishers_search" json:"publishers_search"`
AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"`
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
@@ -13508,7 +13522,14 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt
&i.TagsSearch,
&i.ContributorsSearch,
&i.Narrators,
&i.NarratorsSearch,
&i.Abridged,
&i.Subtitle,
&i.Explicit,
&i.Authors,
&i.AuthorsSearch,
&i.Publishers,
&i.PublishersSearch,
&i.AudioMetadata,
&i.FileSha256,
&i.OpfIdentifier,
+7
View File
@@ -662,7 +662,14 @@ SELECT
mi.tags_search,
mi.contributors_search,
mi.narrators,
mi.narrators_search,
mi.abridged,
mi.subtitle,
mi.explicit,
mi.authors,
mi.authors_search,
mi.publishers,
mi.publishers_search,
mi.audio_metadata,
mi.file_sha256,
mi.opf_identifier,
+8
View File
@@ -844,7 +844,9 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
"id": uuid.UUID(item.ID.Bytes).String(),
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
"title": item.Title,
"subtitle": utils.PgTextString(item.Subtitle),
"author": utils.PgTextString(item.Author),
"authors": item.Authors,
"isbn": utils.PgTextString(item.Isbn),
"description": utils.PgTextString(item.Description),
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
@@ -857,6 +859,8 @@ func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
"asin": utils.PgTextString(item.Asin),
"date_published": item.DatePublished.Time.Format("2006-01-02"),
"publisher": utils.PgTextString(item.Publisher),
"publishers": item.Publishers,
"explicit": item.Explicit,
"contributors": item.Contributors,
"language": utils.PgTextString(item.Language),
"edition": utils.PgTextString(item.Edition),
@@ -916,7 +920,9 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
"id": uuid.UUID(item.ID.Bytes).String(),
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
"title": item.Title,
"subtitle": utils.PgTextString(item.Subtitle),
"author": utils.PgTextString(item.Author),
"authors": item.Authors,
"isbn": utils.PgTextString(item.Isbn),
"description": utils.PgTextString(item.Description),
"file_path": utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
@@ -929,6 +935,8 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
"asin": utils.PgTextString(item.Asin),
"date_published": item.DatePublished.Time.Format("2006-01-02"),
"publisher": utils.PgTextString(item.Publisher),
"publishers": item.Publishers,
"explicit": item.Explicit,
"contributors": item.Contributors,
"language": utils.PgTextString(item.Language),
"edition": utils.PgTextString(item.Edition),