From 2b254f417f8cebc644e8a8dfc395b6d46e834216 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Wed, 12 Aug 2026 01:41:00 +0100 Subject: [PATCH] Updated readme --- README.md | 313 +++++++++++++++++++++--------------------------------- 1 file changed, 124 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index 2e5695c..4346f8a 100644 --- a/README.md +++ b/README.md @@ -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 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_` (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` |