156 lines
7.4 KiB
Markdown
156 lines
7.4 KiB
Markdown
---
|
|
id: doc-14
|
|
title: "Research: Exposing additional data points for collection API"
|
|
type: specification
|
|
created_date: "2026-05-08 13:03"
|
|
updated_date: "2026-05-08 14:01"
|
|
tags:
|
|
- api
|
|
- collection
|
|
- research
|
|
---
|
|
|
|
# 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 from `records`
|
|
- `SearchIndex` has a subset of Record columns; no `musicbrainz_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:**
|
|
|
|
1. Execute existing SearchIndex query as before
|
|
2. Pass results through `Collection.enrich/1` which:
|
|
a. Collects all record IDs → batch-queries `records` table for `release_ids` and main artist MBIDs
|
|
b. Collects all release IDs → batch-queries `scrobbled_tracks` for play counts and most recent scrobble per release group
|
|
c. Collects unique artist MBIDs → batch-queries `artist_infos` for country data
|
|
d. Optionally queries `record_releases` for collected edition details
|
|
3. 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/1` to accept enriched data
|
|
- Update `CollectionController` actions 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,releases` query 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:**
|
|
|
|
1. Migration: add `scrobble_count INTEGER`, `last_listened_at INTEGER`, `artist_country TEXT`, `release_details TEXT` to SearchIndex
|
|
2. Update triggers to compute or join these values when records change
|
|
3. Scrobble data would need separate triggers on `scrobbled_tracks` to update SearchIndex
|
|
|
|
**Changes required:**
|
|
|
|
- Migration with `ALTER VIRTUAL TABLE` (FTS5 has limitations)
|
|
- New triggers on `records` for artist country, release details
|
|
- New triggers on `scrobbled_tracks` for scrobble counts and last listened at
|
|
- Update `essential_fields()` to include new columns
|
|
- Update `CollectionJSON` to 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_tracks` fire 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_infos` in 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:**
|
|
|
|
1. Index endpoints remain unchanged
|
|
2. New show endpoint fetches a record + all enrichment data
|
|
3. Can use `ListeningStats.play_count/1` and `get_last_listened_track/1` directly 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:
|
|
|
|
1. It satisfies the requirement of adding data to existing endpoints without architectural changes
|
|
2. Performance impact is minimal (~3 batch queries regardless of page size)
|
|
3. No migration or trigger complexity
|
|
4. Can be implemented incrementally (scrobble stats first, then country, then releases)
|
|
5. 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
|
|
|
|
1. **"Collected release information" — what specifically?** Options include:
|
|
- Just the `selected_release_id` (already in SearchIndex — trivial)
|
|
- List of `release_ids` with their formats/types from `record_releases`
|
|
- Full MusicBrainz release data (tracks, dates, countries) from `records.musicbrainz_data`
|
|
- Need clarification from stakeholder
|
|
|
|
2. **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`?
|
|
|
|
3. **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.
|