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
|
||||
loop. The UI state remains synchronous and deterministic; filesystem, SQLite,
|
||||
audio, networking, artwork, and media-control work is performed by runtime
|
||||
services and reported back as application events.
|
||||
Furumi is an autonomous music player that can cooperate with other Furumi
|
||||
players. The architecture starts from one constraint: **a node must remain a
|
||||
complete and useful player when every other node is unavailable**.
|
||||
|
||||
## 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
|
||||
terminal/media/player/network events
|
||||
|
|
||||
v
|
||||
app::event::AppEvent
|
||||
|
|
||||
v
|
||||
input/keymap -> Action
|
||||
|
|
||||
v
|
||||
app::update()
|
||||
mutates AppState and
|
||||
requests an Effect
|
||||
|
|
||||
v
|
||||
app runtime performs I/O
|
||||
|
|
||||
+----> new AppEvent
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Furumi node │
|
||||
│ │
|
||||
│ Local library ── Playback engine ── TUI / media keys │
|
||||
│ │ │ │
|
||||
│ ├── Trusted-device replication │
|
||||
│ │ │
|
||||
│ └── Federation: discovery, catalog, audio │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
`AppState` is the single UI source of truth. Rendering modules receive shared
|
||||
state and do not own background tasks. Blocking database and file operations
|
||||
run outside the terminal event loop.
|
||||
The local SQLite library is authoritative for that node. Network data is
|
||||
merged into local views or materialized as pending remote content; it does not
|
||||
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
|
||||
src/
|
||||
main.rs process, terminal, Tokio, and OS-media setup
|
||||
app/
|
||||
state.rs UI and navigation state
|
||||
action.rs semantic user actions
|
||||
event.rs runtime-to-UI events
|
||||
update.rs pure state transitions and requested effects
|
||||
update_tests.rs update/selection/queue behavior tests
|
||||
mod.rs runtime orchestration and effect execution
|
||||
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
|
||||
discovery plane
|
||||
Local catalog ───────> DHT <─────── Other catalogs
|
||||
│
|
||||
│ peer + content identity
|
||||
v
|
||||
direct P2P connection
|
||||
├── catalog protocol
|
||||
├── audio protocol
|
||||
└── device-sync protocol
|
||||
```
|
||||
|
||||
Large orchestration modules are intentionally separated from their tests.
|
||||
When they are split further, boundaries should follow services rather than
|
||||
line count: playback coordination, network-library maintenance, device
|
||||
storage, and device transport are the natural seams.
|
||||
The DHT is the distributed index. Nodes publish compact searchable
|
||||
descriptions of their local library and query the network without contacting a
|
||||
central search service.
|
||||
|
||||
## 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
|
||||
layer that issues library SQL and returns typed models to the rest of the
|
||||
application. The schema covers artists, releases, tracks, artist relations,
|
||||
playlists, likes, playback history, federated pending tracks, and cached
|
||||
network artists.
|
||||
Relay-assisted connectivity may help peers establish a route, but relays do
|
||||
not become catalog authorities or application-state owners.
|
||||
|
||||
Imports read tags with `lofty`, inspect audio properties, calculate content
|
||||
identifiers, and upsert normalized library records. File paths remain
|
||||
device-local.
|
||||
## Identity and content resolution
|
||||
|
||||
## 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
|
||||
the logical queue and playback state, while the controller receives play,
|
||||
pause, seek, volume, and prefetch commands. The next source is opened early
|
||||
for gapless transitions. The analyzer publishes levels and scope samples for
|
||||
Rhai visualization scripts.
|
||||
1. Is this track already present in my local library?
|
||||
2. Does one of my trusted devices have it?
|
||||
3. Which federation peers currently advertise it?
|
||||
|
||||
OS media commands enter through `media.rs`; current metadata and position are
|
||||
published back to the platform now-playing surface.
|
||||
Resolution follows that order conceptually: prefer a ready local source, reuse
|
||||
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;
|
||||
- search merges DHT records, peer catalogs, and cached metadata;
|
||||
- catalog requests provide richer artist/release views;
|
||||
- audio requests stream content from peers;
|
||||
- downloads may remain cached or be imported into the local library.
|
||||
Trusted devices do not share a live database connection. Each device records
|
||||
operations locally and exchanges them when connectivity returns.
|
||||
|
||||
Federation is disabled until configured by the user. Paths are never
|
||||
published as portable identifiers; content hashes and peer item IDs are used
|
||||
instead.
|
||||
The synchronization model combines:
|
||||
|
||||
## 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
|
||||
ALPN. Likes, playlists, membership changes, and playback control are
|
||||
represented as an append-only operation log with materialized SQLite tables.
|
||||
Hybrid logical timestamps and acknowledgements make offline merging and
|
||||
tombstone compaction deterministic.
|
||||
This is eventual consistency scoped to a trusted device group. A temporarily
|
||||
offline laptop can modify playlists, another device can continue playback, and
|
||||
both can later converge without a permanently available coordinator.
|
||||
|
||||
Pairing uses short-lived invites. Device-local file paths are deliberately
|
||||
excluded from synchronized playback tracks; receiving devices resolve them
|
||||
through content IDs, their own library, or federation metadata.
|
||||
Membership changes use the same replicated model. Revocation is state that
|
||||
must propagate and converge, not an ephemeral server-side session flag.
|
||||
|
||||
## Configuration and persistence
|
||||
## Playback across devices
|
||||
|
||||
The `directories` crate selects platform-standard config, data, and cache
|
||||
locations. Settings, keymaps, device identity, and federation configuration
|
||||
are separate files. SQLite databases and downloaded covers/audio are stored
|
||||
under application data/cache directories rather than the repository.
|
||||
Playback has one logical state but remains physically local to the device
|
||||
producing audio.
|
||||
|
||||
## 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
|
||||
on normal exit and panic.
|
||||
- stderr from native audio libraries is captured into tracing so it cannot
|
||||
corrupt the alternate screen.
|
||||
- Blocking work is kept out of the UI loop.
|
||||
- Runtime failures are converted to visible status/events where recovery is
|
||||
possible.
|
||||
- Device paths are not treated as portable network identities.
|
||||
- Formatting, all-target compilation, Clippy, and unit tests should pass
|
||||
before a release tag is pushed.
|
||||
Active-device leases and idle timing prevent stale snapshots from immediately
|
||||
taking control after a device reconnects. This provides practical coordination
|
||||
without introducing a central playback arbiter.
|
||||
|
||||
## Local application architecture
|
||||
|
||||
Inside one node, Furumi uses an event-driven state machine:
|
||||
|
||||
```text
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user