furumi-fd
Localhost music library index manager. A small REST API over a local SQLite database that mirrors the library primitives of the furumusic schema (artists, releases, tracks, media files, genres, weighted genre tags, external metadata ids, popularity), so data can be migrated from furumusic 1:1 by an external script.
This service never touches real files. It only stores records about them. Audio processing, metadata extraction, artwork handling etc. are done by other software that talks to this API.
Running
cargo run
| Env var | Default | Meaning |
|---|---|---|
FURUMI_FD_DB |
furumi-fd.sqlite3 |
Path to the SQLite database file (created if missing) |
FURUMI_FD_LISTEN |
127.0.0.1:8321 |
Listen address |
FURUMI_FD_FEDERATION_DIR |
<db>.federation |
Directory for the federation identity and DHT state |
FURUMI_FD_MEDIA_ROOT |
(unset) | Root of the audio library; media_files.file_path resolves against it. Unset ⇒ playback is disabled, the index API keeps working. Files are only ever read. |
No authentication — localhost only by default.
GET / serves a small built-in web UI (embedded into the binary from
static/index.html) for browsing, adding and deleting artists, releases,
tracks and media file records — plus the Федерация page with the
federation settings, live statistics and network-wide search.
Federation (P2P library search)
Instances of furumi-fd can form a federated network: each publishes its
library index (artist names, release titles, track titles and small metadata
— never files or file paths) into a distributed hash table and can search
the libraries of all other participants. Built on the
music-dht / federation-net crates; peers
find each other automatically knowing only the shared network id (no
bootstrap servers, no invites).
Federation is off by default. Enable it on the Федерация page (or via
the API): tick the checkbox, enter a network id and save. Every instance
using the same id joins the same network; a different id forms a separate,
isolated network. The id is effectively a shared secret — pick something
unique like my-crew-music-7f3a.
While enabled, the library is re-synchronized into the DHT every minute (added/changed items are republished, removed ones are tombstoned), and the page shows the node ids, connected peers, published item count and the last sync outcome.
Federation API
GET /api/federation— settings + live node status.PUT /api/federation/settings—{enabled, network_id}; starts/stops the node immediately and persists the settings across restarts.GET /api/federation/search?q=&kind=— search the network (kindoptional:artist | release | track). Results carry the kind, name, artist names, year/type/duration and the owning peer's id.POST /api/federation/sync— force an immediate library sync.
Schema mapping (furumusic → furumi-fd)
| furumusic (PostgreSQL) | furumi-fd (SQLite) | Notes |
|---|---|---|
furumusic__media_file |
media_files |
uploaded_by_user_id dropped (no users here); uploader_name kept |
furumusic__artist |
artists |
same fields |
furumusic__release |
releases |
same fields, same release_type vocabulary |
furumusic__release_artist |
release_artists |
surrogate id dropped; PK (release_id, artist_id) |
furumusic__track |
tracks |
includes lastfm_listeners/playcount/rating/updated_at |
furumusic__track_artist |
track_artists |
surrogate id dropped; PK (track_id, artist_id, role) |
furumusic__genre |
genres |
same fields |
furumusic__track_genre |
track_genres |
surrogate id dropped; PK (track_id, genre_id) |
furumusic__entity_genre_tag |
entity_genre_tags |
same fields + unique key |
furumusic__external_metadata_id |
external_metadata_ids |
same fields + unique key |
furumusic__track_popularity_history |
track_popularity_history |
schema only, no API endpoints yet |
Shared conventions kept from furumusic:
- timestamps are
TEXTin%Y-%m-%dT%H:%M:%SZformat; name_sort/title_sort= trimmed lowercase of the display value;media_files.file_pathis relative to the media root;entity_kindis one ofartist | release | track;- track artist
roleis one ofmain | featuring | remixer | producer; release_typeis one ofalbum | single | ep | compilation | mixtape | live | soundtrack | remix | demo.
Migration notes
- Every create endpoint accepts an optional explicit
id, plus optionalcreated_at/updated_atoverrides — so a migration script can replay rows through the API preserving original ids and timestamps. Insert in FK order: media files → artists → releases (+ artist links) → tracks (+ artist/genre links) → genre tags / external ids. - Alternatively write straight into the SQLite file; the schema is in
src/schema.rs.
REST API
Base path /api. All bodies are JSON. Errors come back as
{"error": "..."} with 400/404/409/500.
GET /api — health check.
Common conventions
- List endpoints accept
limit(default 100, max 1000) andoffset. PATCHis a partial update: absent fields are unchanged; explicitnullclears a nullable field.DELETEremoves index records only, never files on disk.
Media files
GET /api/media-files?file_type=&sha256=&q=&limit=&offset=—qmatchesfile_path.POST /api/media-files—{file_type ("audio"|"cover_art"), file_path, original_filename?, mime_type?, file_size_bytes?, sha256_hash?, audio_format?, audio_bitrate?, audio_sample_rate?, audio_bit_depth?, uploader_name?, id?, created_at?}GET | PATCH | DELETE /api/media-files/{id}— delete is refused (409) while the file is referenced as a track'saudio_file_id; cover references are cleared automatically.
Artists
GET /api/artists?q=&hidden=&limit=&offset=POST /api/artists—{name, image_file_id?, is_hidden?, model_name?, id?, created_at?, updated_at?}GET /api/artists/{id}— artist + releases + track count.PATCH /api/artists/{id}DELETE /api/artists/{id}— releases/tracks stay, artist links and the artist's genre tags / external ids are removed.
Releases
GET /api/releases?q=&artist_id=&release_type=&year=&hidden=&limit=&offset=POST /api/releases—{title, release_type?, year?, cover_file_id?, total_tracks?, total_discs?, is_hidden?, model_name?, artist_ids?, id?, created_at?, updated_at?}GET /api/releases/{id}— release + ordered artists + tracks.PATCH /api/releases/{id}PUT /api/releases/{id}/artists—{artist_ids: [..]}replaces the artist list (positions by index).DELETE /api/releases/{id}— deletes the release's tracks too (index records only).
Tracks
GET /api/tracks?q=&release_id=&artist_id=&genre_id=&hidden=&limit=&offset=POST /api/tracks—{title, release_id, audio_file_id, track_number?, disc_number?, duration_seconds?, cover_file_id?, year?, is_hidden?, model_name?, artists? [{artist_id, role?, position?}], genre_ids?, lastfm_*?, id?, created_at?, updated_at?}GET /api/tracks/{id}— track + artists (with roles) + genres.PATCH /api/tracks/{id}— includinglastfm_listeners/playcount/rating/updated_at.PUT /api/tracks/{id}/artists—{artists: [{artist_id, role?, position?}]}replaces links.PUT /api/tracks/{id}/genres—{genre_ids: [..]}replaces links.DELETE /api/tracks/{id}
Genres
GET /api/genres?q=&limit=&offset=POST /api/genres—{name, id?}; upsert by normalized name (posting an existing genre returns it).GET | DELETE /api/genres/{id}
Genre tags (weighted, per source)
GET /api/genre-tags?entity_kind=&entity_id=&genre_id=&source=POST /api/genre-tags—{entity_kind, entity_id, genre_id, source, weight?}; upsert on(entity_kind, entity_id, genre_id, source).DELETE /api/genre-tags/{id}
External ids (MusicBrainz, Last.fm, Discogs, …)
GET /api/external-ids?entity_kind=&entity_id=&source=&id_kind=&external_id=POST /api/external-ids—{entity_kind, entity_id, source, id_kind, external_id, confidence?}; upsert on(entity_kind, entity_id, source, id_kind).DELETE /api/external-ids/{id}