v0.31.0: Cassette followed playlists — a friend's playlist in your sidebar

- A followed playlist (the UFO in friend mode) appears in my sidebar with
  a purple UFO: mixed in with my playlists alphabetically, or inside a
  folder named after the friend (per-friend, in Sync Settings). It's never
  a Playlist of mine, so it stays out of the library and its merges.
- It plays from the cache. Player's queue accepts friend-track keys,
  resolved through Cassette: they scrobble to Last.fm but don't count in
  my play journal. Songs still on their way are grey and skipped.
- The cassette column there is hidden by default (right-click the
  header); pressing one takes the song into my library — straight from
  the cache if it's there, otherwise by asking for it.
- Sync Settings per friend: keep followed songs separate (default) or add
  them to my library. Unfollowing (right-click) frees their cache.
- The bottom bar says when the friend's folder last synced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 21:40:59 -07:00
co-authored by Claude Opus 5.5
parent ea7f57e38b
commit 87d43f8e0c
14 changed files with 809 additions and 24 deletions
+9
View File
@@ -505,6 +505,15 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
already in my library is never imported twice, which is what makes a crash
between import and requests-rewrite harmless; a missing `library.json`
prunes nothing.
**Followed playlists** (a UFO in friend mode) are never `Playlist`s of mine:
`PlaylistTree.followed_provider` injects them (mixed alphabetically or in a
friend folder, per `Friend.layout`), and `gui/followed_view.py` plays them
from `friends/<token>/cache/`. Player queue entries may be **string keys**
(`cassette:<token>:<their id>`) resolved by `Player.track_resolver`; a key
scrobbles (`track_finished`) but never calls `record_play`. Requests are
always computed by `service._write_requests` (cassetted ∪ followed songs not
yet cached), so a save never re-asks for what's already in the cache, and a
pass prunes cache files no followed playlist uses.
- **`lintunes/cast/`** — Chromecast playback (Connections menu), using the
**media-receiver model**: `server.py` runs a `ThreadingHTTPServer` on an