Rust-powered musical graph — SQLite-backed, MCP-compatible, with a built-in local dashboard.
Static playlists don't capture the fluid nature of musical tastes. spotify-graph models artists as a weighted graph where discovery happens through proximity — not platform algorithms.
spotify-graph (single binary)
├── mcp → MCP stdio server (current mode)
├── tracker → Poll Last.fm (primary) + Spotify (fallback) → SQLite
└── serve → axum HTTP API + Askama/HTMX dashboard
Everything in Rust. No Python, no Node, no separate services.
Spotify (listen) ──auto-scrobble──→ Last.fm (hub)
│
┌─────────────────┤
▼ ▼
plays history similar artists + tags
│ │
└────────┬────────┘
▼
SQLite (local)
│
┌────────────┼────────────┐
▼ ▼ ▼
MCP tools REST API Dashboard
(stdio) (axum) (Askama+HTMX)
- Rust — axum (HTTP), rmcp (MCP), rusqlite (SQLite), reqwest (API calls)
- SQLite — single file, no ORM
- Askama — compile-time HTML templates
- HTMX — server-driven interactivity (no JS framework)
- Cytoscape.js — graph visualization (bonus, post-v1)
- Last.fm API — primary source for plays, tags, similar artists
- Spotify API — fallback for plays + audio features
Each node has 6 continuous axes (1–10) describing its sonic profile:
| Axis | 1 | 10 |
|---|---|---|
| Energy | Silence | Full power |
| Tempo | No beat | 140+ BPM |
| Repetition | Varied | Pure hypnotic |
| Organic | 100% synth | 100% acoustic |
| Depth | Surface | Immersive / meditative |
| Brightness | Dark | Bright |
Plus CSV tags, free-form notes, and an optional Spotify URI.
Types: similar_vibe, same_scene, same_label, influenced_by, collaborated_with, lastfm_similar
Each edge carries a weight (0–1) representing link strength.
Tracks played, sourced from Last.fm (primary) and Spotify (fallback). Artist, track name, timestamp, duration, URI.
liked / disliked / neutral per context (coding, chill, morning…).
# MCP server (stdio) — for Hermes/any MCP client
spotify-graph mcp
# Track plays — poll Last.fm + Spotify, enrich graph
spotify-graph tracker
# Serve dashboard + API
spotify-graph serve --port 3030Default (no subcommand) starts MCP mode for backward compatibility.
GET /api/stats— graph statisticsGET /api/plays?limit=50&offset=0&artist=— play historyGET /api/artists— artist list with tagsGET /api/artists/:name— artist detail + relationsGET /api/discover/:name?depth=2— BFS traversal
POST /api/nodes— add a nodePOST /api/edges— add a relationPATCH /api/artists/:name— edit tags/axes/noteDELETE /api/nodes/:name— remove a node (cascade)
| Tool | Description |
|---|---|
spotify_graph_add_node |
Add a node with audio attributes |
spotify_graph_link |
Link two nodes with a typed relation |
spotify_graph_discover |
BFS traversal filtered by minimum weight |
spotify_graph_log_play |
Log a single play event |
spotify_graph_log_plays_batch |
Log multiple plays at once |
spotify_graph_seed |
Bulk JSON import of nodes + edges |
spotify_graph_stats |
Graph statistics |
# ~/.config/spotify-graph/config.toml
[server]
port = 3030
host = "0.0.0.0"
[database]
path = "~/.local/share/spotify-graph/graph.sqlite"
[lastfm]
api_key = "..."
username = "..."
rate_limit_ms = 250
[spotify]
client_id = "..."
auth_path = "~/.hermes/auth.json"
[tracker]
poll_interval_min = 120
max_similar_match = 0.4Defaults are used if the file is missing. Override via SPOTIFY_GRAPH_* env vars.
cargo build --release
# binary: target/release/spotify-graph| Route | Description |
|---|---|
/ |
Quick stats |
/plays |
Play history, filters, pagination |
/artists |
Artist list + tags, inline edit |
/artists/:name |
Detail: tags, axes, relations |
/discover |
BFS discovery — interactive |
/graph |
Force-directed visualization (Cytoscape.js, post-v1) |
spotify-graph/
├── src/
│ ├── main.rs # CLI (clap subcommands)
│ ├── config.rs # TOML config loading
│ ├── db/
│ │ ├── mod.rs
│ │ ├── schema.sql
│ │ └── queries.rs # SQLite queries
│ ├── mcp/
│ │ ├── mod.rs
│ │ └── tools.rs # MCP tool handlers
│ ├── tracker/
│ │ ├── mod.rs
│ │ ├── lastfm.rs # Last.fm API client
│ │ └── spotify.rs # Spotify API client
│ └── api/
│ ├── mod.rs
│ ├── routes.rs # axum routes
│ ├── handlers.rs # route handlers
│ ├── error.rs # error types
│ └── templates/ # Askama HTML templates
│ ├── base.html
│ ├── index.html
│ ├── plays.html
│ ├── artists.html
│ ├── detail.html
│ └── discover.html
├── static/ # Cytoscape.js, HTMX, Tailwind
├── Cargo.toml
└── schema.sql
- Schema + SQLite DB
- 5 MCP tools (add_node, link, discover, seed, stats)
- Initial dataset (Ludo's artists)
- Python tracker (Spotify polling)
- #21 — CLI subcommands (mcp / tracker / serve)
- #22 — Last.fm tracker (primary) + Spotify fallback (Rust)
- #23 — REST API (axum)
- #24 — Dashboard (Askama + HTMX)
- #25 — Graph visualization (Cytoscape.js)
- #26 — Remove Python scripts
- #27 — Config file (TOML)
- #17 — MCP tool: search
- #18 — MCP tool: Graphviz export
- Audio features from Spotify API (energy, tempo, etc.)
- Feedback system (liked/disliked per context)