v0.29.0: Cassette friend mode — browse a friend's library, cassette songs

- A ▾ cut from the right end of the Library button (shown once there's a
  friend) opens a popup of friends. Choosing one enters friend mode: the
  button becomes "<Name>'s Library" in light purple, the sidebar lists
  their shared playlists with a UFO follow toggle on each, and the content
  area is their library — browse only, nothing plays or edits.
- A cassette column on the far left and cassette buttons on every artist
  and album: pressed grey = send me this, medium grey on hover to let go.
  An artist cassettes all its albums and tracks (a snapshot).
- Bottom bar: Hide tracks I have (exact matches: artist, album, title,
  length within 2 s); close matches show light grey. Size of what's
  cassetted against free space: orange when Syncthing would stall at its
  free-space floor, red when it won't fit.
- Save and Close writes requests.json; Cancel discards; the ▾ or quitting
  with changes asks Save / Don't Save / Cancel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 21:27:06 -07:00
co-authored by Claude Opus 5.5
parent 759fa85153
commit 8099cfc85f
15 changed files with 1565 additions and 14 deletions
+12
View File
@@ -481,6 +481,18 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
`syncthing` run a real handshake with `pytest --syncthing`. The service
starts from `run_gui`, never from constructing `MainWindow` — tests build
windows by the dozen and must not reach a real Syncthing.
**Friend mode** (`gui/friend_mode.py`, entered from the ▾ beside Library)
shows a friend's `library.json` through a second `LibraryView` over
`cassette/friend_library.FriendSource` — the slice of `LibraryManager` that
view reads, read-only. Their track ids are *theirs*: the table is
`set_read_only(True)` (no play, no drag, no copy into my playlists, only
"lookup on youtube"), and the cassette column is `track_table.CASSETTE_FIELD`
(registered in `COLUMN_MAP`, deliberately not `ALL_COLUMNS`), live only
where `cassette_hooks` is set. Nothing is written until Save and Close
(`service.save_requests` → `requests.json`); any other exit with changes
asks "Save changes?". `cassette/matching.py` is best effort (exact vs
fuzzy); `cassette/space.py` judges the music disk (red) and Syncthing's
`minDiskFree` floor on the cassette disk (orange) separately.
- **`lintunes/cast/`** — Chromecast playback (Connections menu), using the
**media-receiver model**: `server.py` runs a `ThreadingHTTPServer` on an