Files

163 lines
8.2 KiB
Markdown
Raw Permalink Normal View History

2026-07-16 17:22:32 +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
```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 |
2026-07-16 18:45:39 +03:00
| `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**. |
2026-07-16 17:22:32 +03:00
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}`