Skewffle (skew + shuffle) is a weighted auto-queue service for MPD (Music Player Daemon). Like ashuffle, it watches MPD and keeps the play queue topped up with random tracks — but instead of a uniform shuffle, every track, album, and artist can carry a rating that skews its selection probability. Favorites appear more often, deprioritized music appears rarely but is never fully gone, and smart modes (whole-album adds, rediscovery of long-unplayed tracks) keep the queue both familiar and fresh.
- Keeps MPD's queue fed using ashuffle's proven event-driven core: blocks on MPD
idle, reacts to queue/player/database events, maintains a configurable queue buffer, and reconnects forever after MPD outages. - Ratings with fallback — integer 0–10 on tracks, albums, and artists; the effective rating resolves track → album → artist → default (5). Selection weight is linear in the effective rating: a 10-rated track plays twice as often as a neutral one.
- Rating 0 and hard bans — rating 0 removes a track from the shuffle but keeps it inside whole-album adds; a banned track never plays, not even as part of an album.
- Exclude rules with ashuffle semantics (case-insensitive substring, AND within a rule,
OR across rules), plus one-time import of an existing ashuffle
excludes.yaml. - Album mode — occasionally (or exclusively) add a whole album, selected by album rating, with configurable probability, time-of-day/day-of-week windows, and an anti-repeat cooldown.
- Rediscovery mode — a periodic pull toward tracks that have not played in months (or ever).
- Play history — counts a play when ≥ 50 % (configurable) of the duration was actually listened, derived from MPD player events. Powers rediscovery and the stats page.
- Last.fm loved tracks — periodic sync assigns loved tracks a configurable high rating (manual ratings always win; un-loved tracks revert).
- Tag-based identity — ratings, bans, and history are keyed by normalized tags, not file paths, so everything survives renames, moves, and format changes. Fully exportable/importable.
- Persistent shuffle state — the anti-repeat window and pool ordering survive restarts.
- Web UI — status dashboard with live updates, library browser with inline rating/ban editing, rules management, history and stats, settings applied without restart.
Skewffle needs Node.js ≥ 24 and a reachable MPD server.
npx skewffleThen open http://127.0.0.1:4816. MPD location comes from the standard MPD_HOST/MPD_PORT
environment variables or flags:
npx skewffle --mpd-host 192.168.1.10 --mpd-port 6600
npx skewffle --mpd-host /run/mpd/socket
npx skewffle --mpd-host password@mpd.localdocker run -d \
-e MPD_HOST=mpd.local \
-v skewffle-data:/data \
-p 4816:4816 \
ghcr.io/avol-v/skewffleThe image is published to both GitHub Container Registry (ghcr.io/avol-v/skewffle) and Docker Hub (docker.io/avolv/skewffle).
See docker-compose.example.yml for pairing with an MPD container.
These are needed before the database opens; changing them requires a restart.
| Flag | Environment variable | Default |
|---|---|---|
--data-dir |
SKEWFFLE_DATA_DIR |
$XDG_DATA_HOME/skewffle → ~/.local/share/skewffle |
--http-host |
SKEWFFLE_HTTP_HOST |
127.0.0.1 (Docker image: 0.0.0.0) |
--http-port |
SKEWFFLE_HTTP_PORT |
4816 |
--mpd-host |
MPD_HOST |
localhost (supports password@host and socket paths) |
--mpd-port |
MPD_PORT |
6600 |
Precedence: flag > environment variable > default.
Everything else lives in SQLite and is edited on the Settings page — no restart needed: queue buffer, shuffle window size, play-on-startup, suspend timeout, default rating, album mode (probability, minimum tracks, cooldown, time windows), rediscovery (probability, staleness threshold), play-count threshold, Last.fm credentials and sync interval. Settings and library data (ratings/bans/history) can be exported and imported as JSON/YAML (data also as CSV) from the same page.
The defaults mirror a typical ashuffle setup:
queue-buffer 1, window-size 25, play-on-startup off.
For every queue slot Skewffle decides which picker runs:
- Album mode (if enabled and inside an allowed time window) — with the configured probability, one album is drawn weighted by its effective rating and added whole, in album order. Banned and rule-excluded tracks are punched out; rating-0 tracks stay.
- Rediscovery (with its probability) — one long-unplayed track, weighted by rating.
- Weighted shuffle — ashuffle's window+pool algorithm with rating-weighted sampling: after a track plays it cannot repeat for at least window-size further picks, and within the pool, higher-rated tracks are drawn more often.
Tracks queued by any picker share one cooldown, so an album add will not be followed by the same songs from the shuffle. Full details in docs/selection-engine.md.
| Mechanism | In weighted shuffle? | In album adds? | In rediscovery? |
|---|---|---|---|
| Normal track (rating ≥ 1 or unrated) | yes, weighted | yes | yes, if stale |
| Effective rating 0 | no | yes | no |
| Banned track | no | no | no |
| Matches an exclude rule | no | no | no |
| Not in the MPD library anymore | no | no | no |
- The core behavior (queue buffer, window cooldown, suspend timeout, single-mode handling)
is a faithful port; the defaults match
ashuffle --queue-buffer 1 --tweak window-size=25 --tweak play-on-startup=no. - Import your
excludes.yamlon the Rules page — the semantics are identical.
The web UI is backed by a JSON API under /api (status, SSE events, queue, player controls,
library, rules, settings, history, stats, import/export, Last.fm sync). There is no
authentication — Skewffle is a trusted local/LAN service and binds to 127.0.0.1 by
default. Do not expose it to untrusted networks.
npm install
node --run dev # server from TypeScript sources (native type stripping)
node --run dev:web # Vite dev server for the UI (proxies /api)
node --run typecheck
node --run test # Node.js native test runner
node --run build # dist/server (tsc) + dist/web (vite)Architecture notes live in docs/. The stack is deliberately lean: Hono +
@hono/node-server + yaml at runtime, node:sqlite for storage, a hand-rolled MPD
protocol client, and SolidJS + Vite for the UI (dev-only dependencies).