v0.4.0: cast to a Chromecast from the Connections menu
The Device menu becomes Connections, with "Connect to Chromecast…" beside the Rabbit sync. It opens a dialog that spins while it searches and lists devices as they appear; picking one hands playback over, puts a small cast glyph under the volume slider, and clicking that glyph disconnects. Media-receiver model: lintunes serves the original file over the LAN on an ephemeral port and the Chromecast decodes it itself. Bit-exact — no transcode, no second lossy encode — and the device buffers for itself. The cost is that lintunes is a remote control while connected: no local PCM, so the visualizer shows the cast glyph instead of bars, and transport actions land with about a second of round trip. Player now walks its queue through a swappable PlaybackSink. It keeps owning the queue, shuffle walk, start/stop times and the play-count and scrobble bookkeeping, so casting counts plays and scrobbles exactly like local playback. set_sink() carries track, position and playing-state both ways, so connecting and disconnecting pick up mid-song. LocalSink stays in player.py because the Player tests stub Qt Multimedia in that namespace. The URL carries an opaque random token, never a path, so there is nothing to traverse with; only the last few played tracks stay resolvable and the whole map dies with the session. Range and HEAD are implemented because the device seeks by re-requesting ranges and won't report a duration without them. Failure handling, verified against a real device: a dropped socket gets a 15s grace period, since pychromecast retries on its own and a Wi-Fi blip heals itself. A real loss, another app taking the device, or a network change falls back to local playback still playing, at the same position — the sink reports the state lintunes last asked for rather than the IDLE status a dying connection pushes just before it goes. Quitting stops the device instead of leaving it fetching from a server that just died. The ~119 Apple Lossless / AIFF / protected-AAC tracks are skipped with a status-bar message; the other 21,000+ MP3 and AAC files cast natively. New dependency: pychromecast>=14.0.10, imported lazily so the app still launches where it isn't installed (the menu item then explains the install), and python_requires raised to >=3.11 to match its floor. NOTE: the other machine needs `pip install -e .` before casting appears. tests/test_round29.py: 67 tests; 440 pass overall. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,40 @@
|
||||
## Done
|
||||
|
||||
### Round 29 (2026-08-13) — Cast to Chromecast (v0.4.0)
|
||||
|
||||
The Device menu is now **Connections**, with "Connect to Chromecast…" alongside
|
||||
the Rabbit sync. Picking a device from the search dialog hands playback to it;
|
||||
a small cast glyph appears under the volume slider and clicking it disconnects.
|
||||
See `tests/test_round29.py` (67 tests).
|
||||
|
||||
Uses the **media-receiver model**: lintunes serves the original file over the
|
||||
LAN and the Chromecast decodes it. Bit-exact (no transcode, no double-lossy on
|
||||
already-lossy files) and the device buffers for itself; the cost is that
|
||||
lintunes becomes a remote control while connected — no local PCM, so the
|
||||
visualizer shows the cast glyph instead of bars, and transport actions land
|
||||
with ~1 s of round trip.
|
||||
|
||||
- [x] **`lintunes/cast/`** — `support.py` (format gate, source-address
|
||||
selection, range parsing), `server.py` (token-addressed
|
||||
`ThreadingHTTPServer` with Range/HEAD), `discovery.py` (`CastBrowser` behind
|
||||
Qt signals), `sink.py` (`CastSink`), `controller.py` (session lifecycle +
|
||||
its own `SleepInhibitor`).
|
||||
- [x] **`Player` sink split** — new `PlaybackSink` base with `LocalSink` and
|
||||
`CastSink`; `set_sink()` carries track/position/playing-state both ways, so
|
||||
connecting and disconnecting pick up mid-song. Play counts and Last.fm
|
||||
scrobbles fire identically on both paths.
|
||||
- [x] **Format handling** — 12,610 MP3 + ~8,750 AAC cast natively; the ~119
|
||||
Apple Lossless / AIFF / protected-AAC tracks are skipped with a status-bar
|
||||
message rather than stalling the device.
|
||||
- [x] **Failure handling** — a dropped socket gets a 15 s grace period
|
||||
(pychromecast retries on its own, so a Wi-Fi blip heals itself); a real loss,
|
||||
a foreign app taking the device, or a network change falls back to local
|
||||
playback **still playing**, at the same position. Quitting stops the device
|
||||
rather than leaving it fetching from a dead server.
|
||||
- [x] **New dependency: `pychromecast>=14.0.10`**, imported lazily so the app
|
||||
still launches where it isn't installed; `python_requires` raised to >=3.11
|
||||
(its floor). The menu item explains the `pip install -e .` when missing.
|
||||
|
||||
### Misc (recorded 2026-07-02, moved from TASKS.md during the Round 17 rewrite)
|
||||
|
||||
- [x] Confirm Last.fm scrobbling works
|
||||
|
||||
Reference in New Issue
Block a user