Updated readme
This commit is contained in:
@@ -1,208 +1,143 @@
|
||||
# furumusic
|
||||
# Furumusic
|
||||
|
||||
Furumusic can join the decentralized Furumi federation while remaining a
|
||||
complete local web player. Optional similarity search stores versioned audio
|
||||
embeddings in PostgreSQL and uses signed two-level LSH summaries in a separate
|
||||
DHT to discover compatible peers without a central recommendation index. The
|
||||
shared `music-dht` layer owns routing and wire compatibility; model inference
|
||||
and exact cosine ranking stay local to each instance.
|
||||
**Your library. Your users. Your network.**
|
||||
|
||||
Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL.
|
||||
Furumusic is a self-hosted, multi-user music server with a full web player and
|
||||
support for the Furumi federated network. It turns a collection of music files
|
||||
into a shared library that is available from any modern browser, while every
|
||||
user keeps their own playlists, likes, listening history, and playback state.
|
||||
|
||||
Built with Rust ([cot](https://cot.rs) framework).
|
||||
Music can be uploaded directly, imported from a `.torrent` file, or downloaded
|
||||
from a magnet link. An optional AI-assisted import pipeline reads the available
|
||||
tags and path information, reconstructs inconsistent metadata, finds artwork,
|
||||
and organizes the result into artists, releases, and tracks. Uncertain matches
|
||||
are kept for review instead of silently entering the library with bad data.
|
||||
|
||||
## Quick start
|
||||
## Why Furumusic?
|
||||
|
||||
Furumusic is for a household, a small community, or anyone who wants one music
|
||||
library without handing it to a subscription service. The server owns the
|
||||
catalog and media files; the browser is only the player.
|
||||
|
||||
- one shared library with separate user accounts;
|
||||
- a responsive web player with artists, releases, search, queue, and playlists;
|
||||
- direct file uploads and torrent or magnet imports;
|
||||
- optional AI-assisted recognition and normalization of metadata;
|
||||
- password login or OIDC/SSO with group-based access control;
|
||||
- optional federation without a central catalog or search service;
|
||||
- trusted-device pairing, playback handoff, and synchronization between Furumi
|
||||
players;
|
||||
- Last.fm scrobbling and similarity-based discovery when configured.
|
||||
|
||||
Furumusic does not include music. Import only media you are allowed to store and
|
||||
share.
|
||||
|
||||
## Importing music
|
||||
|
||||
Users can add audio files from the web interface or ask the server to download
|
||||
selected files from a torrent. Both routes feed the same import pipeline, so a
|
||||
library remains consistent regardless of where its files came from.
|
||||
|
||||
The importer combines embedded tags, filenames, folder structure, and existing
|
||||
catalog context. With an OpenAI-compatible language model configured, it can
|
||||
normalize artist and release names, separate featured artists, recover track
|
||||
numbers and release types, and flag ambiguous results for a user to approve.
|
||||
Cover art is taken from nearby image files or embedded artwork when available.
|
||||
|
||||
The inbox and permanent library are separate directories. New files are first
|
||||
processed in the inbox and are moved into the organized library only after
|
||||
their metadata has been accepted.
|
||||
|
||||
## Federation
|
||||
|
||||
Federation is optional. Independent Furumusic and Furumi players that use the
|
||||
same Network ID discover one another as a logical network. Each peer keeps its
|
||||
own library and can continue working alone.
|
||||
|
||||
When federation is enabled, local and remote artists, releases, and tracks can
|
||||
appear in the same search and library views. Missing audio is streamed from an
|
||||
available peer and can optionally be retained locally. Similarity search also
|
||||
stays decentralized: embeddings and exact ranking remain on the instance that
|
||||
owns the music.
|
||||
|
||||
Trusted-device pairing is separate from public catalog discovery. It connects
|
||||
a user's own Furumi players so likes, playlists, playback state, and control can
|
||||
move between approved devices.
|
||||
|
||||
## Build and run
|
||||
|
||||
Furumusic requires PostgreSQL and a Rust toolchain with Rust 2024 edition
|
||||
support. Create an empty database, provide its connection URL, and start the
|
||||
server:
|
||||
|
||||
```bash
|
||||
export FURU_DATABASE_URL=postgresql://user:pass@localhost/furumusic
|
||||
cargo run
|
||||
# Open http://localhost:8000/admin/setup to create the first admin account
|
||||
export FURU_DATABASE_URL='postgresql://furumusic:password@127.0.0.1/furumusic'
|
||||
cargo run --release --locked
|
||||
```
|
||||
|
||||
## Project structure
|
||||
Open <http://127.0.0.1:8000/admin/setup> to create the first administrator.
|
||||
After setup, configure the inbox and library directories under **Admin →
|
||||
Settings** before importing music.
|
||||
|
||||
To listen on another address or port:
|
||||
|
||||
```bash
|
||||
cargo run --release --locked -- -l 0.0.0.0:8000
|
||||
```
|
||||
Cargo.toml Project manifest and dependencies
|
||||
build.rs Captures rustc version + target at compile time
|
||||
src/
|
||||
main.rs Entrypoint; HTTP router, login/logout handlers, tracing init
|
||||
config.rs 3-tier config system (default → DB → env); FURU_* env vars
|
||||
auth.rs Session auth, Role enum (Admin/User), login/logout/guards
|
||||
user.rs User + OidcLink DB models, CRUD, password hashing, migrations
|
||||
oidc.rs OIDC/SSO flow: discovery, PKCE, token exchange, user provisioning
|
||||
i18n/
|
||||
mod.rs Language resolution (cookie → Accept-Language → default), extractor
|
||||
phrases.rs All UI strings in English and Russian (translations! macro)
|
||||
api/
|
||||
mod.rs JSON API endpoints (mounted at /api), session-based auth
|
||||
admin/
|
||||
mod.rs Admin sub-app router: dashboard, settings, users, debug, setup
|
||||
views.rs Admin page handlers and templates
|
||||
templates/
|
||||
base.html Root HTML layout with lang/title blocks
|
||||
login.html Login page (password + optional SSO button)
|
||||
admin/
|
||||
layout.html Admin sidebar/nav wrapper
|
||||
index.html Admin dashboard
|
||||
debug.html Build info + config table (with secret redaction)
|
||||
settings.html OIDC and auth settings form
|
||||
setup.html First-run admin account creation
|
||||
users.html User list
|
||||
user_form.html User create/edit form
|
||||
|
||||
A Nix development shell is included for Linux and macOS:
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
cargo run --locked
|
||||
```
|
||||
|
||||
The repository also contains a multi-stage `Dockerfile` for building a small
|
||||
runtime image. A deployment must provide PostgreSQL plus persistent, writable
|
||||
volumes for the inbox and music library.
|
||||
|
||||
## Configuration
|
||||
|
||||
Most settings can be changed from the administration interface. Every setting
|
||||
also has a `FURU_`-prefixed environment variable; environment values take
|
||||
priority over values stored in PostgreSQL.
|
||||
|
||||
The settings needed for a useful first installation are:
|
||||
|
||||
| Setting | Purpose |
|
||||
| --- | --- |
|
||||
| `FURU_DATABASE_URL` | PostgreSQL connection URL; required to run the service |
|
||||
| `FURU_AGENT_INBOX_DIR` | Temporary inbox for uploads and downloaded files |
|
||||
| `FURU_AGENT_STORAGE_DIR` | Permanent, organized music library |
|
||||
| `FURU_AGENT_ENABLED` | Enables the background metadata import pipeline |
|
||||
| `FURU_AGENT_LLM_URL` | Base URL of an OpenAI-compatible model server |
|
||||
| `FURU_AGENT_LLM_MODEL` | Model used to recognize and normalize metadata |
|
||||
| `FURU_FEDERATION_ENABLED` | Publishes the library and enables peer discovery |
|
||||
| `FURU_FEDERATION_NETWORK_ID` | Joins peers with the same value into one logical network |
|
||||
|
||||
AI recognition, federation, similarity search, Last.fm, and OIDC are optional.
|
||||
A local password-authenticated server can be used without any of them.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Config system (`src/config.rs`)
|
||||
Furumusic is written in Rust on the
|
||||
[Cot](https://cot.rs) web framework. PostgreSQL stores the catalog, accounts,
|
||||
playlists, configuration, and background-job state. Audio inspection uses
|
||||
Symphonia, torrent downloads use librqbit, and the shared `music-dht`/Frid
|
||||
protocol stack provides decentralized catalog search, media transfer, and
|
||||
connected-device synchronization.
|
||||
|
||||
Every setting lives in `AppConfig` and is resolved in three layers:
|
||||
The browser interface and JSON API are served by the same application. Import,
|
||||
artwork, metadata enrichment, similarity indexing, and maintenance run as
|
||||
durable background jobs rather than blocking playback requests.
|
||||
|
||||
1. **Compiled default** — `AppConfig::default()`
|
||||
2. **Database override** — rows in the `furumusic__config_entry` table
|
||||
3. **Environment variable** — `FURU_<FIELD_NAME>` (highest priority)
|
||||
## Contributing
|
||||
|
||||
`ConfigSources` tracks where each field's effective value came from (shown in the admin debug page).
|
||||
Bug reports, design discussions, and patches are welcome. Before submitting a
|
||||
change, run:
|
||||
|
||||
**To add a new config field:**
|
||||
|
||||
1. Add the field to `AppConfig` struct
|
||||
2. Set its default in `AppConfig::default()`
|
||||
3. Add the field to `ConfigSources` struct and its `Default` impl
|
||||
4. Add it to the `impl_env_overrides!(…)` invocation
|
||||
5. Add an `apply_db_field!()` call in `apply_db_overrides`
|
||||
6. Add an `entry!()` line in `admin/views.rs → config_display_entries()`
|
||||
|
||||
### Auth (`src/auth.rs`)
|
||||
|
||||
Session-based authentication with two roles:
|
||||
|
||||
- **`Role::Admin`** — full access to admin panel
|
||||
- **`Role::User`** — standard user
|
||||
|
||||
Key functions:
|
||||
- `login(session, user_id)` — sets session, cycles session ID
|
||||
- `logout(session)` — flushes session
|
||||
- `get_session_user(session, db)` — returns `AuthenticatedUser` if active
|
||||
- `require_admin_or_redirect(session, db)` — guard that returns 403 or redirects to `/login`
|
||||
|
||||
### OIDC/SSO (`src/oidc.rs`)
|
||||
|
||||
Full OpenID Connect authorization code flow with PKCE:
|
||||
|
||||
1. `GET /auth/oidc/start` — discovers provider, builds auth URL, stores CSRF/nonce/PKCE in session, redirects to IdP
|
||||
2. `GET /auth/oidc/callback` — validates CSRF, exchanges code for tokens, verifies ID token, provisions user
|
||||
|
||||
Provider metadata is cached for 1 hour and invalidated when OIDC config changes.
|
||||
|
||||
**Group access and role mapping:** The `oidc_user_groups` config field lists OIDC group names (comma-separated) allowed to access the service. When it is set, users outside both `oidc_user_groups` and `oidc_admin_groups` are denied before provisioning/login. The `oidc_admin_groups` config field lists OIDC group names that grant the admin role. Groups are extracted from the `groups` claim in the ID token JWT payload.
|
||||
|
||||
**User provisioning order:**
|
||||
1. Find existing `OidcLink` by issuer+sub → update claims, update role
|
||||
2. Find existing `User` by email → create OidcLink, update role
|
||||
3. Create new user (no password) + OidcLink
|
||||
|
||||
Stale links (pointing to deleted users) are cleaned up automatically.
|
||||
|
||||
### User model (`src/user.rs`)
|
||||
|
||||
Two database models:
|
||||
|
||||
- **`User`** — id, username (unique), password (optional for OIDC-only), email, display_name, avatar_url, role, is_active
|
||||
- **`OidcLink`** — id, user_id, issuer, sub, email, name, avatar_url; unique index on (issuer, sub)
|
||||
|
||||
Migrations: M0003 (User table), M0004 (OidcLink table), M0005 (OidcLink indexes).
|
||||
|
||||
### i18n (`src/i18n/`)
|
||||
|
||||
Compile-time bilingual UI (English + Russian).
|
||||
|
||||
- `translations!` macro in `phrases.rs` generates a `Translations` struct with static `EN` and `RU` instances
|
||||
- Language resolution: `furu_lang` cookie → `Accept-Language` header → English default
|
||||
- `I18n` is a cot request extractor — handlers receive it automatically
|
||||
- `set_lang` endpoint (`/set-lang?lang=ru&next=/`) sets the cookie
|
||||
|
||||
### API (`src/api/`)
|
||||
|
||||
JSON API mounted at `/api`. Uses the same session cookie as HTML pages — works automatically for same-origin frontend requests (no CORS, no tokens needed).
|
||||
|
||||
Helpers in `api/mod.rs`:
|
||||
- `json_ok(value)` — 200 with `application/json`
|
||||
- `json_error(status, message)` — error response as `{"error": "..."}`
|
||||
|
||||
| Route | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| `/api/me` | GET | Current user (id, name, role) or 401 |
|
||||
|
||||
**Swagger UI** is available at `/swagger/` when `FURU_SWAGGER_ENABLED=true`. The OpenAPI spec is auto-generated from handler types.
|
||||
|
||||
To add a new API endpoint:
|
||||
1. Define request/response structs with `#[derive(Serialize, JsonSchema)]`
|
||||
2. Write an async handler, return `Json(response).into_response()`
|
||||
3. Add a `Route::with_api_handler_and_name(…, api_get(handler), …)` in `ApiApp::router()`
|
||||
4. The endpoint appears automatically in Swagger UI
|
||||
|
||||
### Admin panel (`src/admin/`)
|
||||
|
||||
Mounted at `/admin`. All routes (except `/admin/setup`) require `Role::Admin`.
|
||||
|
||||
| Route | Purpose |
|
||||
|-------|---------|
|
||||
| `/admin/setup` | First-run: create initial admin (only works when zero users exist) |
|
||||
| `/admin/` | Dashboard |
|
||||
| `/admin/debug` | Build info, config values with sources, DB connectivity |
|
||||
| `/admin/settings` | OIDC config, auth toggles (saved to DB config table) |
|
||||
| `/admin/users` | User list |
|
||||
| `/admin/users/new` | Create user |
|
||||
| `/admin/users/{id}/edit` | Edit user |
|
||||
| `/admin/users/{id}/delete` | Delete user (POST) |
|
||||
|
||||
## How to extend
|
||||
|
||||
### 1. Add a config field
|
||||
|
||||
See [Config system](#config-system-srcconfigrs) above — 6 locations to update.
|
||||
|
||||
### 2. Add a database model
|
||||
|
||||
1. Define a struct with `#[cot::db::model]` in a new or existing file
|
||||
2. Write a migration struct implementing `cot::db::migrations::Migration`
|
||||
3. Register the migration in the `AdminApp::migrations()` method in `src/admin/mod.rs`
|
||||
|
||||
### 3. Add a page
|
||||
|
||||
1. Create a template in `templates/`
|
||||
2. Write a handler function that returns `Html`
|
||||
3. Add a `Route::with_handler_and_name(…)` in the appropriate `router()` method
|
||||
4. If admin-only, wrap with `require_admin_or_redirect`
|
||||
|
||||
### 4. Add a translation
|
||||
|
||||
Add a line to the `translations!` macro in `src/i18n/phrases.rs`:
|
||||
|
||||
```rust
|
||||
my_key: "English text", "Русский текст";
|
||||
```bash
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
cargo test --all-targets
|
||||
```
|
||||
|
||||
Access it in handlers/templates as `i18n.t.my_key` (or `t.my_key` in templates).
|
||||
|
||||
### 5. Add an API endpoint
|
||||
|
||||
Same as adding a page, but return a JSON response instead of `Html`. The `json` feature is enabled in Cargo.toml.
|
||||
|
||||
## Environment variables
|
||||
|
||||
All prefixed with `FURU_`. Priority: env var > DB override > compiled default.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `FURU_DATABASE_URL` | PostgreSQL connection URL | *(empty — required)* |
|
||||
| `FURU_LOG_LEVEL` | Tracing filter (e.g. `info`, `debug`, `warn,furumusic=trace`) | `info` |
|
||||
| `FURU_AUTH_PASSWORD_ENABLED` | Enable password login | `true` |
|
||||
| `FURU_AUTH_SSO_ENABLED` | Enable SSO/OIDC login | `false` |
|
||||
| `FURU_OIDC_ISSUER` | OIDC issuer URL | *(empty)* |
|
||||
| `FURU_OIDC_CLIENT_ID` | OIDC client ID | *(empty)* |
|
||||
| `FURU_OIDC_CLIENT_SECRET` | OIDC client secret | *(empty)* |
|
||||
| `FURU_OIDC_BUTTON_TEXT` | SSO button label | `Sign in with SSO` |
|
||||
| `FURU_OIDC_ADMIN_GROUPS` | Comma-separated OIDC groups that grant admin | *(empty)* |
|
||||
| `FURU_OIDC_USER_GROUPS` | Comma-separated OIDC groups allowed to access the service. Empty means any authenticated SSO user is allowed. | *(empty)* |
|
||||
| `FURU_SWAGGER_ENABLED` | Serve Swagger UI at `/swagger/` | `false` |
|
||||
|
||||
Reference in New Issue
Block a user