Commit Graph
13 Commits
Author SHA1 Message Date
John O'Keefe 7e586df48a feat(facets): facet registry, Batch A server params, genre model v2, facets endpoint
Phase 1 + backend of Phase 2 of the faceted-navigation plan. One
declarative facet registry (services/facet_registry.go) drives the
bookshelf filter system; a drift-guard unit test reflects over the
generated SearchMediaItemsUnifiedParams so a registry entry and its
sqlc.narg block can never disagree silently.

- Nine new filter params on SearchMediaItemsUnified: story_arc,
  imprint, edition (scalar-fuzzy), age_rating, format_group,
  manga_type, reading_direction (exact), explicit, abridged
  (presence-bound bools). Comma = OR-within multi-select on every
  text facet; AND-across-facets unchanged.
- Genre model v2 (user rule: a subgenre is not a genre, it's a tag):
  genre_lexicon.go rebuilt as shelf set + subgenre->shelf map;
  pickSmartGenre picks direct shelf > mapped subgenre > Fiction floor
  > empty (unknown-only stays tags-only). Type-words (Manga, Comics,
  Graphic Novel) join the demote list. Both 'adventure' and 'action &
  adventure' are shelves — provider data uses both and the verified
  traces pin them.
- Language filter UX: comma elements normalize through the ingest
  language map, so display names ('English') match rows stored as ISO
  codes ('en'). The language autocomplete now serves display names.
- NEW GET /api/media-items/facets?facet=X&library_id=... — registry-
  driven value+count listing (14 static per-column queries, family-lens
  join); language values carry display names; bool/range facets 400.
- Family-lens gates: an explicit hidden-library library_id is now a
  403 on the search endpoints (was a silent empty set) — JSON branch
  and HTMX grid partial both.
- handlers.ParseMediaSearchParams is the SINGLE param parser for the
  search JSON endpoint and the HTMX grid partial; the HTML branch
  previously dropped publisher_filter and narrator_filter silently.

Pins: docs/contract_pins.md changelog entries (additive request
params; new facets endpoint). Tests: facet_registry_test.go (shape +
SQL drift guard + library-type union), series_parsing_test.go genre-v2
cases, facets_integration_test.go (param filtering e2e, counts,
language display, 403s) — container runner.
2026-10-19 18:59:23 -04:00
John O'Keefe 86f0af632e feat(audiobooks): A2 — narrator metadata, audio-aware UX, player polish, bookmarks
A2a (metadata round): narrators TEXT[] + narrators_search (tags
pattern: accumulator + GIN pairs) + abridged bool on media_items;
scanner fills from the Libation/ABS sidecar (narrators verbatim,
abridged flag) with audio-payload promotion; editorial fields ride
the real columns while audio_metadata stays scan-derived (duration/
codec/chapters). Detail/edit/save paths carry the fields; the web
edit dialog grows an Audio section (Narrator(s) comma-input, Abridged
toggle, scanned duration read-only); search weights + narrator filter
(word_similarity over narrators_search) + narrator autocomplete added.

A2c (per-media-type wording): copy helpers keyed on format_group —
Listen Now, Mark Complete/Not Complete, Listening Progress, Your
Listening Stats, Time Listening, Time listened (pages tile swapped for
audio), Position H:MM:SS / total H:MM:SS on detail + progress pages
(duration parsed from audio_metadata). Dashboard: system collections
swap to Continue Listening / Recently Listened at DISPLAY time when
the audiobooks library is selected (stored names unchanged; restores
keep working); week card = Listening Time / Sessions / Books Touched;
cover-corner badges identify audiobook (headphones) / comic / manga
cards on mixed grids — books stay unbadged; hover action icon follows
the media type.

A2b (player): the scrubber binds value to the playhead with an explicit
scrub guard (the handle previously froze at its initial max); header
row = reader-style back arrow + Chapter n/total launcher; chapters
open in a floating top-down drawer (current chapter highlighted, page
never shifts); Skip/Speed rows share one label-left layout; honest
wall-clock session accounting (playing-only deltas, paused flush).

A2d (bookmarks): note-bearing time bookmarks ride the existing
media_bookmarks substrate (notes column + sync-aware service already
there) — audio locator convention audio:<seconds>, chapter_number
filled, auto-title '<chapter> — H:MM:SS', capture popover with optional
note, drawer with jump/edit-note/delete. Zero server changes needed.

Verified live on the Libation sample: narrators backfilled by rescan;
PUT edit persisted + overrides detected (narrators, abridged); force
rescan preserved the user edit; reset-to-scanned-of-defaults restored
truth; narrator filter/autocomplete scoped by library; dashboard
listsening wording + badges render; bookmark save with note deduped to
one row.
2026-10-15 18:03:35 -04:00
John O'Keefe 3d88f0b960 feat(search): publisher_filter for the media search endpoint
Adds publisher_filter to SearchMediaItems/SearchMediaItemsUnified
(fuzzy word_similarity against mi.publisher, mirroring genre_filter),
plumbs it through services.SearchParams and the search handler, and
extends the GREATEST relevance ranking to include publisher matches.
Serves the app's author/publisher/genre/tag click-through browses
(UX pass item 5 — publisher was the only facet without a server
filter).
2026-09-27 19:55:58 -04:00
john-okeefe dc07a2f19f fix(search): add json tags to FieldValue struct for correct API response
FieldValue struct had no json tags, so Go marshaled fields as uppercase
(Value, Count, Score) but frontend expected lowercase (value, count).
This caused all autocomplete dropdowns (tags, author, series, language)
to silently fail — tagSuggestions[].value was undefined, crashing
toLowerCase() calls and producing empty dropdowns.
2026-05-10 16:12:10 -04:00
john-okeefe 9ccff320a1 refactor: use errors.Is()/errors.AsType() for error comparison and rename shadowed variables
Replace direct error equality checks (err == pgx.ErrNoRows, err != http.ErrServerClosed)
with the idiomatic errors.Is() function throughout handlers, services, middleware, and app
startup. This correctly handles wrapped error chains.

Also replace a raw type assertion (*HTTPError) with errors.AsType[*HTTPError]() in the
error handler middleware for consistency.

Additionally, rename shadowed variables for clarity:
- sidecar.go: config -> sidecarConfig, systemConfig (shadowed package-level vars)
- media_scanner.go: uuid -> uuidString (shadowed the uuid package import)
2026-04-20 21:20:22 -04:00
john-okeefe ba243c223d fix: implement proper 3-state boolean handling in backend
Updated the backend services and handlers to properly detect and pass
the has_cover parameter's validity state to the database layer.

Changes:
- services/search.go: Changed HasCover type from bool to pgtype.Bool
  to support 3-state logic (NULL, TRUE, FALSE)
- handlers/media.go: Fixed 3-state detection by checking if has_cover
  exists in query params before setting Valid flag
- router/search.go: Fixed 3-state detection to match media.go logic
- router/frontend.go: Use pgtype.Bool{Valid: false} for SSR initial
  load to ensure no filtering occurs on first page load

The key fix is detecting whether the has_cover parameter was actually
sent in the request:
- Parameter not sent → pgtype.Bool{Bool: false, Valid: false}
- Parameter sent as "true" → pgtype.Bool{Bool: true, Valid: true}
- Parameter sent as "false" → pgtype.Bool{Bool: false, Valid: true}

Previously, media.go was hardcoding Valid: true, which meant it was
always filtering by has_cover=false (only books without covers) when
the parameter wasn't sent, causing searches to incorrectly return
0 results for queries like "1984".

This ensures consistency between the JSON API endpoint (media.go) and
the HTML endpoint (search.go), and fixes the critical bug where SSR
was returning 0 books on initial page load.
2026-03-27 18:07:51 -04:00
john-okeefe a33d521492 feat(search): add ExecuteSearch method to SearchService
Add shared search method that returns results with count. This method will be used by both JSON API endpoints and HTML rendering for HTMX, avoiding duplicate business logic.

- Extracts common search logic into reusable service method
- Returns search results with total count for pagination
- Follows DRY principle by eliminating duplicated search code
2026-03-27 14:50:21 -04:00
john-okeefe be31cc88f1 feat: enhance search with date-prioritized year filtering and true exact matching
Improve media item search functionality with two key enhancements:

1. Date-prioritized year filtering:
   - Prioritize date_published over copyright_year for year range queries
   - Fall back to copyright_year when date_published is NULL
   - Extract year from date_published timestamp for comparison

2. True exact search matching:
   - Replace ILIKE pattern matching with exact equality for quoted queries
   - Use search_query directly instead of wildcard pattern for exact matches
   - Remove SearchPattern parameter and related wildcard logic
   - Add COALESCE handling for author/series NULL values in exact matches

These changes make year filtering more accurate with published dates
and provide genuine exact matching when users wrap queries in quotes.

Refs internal/database/queries/queries.sql:475, internal/services/search.go:62
2026-03-26 15:35:31 -04:00
john-okeefe bb8b4f9f63 feat: implement tags filter in service layer
- Add TagsFilter string to SearchParams struct
- Update dbParams building to include tags_filter
- Add tags case to SearchFieldValues service for autocomplete
- Handle SearchTagsValues query results

Enables the backend service layer to process tag filtering requests
and provide autocomplete suggestions for tag values.

Relates to IMPLEMENTATION_TAGS_FILTER.md Phase 2
2026-03-25 20:38:11 -04:00
john-okeefe a643bd43b8 refactor: clarify database parameter validation in search service
Add comment to document that filter parameters use pgtype.Text
with explicit Valid=true flag to ensure proper SQL parameter handling.
This clarifies the intent behind the parameter building logic.

Improves code documentation for future maintenance.
2026-03-25 18:03:02 -04:00
john-okeefe bd3057ec80 fix: correctly handle NULL library_id in search service
Updates SearchMediaItemsUnified to conditionally set LibraryID parameter
only when it's valid. Previously, the code always set LibraryID in the
dbParams struct, which caused pgx to pass a zero UUID instead of NULL
to PostgreSQL.

New behavior:
  - Only sets dbParams.LibraryID when params.LibraryID.Valid is true
  - When library_id is empty, LibraryID is omitted from the struct
  - Go's zero value + pgx's "field not set" detection = NULL in SQL

Also fixes type mismatches in SearchFieldValues method where
SearchQuery parameter needed explicit pgtype.Text wrapping with
Valid=true flag for proper nullable text handling.

This ensures that omitting the library_id query parameter results in
searching across all libraries, not filtering by zero UUID.
2026-03-24 16:47:30 -04:00
john-okeefe 871d5eafe6 feat: add SearchService for unified search functionality
- Create SearchService with SearchMediaItemsUnified method
- Add SearchFieldValues method for autocomplete dropdown population
- Add parseSearchQuery helper for quote detection (exact vs fuzzy search)
- Move all business logic from handler to service layer
- Follow established service pattern (FiltersService, CollectionService)
- Service created inside handler constructor, not in main.go
- SearchParams struct supports all filter types + sort parameter
- FieldSearchParams struct for field-specific autocomplete queries
- Returns FieldValue results with count and similarity scores

This provides a clean service layer abstraction for search operations.
2026-03-23 22:37:40 -04:00
john-okeefe 9ab2796902 feat: implement SearchService with unified search logic
Create new SearchService to encapsulate all search business logic:

Features:
- Unified search combining text search with filters
- Fuzzy matching using pg_trgm word_similarity (threshold: 0.3)
- Exact search when query is wrapped in quotes
- Field-specific autocomplete for dropdowns (author, genre, series, language)
- Proper pagination with configurable limit/offset

Implementation details:
- SearchMediaItems: Routes to SearchMediaItemsUnified query
  * Detects exact search by checking for quotes in query
  * Builds search pattern for ILIKE matching (%term%)
  * Converts string filters to pgtype.Text with proper Valid flags

- SearchFieldValues: Routes to appropriate field-specific query
  * Uses switch statement to call correct query based on field_type
  * Returns []FieldValue with value, count, and similarity score
  * Handles all 4 field types: author, genre, series, language

Design pattern: Service layer separates business logic from handlers,
following project's established architecture (FiltersService, CollectionService).
2026-03-22 20:35:09 -04:00