feat: write playlists into the mirror from the listening history #9

Merged
lyrathorpe merged 1 commits from feat/playlists into main 2026-08-24 17:53:03 +01:00
Owner

Playlists

Six rules over the scrobble history and the library index, written as extended
M3U into <mirror>/_playlists/ and rebuilt every pass.

Playlist Rule
heavy-rotation Most played over the last twelve months
all-time Most played ever
neglected Played heavily once, silent for twelve months
deep-cuts Never played, from albums whose other tracks you play constantly
unheard-favourites Never played, by the artists you play most
unheard Never played, anywhere in the library

Three look at what gets played and three at what does not, because four fifths
of the library has never been played and a best-of alone would ignore 41,000
tracks.

Two decisions worth flagging

Twelve months, not ninety days. Ninety days is the obvious window and is the
wrong one: on the real history it holds a few hundred plays spread across a
twenty-thousand track rotation, so nothing ranks. The report's own
top track 90d list -- one track at 35 plays, then a cliff to 3 -- is what that
looks like.

Rotation is weekly, not per pass. A pass runs every few hours. A playlist
that reorders itself each time is one that has to be re-imported each time,
because the Music app imports a snapshot of a file rather than tracking it.

Paths, and the part I could not test

Lidarr knows where the FLAC is; the playlist has to point at the MP3
music-mirror made from it. --library-root is stripped from Lidarr's paths and
they are re-rooted under --mirror with the suffix changed.

The root is derived from the common parent of the indexed artist folders, so it
agrees with Lidarr by construction rather than by being kept in step by hand.
Override it if the guess is wrong.

Entries are written relative to the playlist file, so one playlist works from
the NAS, from a Mac over SMB, and from Linux.

A track is listed only once its mirror file is confirmed to exist. Lidarr
holding the FLAC says nothing about whether the MP3 has been encoded. When many
are missing the run says so loudly — that is exactly what a wrong root looks
like, every path mapping to nothing, and silence would present an empty playlist
as a correct one. This is the piece I cannot verify from here; the first real
run is the test.

_playlists/ survives music-mirror's prune, which only deletes *.mp3 and
skips directories that are not empty. Checked in its source, not assumed.

Testing

80 tests, green locally and in docker build --target test. The playlist ones
build a real library on disk with mirror MP3s beside the source paths, and
cover the mapping, relative paths resolving to real files, the limit, weekly
rotation stability, group-readability, and a missing mirror file being left out.

Next

Genre and mood playlists — 80s synths, high energy rock, screamo — follow
immediately in a separate PR. They need Last.fm's crowd tags rather than
MusicBrainz genres, which are too sparse to be useful, so that is a new ingest
and a new table rather than an extra query here.

## Playlists Six rules over the scrobble history and the library index, written as extended M3U into `<mirror>/_playlists/` and rebuilt every pass. | Playlist | Rule | | -------------------- | --------------------------------------------------------- | | `heavy-rotation` | Most played over the last twelve months | | `all-time` | Most played ever | | `neglected` | Played heavily once, silent for twelve months | | `deep-cuts` | Never played, from albums whose other tracks you play constantly | | `unheard-favourites` | Never played, by the artists you play most | | `unheard` | Never played, anywhere in the library | Three look at what gets played and three at what does not, because four fifths of the library has never been played and a best-of alone would ignore 41,000 tracks. ## Two decisions worth flagging **Twelve months, not ninety days.** Ninety days is the obvious window and is the wrong one: on the real history it holds a few hundred plays spread across a twenty-thousand track rotation, so nothing ranks. The report's own `top track 90d` list -- one track at 35 plays, then a cliff to 3 -- is what that looks like. **Rotation is weekly, not per pass.** A pass runs every few hours. A playlist that reorders itself each time is one that has to be re-imported each time, because the Music app imports a snapshot of a file rather than tracking it. ## Paths, and the part I could not test Lidarr knows where the FLAC is; the playlist has to point at the MP3 music-mirror made from it. `--library-root` is stripped from Lidarr's paths and they are re-rooted under `--mirror` with the suffix changed. The root is derived from the common parent of the indexed artist folders, so it agrees with Lidarr by construction rather than by being kept in step by hand. Override it if the guess is wrong. Entries are written relative to the playlist file, so one playlist works from the NAS, from a Mac over SMB, and from Linux. **A track is listed only once its mirror file is confirmed to exist.** Lidarr holding the FLAC says nothing about whether the MP3 has been encoded. When many are missing the run says so loudly — that is exactly what a wrong root looks like, every path mapping to nothing, and silence would present an empty playlist as a correct one. This is the piece I cannot verify from here; the first real run is the test. `_playlists/` survives music-mirror's prune, which only deletes `*.mp3` and skips directories that are not empty. Checked in its source, not assumed. ## Testing 80 tests, green locally and in `docker build --target test`. The playlist ones build a real library on disk with mirror MP3s beside the source paths, and cover the mapping, relative paths resolving to real files, the limit, weekly rotation stability, group-readability, and a missing mirror file being left out. ## Next Genre and mood playlists — 80s synths, high energy rock, screamo — follow immediately in a separate PR. They need Last.fm's crowd tags rather than MusicBrainz genres, which are too sparse to be useful, so that is a new ingest and a new table rather than an extra query here.
lyrathorpe added 1 commit 2026-08-24 17:52:11 +01:00
feat: write playlists into the mirror from the listening history
Build and publish container / build (pull_request) Successful in 5m13s
cda3d8463b
Six rules over the scrobble history and the library index, written as extended
M3U into <mirror>/_playlists/ and rebuilt every pass. Three look at what gets
played -- heavy rotation, all-time, and things played heavily once and silent
for a year. Three look at what does not: album tracks skipped on records
otherwise played constantly, unplayed tracks by the artists played most, and
unplayed tracks from anywhere. Four fifths of the library has never been played,
so both halves are needed.

Ninety days was the obvious window for "recent" and is the wrong one. On a real
history it holds a few hundred plays spread across a twenty-thousand track
rotation, and nothing ranks meaningfully. Twelve months does.

The two rotating playlists are shuffled by week rather than by pass. A pass runs
every few hours; a playlist that reorders itself each time is one that has to be
re-imported each time, because the Music app imports a snapshot of a file rather
than tracking it.

Lidarr knows where the lossless source is, and the playlists have to point at
the MP3s music-mirror produced from it. The library root is derived from the
common parent of the indexed artist folders, so it agrees with Lidarr by
construction instead of being kept in step by hand, and can be overridden.
Entries are relative to the playlist file, so one file works from the NAS, from
a Mac over SMB and from Linux.

A track is listed only once its mirror file has been confirmed to exist: Lidarr
holding the FLAC says nothing about whether the MP3 has been encoded. When many
are missing the run says so, because that is what a misconfigured root looks
like -- every path mapping to nothing -- and silence there would present an
empty playlist as a correct one.
lyrathorpe merged commit 05cca3508c into main 2026-08-24 17:53:03 +01:00
lyrathorpe deleted branch feat/playlists 2026-08-24 17:53:04 +01:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lyrathorpe/music-curator#9