7.4 KiB
id, title, type, created_date, updated_date, tags
| id | title | type | created_date | updated_date | tags | |||
|---|---|---|---|---|---|---|---|---|
| doc-14 | Research: Exposing additional data points for collection API | specification | 2026-05-08 13:03 | 2026-05-08 14:01 |
|
Research: Exposing additional data points for collection API
Problem
The /api/v1/collection/* endpoints currently return a flat subset of SearchIndex fields. We need to add four new data points per record:
| Data Point | Source | Current Access Pattern | Cost |
|---|---|---|---|
| Scrobble count | scrobbled_tracks |
ListeningStats.play_count/1 |
2 queries per record |
| Last listened at | scrobbled_tracks |
ListeningStats.get_last_listened_track/1 |
2 queries per record |
| Collected release info | records.musicbrainz_data, record_releases |
Record.releases/1, Record.selected_release/1 |
Requires Record struct (not in SearchIndex) |
| Artist country | artist_infos.musicbrainz_data |
ArtistInfo.country/1 |
1 query per artist |
Current architecture
- Collection API queries
records_search_index(FTS5 virtual table), populated by triggers fromrecords SearchIndexhas a subset of Record columns; nomusicbrainz_data,artist_infos, or scrobble data- The JSON view (
CollectionJSON) maps SearchIndex structs directly
Implementation routes
Route A: Post-query hydration layer (recommended)
Add a new enrichment module in MusicLibrary.Collection that batch-fetches additional data after the initial SearchIndex query.
How it works:
- Execute existing SearchIndex query as before
- Pass results through
Collection.enrich/1which: a. Collects all record IDs → batch-queriesrecordstable forrelease_idsand main artist MBIDs b. Collects all release IDs → batch-queriesscrobbled_tracksfor play counts and most recent scrobble per release group c. Collects unique artist MBIDs → batch-queriesartist_infosfor country data d. Optionally queriesrecord_releasesfor collected edition details - Merge enriched data into the JSON response map
Changes required:
- New functions:
Collection.enrich_scrobble_stats/1,Collection.enrich_artist_country/1,Collection.enrich_release_details/1 - Update
CollectionJSON.record/1to accept enriched data - Update
CollectionControlleractions to call enrichment - New tests: enriched response shape, batch query correctness, nil handling for records without scrobbles/artist info
Pros:
- No migrations or SearchIndex trigger changes
- Clean separation of concerns — enrichment is opt-in and independently testable
- Can add
?include=scrobbles,country,releasesquery params later for selective enrichment - Existing API shape unchanged for clients not reading new fields
Cons:
- Additional queries (but batchable: ~3-5 queries total regardless of result count)
- Slightly slower response for the index endpoint (est. 5-20ms added for 20 records with SQLite)
Performance profile:
- Batch scrobble lookup: 1 query with
WHERE album_mbid IN (...release_ids...)across all records in the page (constant query count, scales with page size not total table size) - Batch artist_info lookup: 1 query with
WHERE id IN (...artist_mbids...) - Batch record_releases lookup: 1 query with
WHERE record_id IN (...record_ids...) - Total: ~3 additional queries for index endpoint (20 records), ~2 for single-record endpoints
- Memory: minimal — aggregated counts/dates, not loading full tables
Route B: Add columns to SearchIndex + triggers
Extend the FTS5 virtual table and its sync triggers to include the new data points directly.
How it works:
- Migration: add
scrobble_count INTEGER,last_listened_at INTEGER,artist_country TEXT,release_details TEXTto SearchIndex - Update triggers to compute or join these values when records change
- Scrobble data would need separate triggers on
scrobbled_tracksto update SearchIndex
Changes required:
- Migration with
ALTER VIRTUAL TABLE(FTS5 has limitations) - New triggers on
recordsfor artist country, release details - New triggers on
scrobbled_tracksfor scrobble counts and last listened at - Update
essential_fields()to include new columns - Update
CollectionJSONto map new fields
Pros:
- Zero additional queries per API request
- Consistent with existing SearchIndex-first architecture
Cons:
- FTS5 virtual tables are complex to alter; may require full rebuild
- Triggers on
scrobbled_tracksfire on every scrobble import (high write volume), keeping SearchIndex in sync adds write overhead - Data staleness: scrobble counts change frequently; SearchIndex would lag behind (or be constantly rebuilt)
- Tight coupling: SearchIndex purpose is search, not analytics
- Artist country requires joining
artist_records→artist_infosin a trigger, which is fragile
Route C: Separate details endpoint
Keep index endpoints lightweight and add a new GET /api/v1/collection/:id endpoint for enriched single-record views.
How it works:
- Index endpoints remain unchanged
- New show endpoint fetches a record + all enrichment data
- Can use
ListeningStats.play_count/1andget_last_listened_track/1directly since it's a single record
Changes required:
- New route and controller action
- New JSON view for enriched record
- Tests for new endpoint
Pros:
- Zero impact on existing index endpoint performance
- Per-record functions (
play_count/1,get_last_listened_track/1) work for single records without batching - Clean API design: lightweight list, heavy detail
Cons:
- Clients need two API calls to get enriched data for multiple records
- Doesn't satisfy the requirement if the user wants enriched data in index responses
Recommendation
Route A (post-query hydration) is the recommended approach because:
- It satisfies the requirement of adding data to existing endpoints without architectural changes
- Performance impact is minimal (~3 batch queries regardless of page size)
- No migration or trigger complexity
- Can be implemented incrementally (scrobble stats first, then country, then releases)
- Easily extended to support
?include=query parameters for selective enrichment in the future
Routes B and C have merit but introduce either significant complexity (B) or don't fully satisfy the requirement (C).
Open questions for decision
-
"Collected release information" — what specifically? Options include:
- Just the
selected_release_id(already in SearchIndex — trivial) - List of
release_idswith their formats/types fromrecord_releases - Full MusicBrainz release data (tracks, dates, countries) from
records.musicbrainz_data - Need clarification from stakeholder
- Just the
-
Always include vs. opt-in? Should these fields always appear in the response, or should they be gated behind a query parameter like
?include=scrobbles,country? -
Artist country — which artist? A record can have multiple artists (collaborations). Should we show country for the main artist only, or all artists? Main artist is simpler and more predictable.