v0.30.0: Cassette delivery — requested songs arrive in your library

- Sending: a friend's requests ∩ what I still offer is copied into my
  outbox (via a .part name Syncthing never sees); whatever they no longer
  request is deleted, so the outbox cleans itself up.
- Receiving: a finished file I cassetted is imported exactly like Add to
  Library — organized into Artist/Album, tags read, date added now, none
  of their listening history — and a song wanted only for a followed
  playlist lands in the cache. Then requests.json is rewritten without
  what arrived and without what they stopped offering.
- Safe to repeat: Syncthing temp files ignored, Syncthing asked whether a
  file is whole, and an exact match already in my library is never
  imported twice.
- Folders are watched (re-armed after each rename), with a slow fallback.
- file_importer split into stage_file (off the GUI thread) and
  track_from_file (date added = now).
- pytest --syncthing now runs a real invite → share → request → deliver →
  cleanup between two Syncthing instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 21:32:25 -07:00
co-authored by Claude Opus 5.5
parent 8099cfc85f
commit ea7f57e38b
9 changed files with 889 additions and 4 deletions
+12
View File
@@ -493,6 +493,18 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
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.
**Delivery** (`cassette/delivery.py` + `gui/cassette_delivery.py`) is
rebuilt from the folders on every pass, both directions: sender = their
`requests.json` ∩ what I offer *now* → `outbox/<id> ~ <name>` (copied to a
`.part` the `.stignore` hides, then renamed), and anything no longer
requested is deleted — a request vanishing *is* the delivery receipt.
Receiver: planning on the GUI thread (it reads the library), copying on
`DeliveryWorker`'s thread (`file_importer.stage_file` — the import split so
the copy is off the GUI thread), `manager.add_track` back on the GUI thread
(`track_from_file`: date added now, no listening history). An exact match
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.
- **`lintunes/cast/`** — Chromecast playback (Connections menu), using the
**media-receiver model**: `server.py` runs a `ThreadingHTTPServer` on an