All checks were successful
Deploy production / deploy (push) Successful in 2m37s
- music schema: artists/albums/tracks (+album art), plays with source + dedupe, now_playing, spotify_auth, import_state; plays_deduped view hides Last.fm scrobbles Spotify also has - in-app Spotify poller (60s, leased) started from hooks init; no new role or env needed - /api/spotify/connect + callback (owner only), /trackers/music public stats - scripts: music-poll, music-import-lastfm (resumable), music-import-history - deploy/music-collector: optional host timer with its own role
67 lines
4.1 KiB
Markdown
67 lines
4.1 KiB
Markdown
# music (self-hosted Last.fm)
|
|
|
|
`/trackers/music` (stats) and the "listening now" card on the homepage read the `music`
|
|
schema in the `personal` database:
|
|
|
|
| table / view | contents |
|
|
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
| `artists`, `albums` | Spotify ids when known (imports are matched by name), album art URLs (640 px + small) |
|
|
| `tracks`, `track_artists` | `match_key` = normalised `artist\|title` (`src/lib/music/normalize.ts`) for cross-source dedupe |
|
|
| `plays` | one row per play: `source` spotify / lastfm / youtube / local, `origin` api / export / import |
|
|
| `plays_deduped` (view) | `plays` minus Last.fm scrobbles that Spotify also has (what every stat reads) |
|
|
| `now_playing` | row 1, refreshed every minute |
|
|
| `spotify_auth` | row 1, refresh token + poller state. **Not** granted to `personal_ro` |
|
|
| `import_state` | resumable backfill progress (`lastfm:<user>`) |
|
|
|
|
## Spotify (live)
|
|
|
|
1. In the Spotify developer dashboard (the existing app, `SPOTIFY_CLIENT_ID`) add the redirect
|
|
URIs `https://stanrunge.dev/api/spotify/callback` and
|
|
`https://dev.stanrunge.dev/api/spotify/callback`.
|
|
2. Logged in as the owner, open <https://stanrunge.dev/api/spotify/connect> once. Scopes:
|
|
`user-read-currently-playing user-read-recently-played user-read-playback-state user-top-read`.
|
|
3. That's it: the **app itself** polls every minute (`src/lib/server/music/poller.ts`, started
|
|
from `hooks.server.ts` `init`) with its own role and Spotify credentials. Status (last poll,
|
|
last error) is shown to the owner on `/trackers/music`.
|
|
|
|
### Optional: poll from the host instead
|
|
|
|
Only if the poller should keep running independent of the app container. Needs its own role:
|
|
|
|
```sh
|
|
sudo docker exec -i postgres-db-1 psql -U postgres -d personal <<'SQL'
|
|
CREATE ROLE music_collector LOGIN CONNECTION LIMIT 3 PASSWORD '<pick one>';
|
|
GRANT CONNECT ON DATABASE personal TO music_collector;
|
|
GRANT USAGE ON SCHEMA music TO music_collector;
|
|
GRANT SELECT, INSERT, UPDATE ON music.artists, music.albums, music.tracks, music.track_artists,
|
|
music.plays, music.now_playing, music.import_state TO music_collector;
|
|
GRANT SELECT, UPDATE ON music.spotify_auth TO music_collector;
|
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA music TO music_collector;
|
|
SQL
|
|
```
|
|
|
|
`~/.config/music-collector.env` (mode 600): `MUSIC_DB_URL=postgresql://music_collector:…@127.0.0.1:5440/personal`,
|
|
`SPOTIFY_CLIENT_ID=…`, `SPOTIFY_CLIENT_SECRET=…`. Then `bash deploy/music-collector/install.sh --enable`
|
|
and set `MUSIC_POLLER=off` in prod.env. (The music migration grants the same when the role
|
|
already exists.) Debug: `journalctl --user -u music-collector -n 20`.
|
|
|
|
## Backfills
|
|
|
|
Both run from the deployed image with the app role, so no extra credentials:
|
|
|
|
```sh
|
|
tag=$(sudo sed -n 's/^IMAGE_TAG=//p' /opt/personal-website/.env)
|
|
# Last.fm (needs an API key from https://www.last.fm/api/account/create); resumable, re-run any time
|
|
sudo docker run --rm --network backend --env-file /etc/personal-website/prod.env \
|
|
-e LASTFM_API_KEY=<key> git.stanrunge.dev/stan/website:$tag bun scripts/music-import-lastfm.ts --user stanrunge
|
|
# Spotify Extended Streaming History (unzipped export in ~/spotify-export)
|
|
sudo docker run --rm --network backend --env-file /etc/personal-website/prod.env \
|
|
-v ~/spotify-export:/data:ro git.stanrunge.dev/stan/website:$tag bun scripts/music-import-history.ts /data
|
|
```
|
|
|
|
Dedupe: within one source a play within 60 s of the same `match_key` is skipped on insert;
|
|
across sources `plays_deduped` hides a Last.fm scrobble when a Spotify play of the same
|
|
`match_key` ended between 3 min before and track length + 3 min after the scrobble time
|
|
(Last.fm records when a track started, Spotify when it ended). Listening time uses
|
|
`ms_played` when known, else the track length.
|