2026-07-16 18:45:39 +03:00
2026-07-16 18:45:39 +03:00
2026-07-16 18:45:39 +03:00
2026-07-16 18:45:39 +03:00
2026-07-16 18:45:39 +03:00
2026-07-16 18:45:39 +03:00
2026-07-16 18:45:39 +03:00

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.

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 (kind optional: 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 TEXT in %Y-%m-%dT%H:%M:%SZ format;
  • name_sort / title_sort = trimmed lowercase of the display value;
  • media_files.file_path is relative to the media root;
  • entity_kind is one of artist | release | track;
  • track artist role is one of main | featuring | remixer | producer;
  • release_type is one of album | single | ep | compilation | mixtape | live | soundtrack | remix | demo.

Migration notes

  • Every create endpoint accepts an optional explicit id, plus optional created_at / updated_at overrides — 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) and offset.
  • PATCH is a partial update: absent fields are unchanged; explicit null clears a nullable field.
  • DELETE removes index records only, never files on disk.

Media files

  • GET /api/media-files?file_type=&sha256=&q=&limit=&offset=q matches file_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's audio_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} — including lastfm_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}
S
Description
No description provided
Readme
156 KiB
Languages
Rust 78.7%
HTML 21.3%