88 lines
3.2 KiB
Markdown
88 lines
3.2 KiB
Markdown
# music-dht
|
|
|
|
A distributed **music library directory** built on
|
|
[`federation-net`](../federation-net): peers publish their local library
|
|
index — artists, releases and tracks (names and small metadata, **never
|
|
files**) — into a Kademlia-style DHT and search each other's libraries.
|
|
Every running node is simultaneously a client, a DHT router and a storage
|
|
node; there are no dedicated servers of any kind.
|
|
|
|
This crate is the grown-up sibling of the minimal
|
|
[`artist-dht`](../artist-dht) example: same DHT machinery, richer records and
|
|
an application-oriented API.
|
|
|
|
## Records
|
|
|
|
A [`LibraryItem`] carries: `kind` (artist | release | track), `name`,
|
|
`artist_names` (main artists for releases/tracks), `featured_artist_names`
|
|
(for track guest appearances), `year`, `release_type`, `release_title`,
|
|
`track_number`, `disc_number`, `duration_seconds`, optional track `content_id`
|
|
(`b3:<64 hex>`), plus ownership and versioning metadata. Records are
|
|
published under one exact key (the
|
|
normalized name) and one token key per word of the name, every main/featured
|
|
artist name and the track release title, so searching for an artist also
|
|
returns their releases, tracks and guest appearances. Track records with a
|
|
`content_id` are also published under a content key for exact-audio fallback.
|
|
|
|
## API
|
|
|
|
The application does not add or delete records one by one — it declares the
|
|
desired state and the service diffs:
|
|
|
|
```rust
|
|
let (service, events) = MusicDhtService::start(config).await?;
|
|
// Publish (and later re-publish) the whole library; matched by local_key.
|
|
service.sync_library(vec![
|
|
ItemSpec {
|
|
local_key: "artist:1".into(),
|
|
kind: ItemKind::Artist,
|
|
name: "Massive Attack".into(),
|
|
artist_names: vec![],
|
|
featured_artist_names: vec![],
|
|
year: None,
|
|
release_type: None,
|
|
release_title: None,
|
|
track_number: None,
|
|
disc_number: None,
|
|
duration_seconds: None,
|
|
content_id: None,
|
|
},
|
|
// ...
|
|
]).await?;
|
|
let outcome = service.search_network("teardrop").await?;
|
|
```
|
|
|
|
`sync_library` is idempotent: item ids are derived from
|
|
`(owner, kind, local_key)`, so unchanged items are skipped, changed ones are
|
|
republished with a bumped revision and items that disappeared from the input
|
|
are tombstoned network-wide.
|
|
|
|
## Peer discovery
|
|
|
|
With `.rendezvous(RendezvousConfig::default())` in the config, peers of a
|
|
network find each other knowing **only the network id** (a shared rendezvous
|
|
record in the public BitTorrent Mainline DHT — see the `federation-net`
|
|
README). Tickets (`service.ticket()` / `service.connect(ticket)`) remain as a
|
|
manual fallback for isolated networks.
|
|
|
|
The network id is a public rendezvous token: anyone who knows it can join
|
|
and see the published names. Use a unique, hard-to-guess name for a private
|
|
network.
|
|
|
|
## Consumers
|
|
|
|
[`furumi-fd`](../../../furumi-stack/furumi-fd) uses this crate for its
|
|
federation feature: every instance publishes its library index and can search
|
|
the libraries of all other instances on the same network.
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
cargo fmt --check
|
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
|
cargo test -p music-dht
|
|
```
|
|
|
|
The integration test starts three real nodes in one process (they use Iroh's
|
|
public relay infrastructure), so it needs network access.
|