163 lines
8.2 KiB
Markdown
163 lines
8.2 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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`](../../frid/crates/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}`
|