The beginning of something great
This commit is contained in:
451
README.md
Normal file
451
README.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# mpris-scrobbler
|
||||
|
||||
A local music scrobbler for MPRIS-compatible players, built in Rust. It tracks what you listen to via `playerctl`, stores scrobble data in a local SQLite database, and generates listening reports.
|
||||
|
||||
## Features
|
||||
|
||||
- **Accurate play time tracking** — monitors both metadata changes and play/pause status, so paused time doesn't count toward scrobble thresholds
|
||||
- **Last.fm-style scrobble rules** — a track is scrobbled after 50% of its duration or 4 minutes of play, whichever is shorter
|
||||
- **Local storage** — all data stays on your machine in a single SQLite file
|
||||
- **Reports** — terminal tables or JSON output, filterable by time period (today, week, month, year, all time)
|
||||
- **Adaptive terminal tables** — report columns shrink to fit narrower terminal widths
|
||||
- **HTML reports** — generate a standalone dark-themed HTML file with album art cards
|
||||
- **Terminal-style typography** — HTML uses JetBrains Mono with monospace fallbacks
|
||||
- **Fair ranking tie-breaks** — top artists/albums/tracks are ranked by plays first, then total listen time
|
||||
- **Album visuals + bars** — HTML shows both large album cover grids and top-album bar tables
|
||||
- **Genre stats** — reports include top genres (plays + listen time) when metadata is available
|
||||
- **Mood labels by period** — each report section highlights up to 6 dominant genres
|
||||
- **Mobile jump menu** — sticky section links (Today/Week/Month/All Time) reduce scrolling on phones
|
||||
- **Enrichment** — fetch album covers + genres from MusicBrainz/Cover Art Archive
|
||||
- **Incremental publish helper** — query latest scrobble and publish only when new data exists
|
||||
- **Configurable player** — defaults to `com.blitzfc.qbz`, configurable via `--player`
|
||||
|
||||
## Requirements
|
||||
|
||||
- [playerctl](https://github.com/altdesktop/playerctl)
|
||||
- Rust toolchain (for building)
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick install (interactive)
|
||||
|
||||
The repo includes an interactive install script that builds the binary, installs it,
|
||||
sets up the systemd service, and starts it — asking before each step:
|
||||
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
|
||||
### Manual install
|
||||
|
||||
Build and install the binary into `~/.local/bin`:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
install -Dm755 target/release/mpris-scrobbler ~/.local/bin/mpris-scrobbler
|
||||
```
|
||||
|
||||
Make sure `~/.local/bin` is in your `PATH`:
|
||||
|
||||
```bash
|
||||
command -v mpris-scrobbler
|
||||
```
|
||||
|
||||
If that prints nothing, add this to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
Then open a new shell and verify:
|
||||
|
||||
```bash
|
||||
mpris-scrobbler --help
|
||||
```
|
||||
|
||||
## Autostart (recommended)
|
||||
|
||||
Use a **systemd user service** so scrobbling starts automatically when you log in.
|
||||
|
||||
### 1) Install the service unit
|
||||
|
||||
Copy the provided unit file from the repo:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/systemd/user
|
||||
cp contrib/systemd/user/mpris-scrobbler.service ~/.config/systemd/user/
|
||||
```
|
||||
|
||||
If needed, edit the player in the service (`--player com.blitzfc.qbz`).
|
||||
|
||||
### 2) Enable and start
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now mpris-scrobbler.service
|
||||
```
|
||||
|
||||
### 3) Verify
|
||||
|
||||
```bash
|
||||
systemctl --user status mpris-scrobbler.service
|
||||
journalctl --user -u mpris-scrobbler.service -f
|
||||
```
|
||||
|
||||
### Optional: keep running without active login
|
||||
|
||||
If you want user services to keep running after logout/reboot (without an active shell login), enable lingering:
|
||||
|
||||
```bash
|
||||
loginctl enable-linger "$USER"
|
||||
```
|
||||
|
||||
### Important session note
|
||||
|
||||
`playerctl` and MPRIS are tied to the session D-Bus. The scrobbler must run in the **same user/session** as your player. If the player runs under another account/session, this service will not see it.
|
||||
|
||||
## Report workflow
|
||||
|
||||
Recommended workflow:
|
||||
|
||||
- Keep only `watch` running as a service.
|
||||
- Generate HTML reports manually whenever you want:
|
||||
|
||||
```bash
|
||||
mpris-scrobbler report --html --output ~/music-report
|
||||
```
|
||||
|
||||
This creates a self-contained directory:
|
||||
|
||||
```
|
||||
~/music-report/
|
||||
├── index.html # Open this in a browser
|
||||
└── covers/
|
||||
├── <mbid1>.jpg
|
||||
├── <mbid2>.jpg
|
||||
└── ...
|
||||
```
|
||||
|
||||
When `--html` is used, the tool automatically runs enrichment first (for missing albums) so covers/genres are available in the generated report.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary will be at `target/release/mpris-scrobbler`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Scrobbling
|
||||
|
||||
Start the watcher to begin recording what you listen to:
|
||||
|
||||
```bash
|
||||
# Default player (com.blitzfc.qbz)
|
||||
mpris-scrobbler watch
|
||||
|
||||
# Specify a different player
|
||||
mpris-scrobbler watch --player spotify
|
||||
```
|
||||
|
||||
The watcher runs in the foreground and logs scrobbles to stderr:
|
||||
|
||||
```
|
||||
Database: /home/user/.local/share/mpris-scrobbler/scrobbles.db
|
||||
Watching player: com.blitzfc.qbz
|
||||
[scrobbled] ††† (Crosses) - This Is a Trick (186s)
|
||||
[scrobbled] ††† (Crosses) - Telepathy (200s)
|
||||
```
|
||||
|
||||
Press `Ctrl+C` to stop. The last track will be evaluated before shutdown.
|
||||
|
||||
### Reports
|
||||
|
||||
All-time sections default to 2.5x the `--limit` value (rounded to nearest 5)
|
||||
for a broader view; shorter periods use the limit as-is. Default limit is 10,
|
||||
giving 25 for all-time. Use `--all-time-limit` to override explicitly.
|
||||
|
||||
Top lists use this ordering logic:
|
||||
|
||||
- Primary: number of plays (descending)
|
||||
- Secondary: total listen time (descending)
|
||||
- Final stable tie-break: name fields (artist/album/title)
|
||||
|
||||
In HTML reports, album covers are shown in fixed full rows (6 columns on desktop, 3 on tablet, 2 on mobile). For cleaner layout, the cover grid rounds the visual cover count up to the next full desktop row when enough albums exist.
|
||||
|
||||
The HTML head loads JetBrains Mono from Google Fonts and falls back to local monospace fonts when offline.
|
||||
|
||||
```bash
|
||||
# All-time summary with top artists, albums, genres, tracks
|
||||
mpris-scrobbler report
|
||||
|
||||
# Filter by period
|
||||
mpris-scrobbler report --period today
|
||||
mpris-scrobbler report --period week
|
||||
mpris-scrobbler report --period month
|
||||
mpris-scrobbler report --period year
|
||||
|
||||
# JSON output
|
||||
mpris-scrobbler report --json
|
||||
|
||||
# HTML output to stdout (no covers)
|
||||
mpris-scrobbler report --html
|
||||
|
||||
# HTML output to directory (with covers)
|
||||
mpris-scrobbler report --html --output ~/music-report
|
||||
|
||||
# Change the number of entries in top-N lists (default: 20)
|
||||
mpris-scrobbler report --limit 20
|
||||
```
|
||||
|
||||
Example terminal output:
|
||||
|
||||
```
|
||||
=== Scrobble Report: This Week (2026-03-13 → 2026-03-19) ===
|
||||
|
||||
+-------------------+----------+
|
||||
| Metric | Value |
|
||||
+-------------------+----------+
|
||||
| Total scrobbles | 142 |
|
||||
| Total listen time | 8h 23m |
|
||||
| Unique artists | 31 |
|
||||
| Unique albums | 47 |
|
||||
| Unique tracks | 98 |
|
||||
+-------------------+----------+
|
||||
|
||||
Top Artists
|
||||
+---+----------------+-------+-------------+
|
||||
| # | Artist | Plays | Listen Time |
|
||||
+---+----------------+-------+-------------+
|
||||
| 1 | ††† (Crosses) | 23 | 1h 12m |
|
||||
| 2 | Deftones | 18 | 1h 05m |
|
||||
+---+----------------+-------+-------------+
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
mpris-scrobbler watch [OPTIONS]
|
||||
--player <NAME> Player name for playerctl [default: com.blitzfc.qbz]
|
||||
--db-path <PATH> Path to the SQLite database
|
||||
|
||||
mpris-scrobbler report [OPTIONS]
|
||||
--period <PERIOD> today, week, month, year, all [default: all]
|
||||
--json Output as JSON
|
||||
--html Output as standalone HTML
|
||||
--output <PATH> Write HTML report to this directory (index.html + covers/)
|
||||
--limit <LIMIT> Number of entries in top-N lists [default: 10]
|
||||
--all-time-limit <N> Override all-time top-N limit [default: 2.5x --limit]
|
||||
--db-path <PATH> Path to the SQLite database
|
||||
|
||||
mpris-scrobbler enrich [OPTIONS]
|
||||
--force Re-fetch metadata for all albums
|
||||
--db-path <PATH> Path to the SQLite database
|
||||
|
||||
mpris-scrobbler last-scrobble [OPTIONS]
|
||||
--db-path <PATH> Path to the SQLite database
|
||||
```
|
||||
|
||||
### Enrichment (covers + genres)
|
||||
|
||||
Enrichment is automatic for `report --html`, but you can still run it manually if you want to prefetch metadata:
|
||||
|
||||
```bash
|
||||
mpris-scrobbler enrich
|
||||
```
|
||||
|
||||
When MusicBrainz matching is tricky, enrichment now retries with normalised
|
||||
album variants (e.g. strips parenthetical suffixes like `(Killing Eve)`, then
|
||||
progressively shortens trailing words), tries artist aliases for symbol-heavy
|
||||
names, and falls back to recording search before giving up.
|
||||
|
||||
Genre extraction order:
|
||||
|
||||
1. Release `genres`
|
||||
2. Release `tags`
|
||||
3. Release-group `genres`
|
||||
4. Release-group `tags`
|
||||
|
||||
Automatic enrichment (triggered by `report --html`) uses a retry cooldown for
|
||||
incomplete cache entries (missing cover or missing genre): those entries are
|
||||
re-tried after 7 days, not on every report run.
|
||||
|
||||
Use force mode when you want immediate backfill/refresh for everything:
|
||||
|
||||
```bash
|
||||
mpris-scrobbler enrich --force
|
||||
```
|
||||
|
||||
Genre normalisation notes:
|
||||
|
||||
- Genre labels are currently passed through from MusicBrainz with light cleanup only.
|
||||
- We split comma-separated values and trim spaces.
|
||||
- For aggregation, hyphen/space variants are grouped (e.g. `post-rock` + `post rock`).
|
||||
- When both forms exist, the spaced form is preferred for display.
|
||||
- Album cards display at most 3 genre pills for readability.
|
||||
- Top Genre and Mood sections aggregate using that normalised grouping.
|
||||
|
||||
Downloaded covers are stored in:
|
||||
|
||||
`~/.local/share/mpris-scrobbler/covers/`
|
||||
|
||||
#### Manually pinning an album (`pin-album`)
|
||||
|
||||
Sometimes automatic search fails — most commonly for classical recordings where
|
||||
the scrobbled artist tag (e.g. a choir or soloist) doesn't match the release
|
||||
credits on MusicBrainz, or where the album title wording differs significantly.
|
||||
Enrich will print:
|
||||
|
||||
```
|
||||
No match found on MusicBrainz.
|
||||
```
|
||||
|
||||
When that happens you can pin the correct MusicBrainz release manually:
|
||||
|
||||
1. Search for the release on [musicbrainz.org](https://musicbrainz.org).
|
||||
2. Open the release page. The MBID is the UUID in the URL:
|
||||
`https://musicbrainz.org/release/`**`f2ff907a-0355-451b-9c68-f0b7c09bb145`**
|
||||
3. Copy the exact artist and album strings from the `enrich` output (the
|
||||
`[N/M] Artist - Album` line) and run:
|
||||
|
||||
```bash
|
||||
mpris-scrobbler pin-album \
|
||||
--artist "Coro della Radiotelevisione Svizzera" \
|
||||
--album "Vivaldi: Gloria; Nisi Dominus; Nulla in mundo pax" \
|
||||
--mbid "f2ff907a-0355-451b-9c68-f0b7c09bb145"
|
||||
```
|
||||
|
||||
This fetches genres and cover art for that specific release and stores them in
|
||||
the local cache, overwriting any previous (failed) entry. Re-run `enrich` or
|
||||
`report --html` afterwards to pick up the result.
|
||||
|
||||
If the Cover Art Archive has no image for the release (the command prints
|
||||
"No cover art available"), supply one with `--cover-url`:
|
||||
|
||||
```bash
|
||||
mpris-scrobbler pin-album \
|
||||
--artist "Coro della Radiotelevisione Svizzera" \
|
||||
--album "Vivaldi: Gloria; Nisi Dominus; Nulla in mundo pax" \
|
||||
--mbid "f2ff907a-0355-451b-9c68-f0b7c09bb145" \
|
||||
--cover-url "https://example.com/cover.jpg"
|
||||
```
|
||||
|
||||
The image is downloaded, resized to 500 px, and stored locally just like a
|
||||
Cover Art Archive image. Any HTTPS image URL works — Discogs, Wikipedia,
|
||||
Bandcamp, etc.
|
||||
|
||||
### Incremental publish script
|
||||
|
||||
If you publish the report to a remote host, use the included helper script:
|
||||
|
||||
```bash
|
||||
./mpris-scrobbler-publish.sh
|
||||
```
|
||||
|
||||
It runs `report --html` and `rsync` only when a newer scrobble exists.
|
||||
The script tracks the last published scrobble timestamp in:
|
||||
|
||||
`$XDG_STATE_HOME/mpris-scrobbler/last-published-scrobble.txt`
|
||||
|
||||
Set defaults in a config file so you can run the script without passing flags:
|
||||
|
||||
`~/.config/mpris-scrobbler/publish.conf`
|
||||
|
||||
If you use `./install.sh`, an example config is installed there automatically
|
||||
when the file does not already exist.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
OUTPUT_DIR="$HOME/music-report"
|
||||
REMOTE_TARGET="user@host:/var/www/music-report"
|
||||
DB_PATH=""
|
||||
```
|
||||
|
||||
Legacy fallback is also supported:
|
||||
|
||||
`~/.mpris-scrobbler-publish.conf`
|
||||
|
||||
Flags:
|
||||
|
||||
```bash
|
||||
./mpris-scrobbler-publish.sh --output ~/music-report --remote user@host:/var/www/music-report
|
||||
./mpris-scrobbler-publish.sh --db-path /custom/path/scrobbles.db
|
||||
|
||||
# Keep running and check every 5 minutes (default interval):
|
||||
./mpris-scrobbler-publish.sh --watch
|
||||
|
||||
# Custom interval (seconds):
|
||||
./mpris-scrobbler-publish.sh --watch --interval 600
|
||||
|
||||
# Force regeneration even when no new scrobbles exist:
|
||||
./mpris-scrobbler-publish.sh --force
|
||||
```
|
||||
|
||||
The installer also places this helper in `~/.local/bin` as:
|
||||
|
||||
`mpris-scrobbler-publish`
|
||||
|
||||
## Data storage
|
||||
|
||||
Scrobbles are stored in SQLite at `~/.local/share/mpris-scrobbler/scrobbles.db` (respects `$XDG_DATA_HOME`).
|
||||
|
||||
Each scrobble records:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| artist | Artist name |
|
||||
| album | Album name |
|
||||
| title | Track title |
|
||||
| track_duration_secs | Full track duration in seconds |
|
||||
| played_duration_secs | Actual time spent listening |
|
||||
| scrobbled_at | ISO 8601 timestamp |
|
||||
|
||||
## How it works
|
||||
|
||||
The watcher spawns two `playerctl --follow` processes:
|
||||
|
||||
1. **Metadata follower** — emits a line each time the track changes
|
||||
2. **Status follower** — emits `Playing`, `Paused`, or `Stopped` on state changes
|
||||
|
||||
A state machine accumulates play time only while the player is in `Playing` state. When a new track starts (or the player stops), the previous track is evaluated against the scrobble threshold and recorded if it qualifies.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service is running but nothing is scrobbled
|
||||
|
||||
Check the player name:
|
||||
|
||||
```bash
|
||||
playerctl -l
|
||||
```
|
||||
|
||||
If needed, edit `~/.config/systemd/user/mpris-scrobbler.service` and change `--player ...`, then reload/restart:
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart mpris-scrobbler.service
|
||||
```
|
||||
|
||||
### Player is in another account/session
|
||||
|
||||
MPRIS is session-scoped. The service must run in the same account/session as the player process.
|
||||
|
||||
### Check logs
|
||||
|
||||
```bash
|
||||
journalctl --user -u mpris-scrobbler.service -n 200
|
||||
journalctl --user -u mpris-scrobbler.service -f
|
||||
```
|
||||
|
||||
### Verify database is being written
|
||||
|
||||
```bash
|
||||
mpris-scrobbler report --period today
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
||||
MIT
|
||||
Reference in New Issue
Block a user