Updated readme
This commit is contained in:
+250
-120
@@ -1,148 +1,278 @@
|
|||||||
# furumi architecture
|
# Furumi architecture
|
||||||
|
|
||||||
`furumi` is a single Rust binary organized around an Elm-style state/update
|
Furumi is an autonomous music player that can cooperate with other Furumi
|
||||||
loop. The UI state remains synchronous and deterministic; filesystem, SQLite,
|
players. The architecture starts from one constraint: **a node must remain a
|
||||||
audio, networking, artwork, and media-control work is performed by runtime
|
complete and useful player when every other node is unavailable**.
|
||||||
services and reported back as application events.
|
|
||||||
|
|
||||||
## Runtime flow
|
Networking therefore extends a local player instead of becoming a prerequisite
|
||||||
|
for it. There is no control plane, account service, canonical catalog, or
|
||||||
|
server-owned source of truth.
|
||||||
|
|
||||||
|
## Architectural goals
|
||||||
|
|
||||||
|
The design optimizes for five properties:
|
||||||
|
|
||||||
|
1. **Local autonomy** — importing, browsing, playback, playlists, likes, and
|
||||||
|
history work entirely on one device.
|
||||||
|
2. **No central failure domain** — discovery, catalog exchange, streaming, and
|
||||||
|
synchronization do not depend on a Furumi-operated service.
|
||||||
|
3. **Offline tolerance** — trusted devices may change state independently and
|
||||||
|
reconcile after reconnecting.
|
||||||
|
4. **Incremental federation** — one node is complete; every additional node
|
||||||
|
increases availability and the amount of discoverable music.
|
||||||
|
5. **Explicit trust boundaries** — personal-device replication and wider
|
||||||
|
music federation are different protocols with different authority.
|
||||||
|
|
||||||
|
These goals are more important than maintaining a globally identical view of
|
||||||
|
the network. Furumi prefers useful local progress and eventual reconciliation
|
||||||
|
over distributed consensus.
|
||||||
|
|
||||||
|
## The node model
|
||||||
|
|
||||||
|
Every running Furumi instance contains the same four capabilities:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
terminal/media/player/network events
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
|
│ Furumi node │
|
||||||
v
|
│ │
|
||||||
app::event::AppEvent
|
│ Local library ── Playback engine ── TUI / media keys │
|
||||||
|
|
│ │ │ │
|
||||||
v
|
│ ├── Trusted-device replication │
|
||||||
input/keymap -> Action
|
│ │ │
|
||||||
|
|
│ └── Federation: discovery, catalog, audio │
|
||||||
v
|
└─────────────────────────────────────────────────────────┘
|
||||||
app::update()
|
|
||||||
mutates AppState and
|
|
||||||
requests an Effect
|
|
||||||
|
|
|
||||||
v
|
|
||||||
app runtime performs I/O
|
|
||||||
|
|
|
||||||
+----> new AppEvent
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`AppState` is the single UI source of truth. Rendering modules receive shared
|
The local SQLite library is authoritative for that node. Network data is
|
||||||
state and do not own background tasks. Blocking database and file operations
|
merged into local views or materialized as pending remote content; it does not
|
||||||
run outside the terminal event loop.
|
replace the local database with a remote database abstraction.
|
||||||
|
|
||||||
## Module layout
|
This keeps the core behavior predictable:
|
||||||
|
|
||||||
|
- disconnecting never makes the local collection unavailable;
|
||||||
|
- downloaded content can become ordinary local content;
|
||||||
|
- a peer disappearing reduces availability but does not invalidate local
|
||||||
|
state;
|
||||||
|
- nodes may join and leave without electing a leader.
|
||||||
|
|
||||||
|
## Two network layers
|
||||||
|
|
||||||
|
Furumi deliberately separates **trusted-device synchronization** from
|
||||||
|
**federated music exchange**.
|
||||||
|
|
||||||
|
### Trusted-device synchronization
|
||||||
|
|
||||||
|
This layer connects devices owned or trusted by the same user. It carries
|
||||||
|
personal state such as:
|
||||||
|
|
||||||
|
- likes and playlist operations;
|
||||||
|
- device membership and revocation;
|
||||||
|
- acknowledgements and synchronization progress;
|
||||||
|
- playback state, commands, and handoff information.
|
||||||
|
|
||||||
|
Pairing establishes the trust relationship. After that, changes are replicated
|
||||||
|
through an append-only operation log and applied to materialized local tables.
|
||||||
|
|
||||||
|
### Music federation
|
||||||
|
|
||||||
|
Federation connects independent libraries. It provides:
|
||||||
|
|
||||||
|
- decentralized discovery through the DHT;
|
||||||
|
- automatic publication of searchable local catalog metadata;
|
||||||
|
- richer artist and release catalogs fetched from peers;
|
||||||
|
- direct audio transfer when a selected track is not available locally.
|
||||||
|
|
||||||
|
Federation does not grant another peer authority over personal playlists,
|
||||||
|
likes, or device membership. A node may participate in federation without
|
||||||
|
joining another user's trusted-device group.
|
||||||
|
|
||||||
|
Keeping these layers separate prevents discovery convenience from silently
|
||||||
|
becoming a synchronization trust decision.
|
||||||
|
|
||||||
|
## Discovery and direct communication
|
||||||
|
|
||||||
|
Furumi separates finding content from transferring it.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/
|
discovery plane
|
||||||
main.rs process, terminal, Tokio, and OS-media setup
|
Local catalog ───────> DHT <─────── Other catalogs
|
||||||
app/
|
│
|
||||||
state.rs UI and navigation state
|
│ peer + content identity
|
||||||
action.rs semantic user actions
|
v
|
||||||
event.rs runtime-to-UI events
|
direct P2P connection
|
||||||
update.rs pure state transitions and requested effects
|
├── catalog protocol
|
||||||
update_tests.rs update/selection/queue behavior tests
|
├── audio protocol
|
||||||
mod.rs runtime orchestration and effect execution
|
└── device-sync protocol
|
||||||
popup.rs popup submission behavior
|
|
||||||
input.rs editable text input
|
|
||||||
command.rs command model
|
|
||||||
cmdline.rs command-line execution
|
|
||||||
library/
|
|
||||||
mod.rs SQLite-backed library operations
|
|
||||||
import.rs tags, audio metadata, and directory import
|
|
||||||
models.rs library-facing data types
|
|
||||||
tests.rs library integration tests
|
|
||||||
player/
|
|
||||||
mod.rs rodio playback controller
|
|
||||||
analyzer.rs visualization audio analysis
|
|
||||||
federation/
|
|
||||||
mod.rs DHT manager, search, downloads, and caching
|
|
||||||
catalog.rs peer catalog protocol and merge logic
|
|
||||||
audio.rs peer audio transport
|
|
||||||
tests.rs federation ranking/appearance tests
|
|
||||||
devices/
|
|
||||||
tests.rs trusted-device sync tests
|
|
||||||
devices.rs trusted-device operation log and wire protocol
|
|
||||||
ui/ ratatui rendering by screen
|
|
||||||
config/ settings, logging, and keymaps
|
|
||||||
media.rs platform media-key/now-playing integration
|
|
||||||
visualizer.rs Rhai visualization host
|
|
||||||
visualizations/ bundled Rhai scripts
|
|
||||||
art.rs image decode and terminal-cell preparation
|
|
||||||
share.rs share-link parsing and generation
|
|
||||||
streaming.rs growing-file reader used during downloads
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Large orchestration modules are intentionally separated from their tests.
|
The DHT is the distributed index. Nodes publish compact searchable
|
||||||
When they are split further, boundaries should follow services rather than
|
descriptions of their local library and query the network without contacting a
|
||||||
line count: playback coordination, network-library maintenance, device
|
central search service.
|
||||||
storage, and device transport are the natural seams.
|
|
||||||
|
|
||||||
## Local library
|
Once a peer is known, communication moves to direct P2P streams provided by
|
||||||
|
iroh through `music-dht`. Furumi defines separate application protocols for
|
||||||
|
catalog requests, audio transfer, and trusted-device synchronization. This
|
||||||
|
keeps discovery traffic small and lets large or private exchanges happen only
|
||||||
|
between the participating peers.
|
||||||
|
|
||||||
`library::Library` owns a mutex-protected SQLite connection. It is the only
|
Relay-assisted connectivity may help peers establish a route, but relays do
|
||||||
layer that issues library SQL and returns typed models to the rest of the
|
not become catalog authorities or application-state owners.
|
||||||
application. The schema covers artists, releases, tracks, artist relations,
|
|
||||||
playlists, likes, playback history, federated pending tracks, and cached
|
|
||||||
network artists.
|
|
||||||
|
|
||||||
Imports read tags with `lofty`, inspect audio properties, calculate content
|
## Identity and content resolution
|
||||||
identifiers, and upsert normalized library records. File paths remain
|
|
||||||
device-local.
|
|
||||||
|
|
||||||
## Playback
|
Network operations refer to content independently of any one library row.
|
||||||
|
Content identifiers allow a node to ask:
|
||||||
|
|
||||||
`player::Controller` owns the rodio audio thread. The application maintains
|
1. Is this track already present in my local library?
|
||||||
the logical queue and playback state, while the controller receives play,
|
2. Does one of my trusted devices have it?
|
||||||
pause, seek, volume, and prefetch commands. The next source is opened early
|
3. Which federation peers currently advertise it?
|
||||||
for gapless transitions. The analyzer publishes levels and scope samples for
|
|
||||||
Rhai visualization scripts.
|
|
||||||
|
|
||||||
OS media commands enter through `media.rs`; current metadata and position are
|
Resolution follows that order conceptually: prefer a ready local source, reuse
|
||||||
published back to the platform now-playing surface.
|
known content where possible, and fetch from a peer only when necessary.
|
||||||
|
|
||||||
## Federation
|
A federated track can initially exist as a lightweight pending item in a queue
|
||||||
|
or playlist. When playback reaches it, Furumi resolves an available peer,
|
||||||
|
starts the transfer, and either uses the cache or imports the result into the
|
||||||
|
local library. The rest of the player continues to work with the same
|
||||||
|
`TrackItem` model, so local and remote availability do not require separate
|
||||||
|
playback systems.
|
||||||
|
|
||||||
Federation uses `music-dht` for discovery and byte streams:
|
## Offline-first synchronization
|
||||||
|
|
||||||
- the local library publishes metadata-only item specifications;
|
Trusted devices do not share a live database connection. Each device records
|
||||||
- search merges DHT records, peer catalogs, and cached metadata;
|
operations locally and exchanges them when connectivity returns.
|
||||||
- catalog requests provide richer artist/release views;
|
|
||||||
- audio requests stream content from peers;
|
|
||||||
- downloads may remain cached or be imported into the local library.
|
|
||||||
|
|
||||||
Federation is disabled until configured by the user. Paths are never
|
The synchronization model combines:
|
||||||
published as portable identifiers; content hashes and peer item IDs are used
|
|
||||||
instead.
|
|
||||||
|
|
||||||
## Trusted-device sync
|
- immutable operation identifiers for deduplication;
|
||||||
|
- hybrid logical timestamps for deterministic last-writer decisions;
|
||||||
|
- materialized tables for fast UI queries;
|
||||||
|
- tombstones so deletion survives offline replicas;
|
||||||
|
- per-peer acknowledgements to determine when old tombstones can be compacted;
|
||||||
|
- snapshots to repair a peer that missed older operations.
|
||||||
|
|
||||||
`devices.rs` implements a separate trusted-device protocol over a dedicated
|
This is eventual consistency scoped to a trusted device group. A temporarily
|
||||||
ALPN. Likes, playlists, membership changes, and playback control are
|
offline laptop can modify playlists, another device can continue playback, and
|
||||||
represented as an append-only operation log with materialized SQLite tables.
|
both can later converge without a permanently available coordinator.
|
||||||
Hybrid logical timestamps and acknowledgements make offline merging and
|
|
||||||
tombstone compaction deterministic.
|
|
||||||
|
|
||||||
Pairing uses short-lived invites. Device-local file paths are deliberately
|
Membership changes use the same replicated model. Revocation is state that
|
||||||
excluded from synchronized playback tracks; receiving devices resolve them
|
must propagate and converge, not an ephemeral server-side session flag.
|
||||||
through content IDs, their own library, or federation metadata.
|
|
||||||
|
|
||||||
## Configuration and persistence
|
## Playback across devices
|
||||||
|
|
||||||
The `directories` crate selects platform-standard config, data, and cache
|
Playback has one logical state but remains physically local to the device
|
||||||
locations. Settings, keymaps, device identity, and federation configuration
|
producing audio.
|
||||||
are separate files. SQLite databases and downloaded covers/audio are stored
|
|
||||||
under application data/cache directories rather than the repository.
|
|
||||||
|
|
||||||
## Reliability rules
|
The synchronized state describes the queue, current item, position, pause
|
||||||
|
state, volume, shuffle/repeat mode, and the active playback owner. Commands are
|
||||||
|
targeted and deduplicated. Handoff transfers intent and position; the receiving
|
||||||
|
device resolves the track against its own library or federation sources before
|
||||||
|
starting its audio engine.
|
||||||
|
|
||||||
- Terminal raw mode, bracketed paste, and keyboard enhancements are restored
|
Active-device leases and idle timing prevent stale snapshots from immediately
|
||||||
on normal exit and panic.
|
taking control after a device reconnects. This provides practical coordination
|
||||||
- stderr from native audio libraries is captured into tracing so it cannot
|
without introducing a central playback arbiter.
|
||||||
corrupt the alternate screen.
|
|
||||||
- Blocking work is kept out of the UI loop.
|
## Local application architecture
|
||||||
- Runtime failures are converted to visible status/events where recovery is
|
|
||||||
possible.
|
Inside one node, Furumi uses an event-driven state machine:
|
||||||
- Device paths are not treated as portable network identities.
|
|
||||||
- Formatting, all-target compilation, Clippy, and unit tests should pass
|
```text
|
||||||
before a release tag is pushed.
|
terminal / player / database / network / OS media events
|
||||||
|
│
|
||||||
|
v
|
||||||
|
AppEvent
|
||||||
|
│
|
||||||
|
input → Action
|
||||||
|
│
|
||||||
|
v
|
||||||
|
update(AppState)
|
||||||
|
│
|
||||||
|
optional Effect
|
||||||
|
│
|
||||||
|
v
|
||||||
|
asynchronous runtime work
|
||||||
|
│
|
||||||
|
└──────────> AppEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
`AppState` is the single source of truth for the interface. The update layer
|
||||||
|
performs deterministic state transitions and requests effects; it does not
|
||||||
|
perform blocking I/O. SQLite access, imports, artwork decoding, DHT queries,
|
||||||
|
peer transfers, and audio preparation run through runtime services and report
|
||||||
|
their results as events.
|
||||||
|
|
||||||
|
This structure gives the TUI three important properties:
|
||||||
|
|
||||||
|
- rendering is a pure projection of current state;
|
||||||
|
- input behavior can be tested without starting audio or networking;
|
||||||
|
- slow peers and large imports cannot block terminal interaction.
|
||||||
|
|
||||||
|
The audio engine is similarly isolated. The application owns the logical
|
||||||
|
queue, while `player::Controller` owns rodio playback and receives explicit
|
||||||
|
commands. Prefetching prepares the next source before the current item ends.
|
||||||
|
|
||||||
|
## Persistence boundaries
|
||||||
|
|
||||||
|
Furumi stores different kinds of state according to their lifetime:
|
||||||
|
|
||||||
|
| State | Storage | Role |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Library, playlists, likes, history | SQLite | Durable local source of truth |
|
||||||
|
| Device operation log and replicas | SQLite | Offline synchronization |
|
||||||
|
| Federation catalog cache | SQLite/cache | Faster network browsing |
|
||||||
|
| Audio and artwork cache | Filesystem cache | Reusable fetched data |
|
||||||
|
| Settings, keymap, identity | Platform config/data dirs | Node configuration |
|
||||||
|
| Queue and playback snapshots | Application/device sync state | Continuity and handoff |
|
||||||
|
|
||||||
|
Caches are replaceable. The local library and device operation log are durable.
|
||||||
|
This distinction lets maintenance and recovery code discard derived network
|
||||||
|
data without risking the user's collection.
|
||||||
|
|
||||||
|
## Failure model
|
||||||
|
|
||||||
|
Expected failures are treated as ordinary state:
|
||||||
|
|
||||||
|
- a DHT query may return partial results;
|
||||||
|
- an advertised peer may be offline by the time a track is requested;
|
||||||
|
- a transfer may stop and be retried through another source;
|
||||||
|
- trusted devices may reconnect with overlapping changes;
|
||||||
|
- cached metadata may be stale;
|
||||||
|
- an audio device may disappear during playback.
|
||||||
|
|
||||||
|
The architecture avoids converting these cases into global failure. Search can
|
||||||
|
show partial data, synchronization can resume, and playback resolution can try
|
||||||
|
another source. Errors return to the application as events so the UI can expose
|
||||||
|
them without terminating the node.
|
||||||
|
|
||||||
|
## Main implementation boundaries
|
||||||
|
|
||||||
|
The source tree follows the architectural responsibilities:
|
||||||
|
|
||||||
|
- `library/` owns the local catalog and import pipeline;
|
||||||
|
- `player/` owns audio playback and analysis;
|
||||||
|
- `federation/` owns DHT-facing search, peer catalogs, and audio exchange;
|
||||||
|
- `devices.rs` owns trusted-device replication and playback coordination;
|
||||||
|
- `app/` owns state transitions and runtime orchestration;
|
||||||
|
- `ui/` renders the TUI;
|
||||||
|
- `media.rs` integrates platform media controls;
|
||||||
|
- `visualizer.rs` hosts programmable Rhai visualizations.
|
||||||
|
|
||||||
|
Dependencies should continue to point inward toward typed models and explicit
|
||||||
|
events. UI code should not own network tasks, network protocols should not
|
||||||
|
mutate UI state directly, and the local library should not depend on the
|
||||||
|
presence of federation.
|
||||||
|
|
||||||
|
## Architectural invariants
|
||||||
|
|
||||||
|
Future changes should preserve these rules:
|
||||||
|
|
||||||
|
1. A node must start and play its local library without network access.
|
||||||
|
2. No central Furumi service may become required for discovery, playback, or
|
||||||
|
trusted-device synchronization.
|
||||||
|
3. Federation membership must not imply personal-device trust.
|
||||||
|
4. Remote state must be merged or cached locally, never treated as an always
|
||||||
|
available database.
|
||||||
|
5. Network and storage work must remain outside the interactive UI path.
|
||||||
|
6. More peers should improve availability; losing peers should only reduce
|
||||||
|
remote capabilities.
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ library management are included.
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
Download a prebuilt archive from the project releases, or build Furumi from
|
Download a prebuilt archive from the project releases, or build Furumi from
|
||||||
source with Rust 1.88 or newer:
|
source with Rust 1.97 or newer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build --release --locked
|
cargo build --release --locked
|
||||||
|
|||||||
Reference in New Issue
Block a user