Backend API documentation for contributors. All endpoints are prefixed with /api/v1.
http://localhost:4400/api/v1
Familiar uses profile-based multi-user support. Include the profile ID header:
X-Profile-ID: <profile-uuid>
Some endpoints (play history, favorites) require this header.
- Paginated lists return
{ items, total, page, page_size } - Errors return the standard envelope (see ERROR-CONTRACTS.md)
GET /library/artists
Get distinct artists with aggregated statistics.
| Parameter | Type | Default | Description |
|---|---|---|---|
search |
string | - | Filter by artist name |
sort_by |
string | name |
Sort by: name, track_count, album_count |
page |
int | 1 | Page number |
page_size |
int | 100 | Items per page |
curl "http://localhost:4400/api/v1/library/artists?sort_by=track_count&page_size=10"{
"items": [
{
"name": "Radiohead",
"track_count": 94,
"album_count": 9,
"first_track_id": "a1b2c3d4-..."
}
],
"total": 156,
"page": 1,
"page_size": 10
}GET /library/albums
Get distinct albums with metadata.
| Parameter | Type | Default | Description |
|---|---|---|---|
artist |
string | - | Filter by artist name |
search |
string | - | Search album or artist name |
sort_by |
string | name |
Sort by: name, year, track_count, artist |
page |
int | 1 | Page number |
page_size |
int | 100 | Items per page |
curl "http://localhost:4400/api/v1/library/albums?artist=Radiohead"{
"items": [
{
"name": "OK Computer",
"artist": "Radiohead",
"year": 1997,
"track_count": 12,
"first_track_id": "a1b2c3d4-..."
}
],
"total": 9,
"page": 1,
"page_size": 100
}GET /tracks
Get tracks with comprehensive filtering and pagination.
| Parameter | Type | Default | Description |
|---|---|---|---|
search |
string | - | Search title, artist, or album |
artist |
string | - | Filter by artist |
album |
string | - | Filter by album |
genre |
string | - | Filter by genre |
year_from |
int | - | Minimum year (inclusive) |
year_to |
int | - | Maximum year (inclusive) |
energy_min |
float | - | Minimum energy (0-1) |
energy_max |
float | - | Maximum energy (0-1) |
valence_min |
float | - | Minimum valence (0-1) |
valence_max |
float | - | Maximum valence (0-1) |
include_features |
bool | false | Include audio analysis features |
page |
int | 1 | Page number |
page_size |
int | 50 | Items per page (max 200) |
curl "http://localhost:4400/api/v1/tracks?artist=Radiohead&include_features=true&page_size=5"{
"items": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"file_path": "/music/Radiohead/OK Computer/01 - Airbag.flac",
"title": "Airbag",
"artist": "Radiohead",
"album": "OK Computer",
"album_artist": "Radiohead",
"album_type": "album",
"track_number": 1,
"disc_number": 1,
"year": 1997,
"genre": "Alternative Rock",
"duration_seconds": 284.5,
"format": "flac",
"analysis_version": 3,
"features": {
"bpm": 122.5,
"key": "A minor",
"energy": 0.72,
"valence": 0.45,
"danceability": 0.65,
"acousticness": 0.12,
"instrumentalness": 0.05,
"speechiness": 0.03
}
}
],
"total": 94,
"page": 1,
"page_size": 5
}GET /tracks/{track_id}
Get a single track with its analysis features.
curl "http://localhost:4400/api/v1/tracks/a1b2c3d4-e5f6-7890-abcd-ef1234567890"GET /tracks/{track_id}/stream
Stream audio file with HTTP range request support for seeking.
# Full file
curl "http://localhost:4400/api/v1/tracks/{id}/stream" -o track.mp3
# Partial content (seeking)
curl -H "Range: bytes=0-65535" "http://localhost:4400/api/v1/tracks/{id}/stream"Returns 206 Partial Content with Content-Range header for range requests.
GET /tracks/{track_id}/artwork
Get album artwork (extracted from audio file and cached).
| Parameter | Type | Default | Description |
|---|---|---|---|
size |
string | full |
Size: full or thumb |
curl "http://localhost:4400/api/v1/tracks/{id}/artwork?size=thumb" -o cover.jpgReturns JPEG image with 1-year cache header.
GET /artwork/coverage
How much of the library has real cover art (ADR-0058 phase 5).
{
"total_albums": 3927,
"with_artwork": 3568,
"generated": 661,
"without_artwork": 359
}total_albums is counted the way /library/albums and /library/stats count, so all three agree.
generated is a subset of with_artwork, not a fourth category: those albums have a thumbnail
on disk, but it is a placeholder Familiar drew rather than a real cover. Real art is
with_artwork - generated. Reporting them as covered would call a library with no real art at all
fully covered.
Artwork existence is a filesystem fact — no column records it — so this stats one thumbnail per
album in a worker thread. Roughly 4k stat calls answer in under a second, but it is a separate
endpoint from /library/stats so the dashboard does not sweep the disk on every load.
GET /tracks/{track_id}/lyrics
Get lyrics for a track (synced with timestamps if available).
curl "http://localhost:4400/api/v1/tracks/{id}/lyrics"{
"synced": true,
"lines": [
{ "time": 12.5, "text": "In the next world war" },
{ "time": 15.2, "text": "In a jackknifed juggernaut" }
],
"plain_text": "In the next world war\nIn a jackknifed juggernaut...",
"source": "lrclib"
}GET /tracks/{track_id}/similar
Find similar tracks using CLAP embedding similarity (pgvector cosine distance).
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 10 | Number of results (max 50) |
curl "http://localhost:4400/api/v1/tracks/{id}/similar?limit=5"GET /library/years
Get track counts by year for timeline visualization.
curl "http://localhost:4400/api/v1/library/years"{
"years": [
{ "year": 1997, "track_count": 24, "album_count": 2, "artist_count": 2 },
{ "year": 1998, "track_count": 18, "album_count": 1, "artist_count": 1 }
],
"total_with_year": 1250,
"total_without_year": 45,
"min_year": 1965,
"max_year": 2024
}GET /library/mood-distribution
Get energy × valence distribution for heatmap visualization.
| Parameter | Type | Default | Description |
|---|---|---|---|
grid_size |
int | 10 | Grid resolution (cells per axis) |
curl "http://localhost:4400/api/v1/library/mood-distribution?grid_size=10"{
"cells": [
{
"energy_min": 0.2,
"energy_max": 0.3,
"valence_min": 0.5,
"valence_max": 0.6,
"track_count": 25,
"sample_track_ids": ["a1b2c3d4-...", "e5f6g7h8-..."]
}
],
"grid_size": 10,
"total_with_mood": 1180,
"total_without_mood": 115
}GET /library/map
Get 2D positions for artists/albums based on audio similarity using UMAP.
| Parameter | Type | Default | Description |
|---|---|---|---|
entity_type |
string | artists |
Entity type: artists or albums |
limit |
int | 200 | Max entities (max 500) |
curl "http://localhost:4400/api/v1/library/map?entity_type=artists&limit=100"{
"nodes": [
{
"id": "radiohead",
"name": "Radiohead",
"x": 0.42,
"y": 0.68,
"track_count": 94,
"first_track_id": "a1b2c3d4-..."
}
],
"edges": [
{ "source": "radiohead", "target": "portishead", "weight": 0.85 }
],
"entity_type": "artists",
"total_entities": 100
}GET /library/stats
Get library statistics.
curl "http://localhost:4400/api/v1/library/stats"{
"total_tracks": 1295,
"total_albums": 156,
"total_artists": 89,
"albums": 1295,
"compilations": 0,
"soundtracks": 0,
"analyzed_tracks": 1250,
"pending_analysis": 45,
"pending_backfill": 0,
"pending_melodic": 0,
"pending_mood_tags": 0
}The three totals count active tracks only, and agree with /library/albums and /library/artists
— same grouping, same canonical tables. tests/test_library_stats.py asserts that agreement.
albums, compilations and soundtracks are deprecated and should not be displayed. Nothing
writes Track.album_type, so every row keeps the column default: albums always equals
total_tracks and the other two are always 0. This example previously showed 150 compilations
and 45 soundtracks, which no instance has ever returned. They remain on the wire only because
library is a generated tag (ADR-0007) and removing required fields breaks the Swift client.
POST /library/sync
Start a unified library sync (discover files → read metadata → analyze audio).
| Parameter | Type | Default | Description |
|---|---|---|---|
reread_unchanged |
bool | false | Re-read metadata even for unchanged files |
curl -X POST "http://localhost:4400/api/v1/library/sync"{
"status": "started",
"message": "Library sync started"
}GET /library/sync/status
Get current sync progress.
curl "http://localhost:4400/api/v1/library/sync/status"{
"status": "running",
"message": "Reading metadata...",
"progress": {
"phase": "reading",
"phase_message": "Reading metadata...",
"files_discovered": 1500,
"files_processed": 750,
"files_total": 1500,
"new_tracks": 45,
"updated_tracks": 12,
"unchanged_tracks": 693,
"tracks_analyzed": 0,
"tracks_pending_analysis": 45,
"analysis_percent": 0.0
}
}POST /library/sync/cancel
Cancel a running library sync.
curl -X POST "http://localhost:4400/api/v1/library/sync/cancel"GET /library/analysis/status
Get audio analysis progress with stuck detection.
curl "http://localhost:4400/api/v1/library/analysis/status"{
"status": "running",
"total": 1295,
"analyzed": 1250,
"pending": 45,
"failed": 0,
"percent": 96.5,
"current_file": "Processing 3 tracks...",
"with_embeddings": 1200,
"without_embeddings": 50,
"embeddings_enabled": true,
"embeddings_disabled_reason": null
}POST /library/analysis/start
Manually trigger analysis for unanalyzed tracks.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 500 | Max tracks to queue |
curl -X POST "http://localhost:4400/api/v1/library/analysis/start?limit=100"{
"status": "started",
"queued": 45,
"message": "Queued 45 tracks for analysis"
}POST /library/analysis/cancel
Cancel running analysis tasks.
curl -X POST "http://localhost:4400/api/v1/library/analysis/cancel"POST /library/import
Upload a zip or audio file for import.
curl -X POST "http://localhost:4400/api/v1/library/import" \
-F "file=@album.zip"{
"status": "processing",
"message": "Imported 12 files, scanning for metadata...",
"import_path": "/music/_imports/2024-01-15_143022",
"files_found": 12,
"files": ["01 - Track One.flac", "02 - Track Two.flac"]
}POST /library/import/preview
Extract and scan files without importing. Returns session ID for later execution.
curl -X POST "http://localhost:4400/api/v1/library/import/preview" \
-F "file=@album.zip"{
"session_id": "abc123",
"tracks": [
{
"filename": "01 - Track One.flac",
"relative_path": "Artist/Album",
"detected_artist": "Artist Name",
"detected_album": "Album Name",
"detected_title": "Track One",
"detected_track_num": 1,
"format": "flac",
"duration_seconds": 245.5,
"file_size_bytes": 35000000
}
],
"total_size_bytes": 420000000,
"estimated_sizes": { "flac": 420000000, "mp3_320": 85000000 },
"has_convertible_formats": true
}POST /library/import/execute
Execute import with user options.
curl -X POST "http://localhost:4400/api/v1/library/import/execute" \
-H "Content-Type: application/json" \
-d '{
"session_id": "abc123",
"tracks": [...],
"options": {
"format": "original",
"organization": "imports",
"duplicate_handling": "rename",
"queue_analysis": true
}
}'GET /library/imports/recent
Get list of recent import directories.
curl "http://localhost:4400/api/v1/library/imports/recent?limit=5"GET /library/missing
Get all tracks with MISSING or PENDING_DELETION status.
curl "http://localhost:4400/api/v1/library/missing"{
"tracks": [
{
"id": "a1b2c3d4-...",
"title": "Track Title",
"artist": "Artist",
"album": "Album",
"file_path": "/old/path/track.flac",
"status": "missing",
"missing_since": "2024-01-10T12:00:00",
"days_missing": 5
}
],
"total_missing": 3,
"total_pending_deletion": 0
}POST /library/missing/relocate
Search a folder for missing files and relocate them by filename match.
curl -X POST "http://localhost:4400/api/v1/library/missing/relocate" \
-H "Content-Type: application/json" \
-d '{"search_path": "/new/music/location"}'{
"found": 2,
"not_found": 1,
"relocated_tracks": [
{
"id": "a1b2c3d4-...",
"title": "Track Title",
"old_path": "/old/path/track.flac",
"new_path": "/new/music/location/track.flac"
}
]
}POST /library/missing/{track_id}/locate
Manually set a new path for a missing track.
curl -X POST "http://localhost:4400/api/v1/library/missing/{id}/locate" \
-H "Content-Type: application/json" \
-d '{"new_path": "/new/path/to/track.flac"}'DELETE /library/missing/{track_id}
Permanently delete a missing track from the database.
curl -X DELETE "http://localhost:4400/api/v1/library/missing/{id}"DELETE /library/missing/batch
Delete multiple missing tracks.
curl -X DELETE "http://localhost:4400/api/v1/library/missing/batch" \
-H "Content-Type: application/json" \
-d '{"track_ids": ["id1", "id2", "id3"]}'Requires X-Profile-ID header.
POST /tracks/{track_id}/played
Record that a track was played.
curl -X POST "http://localhost:4400/api/v1/tracks/{id}/played" \
-H "X-Profile-ID: your-profile-id" \
-H "Content-Type: application/json" \
-d '{"duration_seconds": 245.5}'{
"track_id": "a1b2c3d4-...",
"play_count": 15,
"total_play_seconds": 3682.5
}GET /tracks/stats/plays
Get play statistics for the current profile.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 10 | Number of top tracks (max 50) |
curl "http://localhost:4400/api/v1/tracks/stats/plays?limit=5" \
-H "X-Profile-ID: your-profile-id"{
"total_plays": 1250,
"total_play_seconds": 312500.0,
"unique_tracks": 450,
"top_tracks": [
{
"id": "a1b2c3d4-...",
"title": "Airbag",
"artist": "Radiohead",
"play_count": 42,
"total_play_seconds": 11949.0,
"last_played_at": "2024-01-15T18:30:00"
}
]
}