feat: write playlists into the mirror from the listening history
Build and publish container / build (pull_request) Successful in 5m13s
Build and publish container / build (pull_request) Successful in 5m13s
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.
This commit is contained in:
@@ -7,9 +7,10 @@ which keeps an MP3 copy of a lossless library for an iPod. This one answers the
|
||||
question that mirror cannot: which of it is worth carrying, and which of it has
|
||||
not been played in years.
|
||||
|
||||
**This is stage two.** It ingests the scrobble history, indexes the library from
|
||||
Lidarr, and matches one to the other. There are no playlists yet, and it writes
|
||||
nothing back — every Lidarr call is a `GET`. See "Where this is going" below.
|
||||
**This is stage three.** It ingests the scrobble history, indexes the library
|
||||
from Lidarr, matches one to the other, and writes playlists into the mirror.
|
||||
Nothing is written back to Lidarr — every call there is a `GET`. See "Where this
|
||||
is going" below.
|
||||
|
||||
## What it does today
|
||||
|
||||
@@ -18,6 +19,7 @@ nothing back — every Lidarr call is a `GET`. See "Where this is going" below.
|
||||
- Indexes every artist, album and track Lidarr knows about, with file paths and
|
||||
the date each file landed.
|
||||
- Ties the two together and reports how well it managed.
|
||||
- Writes M3U playlists into the mirror, from the listening history.
|
||||
|
||||
## Matching
|
||||
|
||||
@@ -126,6 +128,50 @@ and a shared title is a weak proxy for a shared song:
|
||||
Only the first is worth chasing. Counting all three as matcher failures
|
||||
overstates the problem and would over-block the cull.
|
||||
|
||||
## Playlists
|
||||
|
||||
Written into `<mirror>/_playlists/` as extended M3U, rebuilt every pass. Six
|
||||
rules, capped at `--playlist-limit` tracks each:
|
||||
|
||||
| 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 |
|
||||
|
||||
Ninety days was the obvious window for "recent" and is the wrong one: on a real
|
||||
history it holds a few hundred plays spread thinly across a twenty-thousand
|
||||
track rotation, so nothing ranks meaningfully. Twelve months does.
|
||||
|
||||
The two `unheard` playlists rotate **weekly**, not per pass. A pass runs every
|
||||
few hours, and a playlist that reorders itself each time is one that has to be
|
||||
re-imported each time — the Music app imports a snapshot of a file, it does not
|
||||
track it.
|
||||
|
||||
### Paths
|
||||
|
||||
Lidarr knows where the lossless source is; the playlists have to point at the
|
||||
MP3s music-mirror made from it. The mapping strips a library root from Lidarr's
|
||||
track paths and re-roots them under the mirror, with the suffix changed.
|
||||
|
||||
`--library-root` is derived from the common parent of the indexed artist folders
|
||||
when unset, so it agrees with Lidarr by construction rather than by being kept
|
||||
in step by hand. Override it if that 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, without rewriting.
|
||||
|
||||
A track is only listed once its mirror file has been confirmed to exist. Lidarr
|
||||
holding the FLAC says nothing about whether the MP3 has been encoded yet. If a
|
||||
large number are missing, the run says so — that is what a wrong `--library-root`
|
||||
or `--mirror` looks like, since the paths then map to nothing at all.
|
||||
|
||||
`_playlists/` survives music-mirror's prune: it only deletes `*.mp3`, and its
|
||||
empty-directory sweep skips a directory holding M3Us.
|
||||
|
||||
## How the ingest works
|
||||
|
||||
Two halves, both taking their bounds from the database rather than from a saved
|
||||
@@ -180,6 +226,9 @@ music-curator --report-only # report on the store, fetch nothing
|
||||
| `--backfill-limit` | `MUSIC_CURATOR_BACKFILL_LIMIT` | `0` | Cap backfill requests per pass; 0 for no cap |
|
||||
| `--lidarr-url` | `MUSIC_CURATOR_LIDARR_URL` | unset | Lidarr base URL, e.g. `http://lidarr:8686` |
|
||||
| `--lidarr-api-key` | `MUSIC_CURATOR_LIDARR_API_KEY` | unset | Lidarr API key |
|
||||
| `--mirror` | `MUSIC_CURATOR_MIRROR` | unset | Root of the MP3 mirror; playlists go here |
|
||||
| `--library-root` | `MUSIC_CURATOR_LIBRARY_ROOT` | derived | Prefix to strip from Lidarr's paths |
|
||||
| `--playlist-limit` | `MUSIC_CURATOR_PLAYLIST_LIMIT` | `100` | Most tracks in any one playlist |
|
||||
| `--skip-index` | — | off | Match against the index already held |
|
||||
| `--report-only` | — | off | Report without fetching |
|
||||
|
||||
@@ -228,7 +277,8 @@ nix shell nixpkgs#python3Packages.pytest -c pytest
|
||||
| ------------------------------------------------ | ------------ |
|
||||
| Last.fm ingest and store | done |
|
||||
| Lidarr index and the scrobble-to-track matcher | done |
|
||||
| M3U playlists written into the mirror | next |
|
||||
| M3U playlists from the listening history | done |
|
||||
| Genre and mood playlists from Last.fm tags | next |
|
||||
| Cold-music report, unmonitoring what is not played | last |
|
||||
|
||||
The cull will unmonitor cold albums in Lidarr and tag their artists. It will
|
||||
|
||||
Reference in New Issue
Block a user