The guard was accidentally placed in HandlePlayRadioBrowser instead of
HandleDevicePlay in the initial fix commit, then removed from there by
the build-fix commit — leaving HandleDevicePlay with no guard at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous edit accidentally inserted the TUNEIN placeholder guard
into HandlePlayRadioBrowser, which uses a different req struct without
SourceAccount/Source fields, breaking the build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Speakers echo back the source name as SourceAccount when no real
credential is set (e.g. SourceAccount="TUNEIN" for a TUNEIN source).
HandleDevicePlay was forwarding this verbatim, causing the speaker to
try authenticating with the source name as a TuneIn account and
returning INVALID_SOURCE.
Clear SourceAccount when it equals Source; preserve it when it differs
(real credentials such as Spotify or STORED_MUSIC UUIDs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TuneIn's profiles API caps initial results at ~10 per container (Stations,
Shows, etc.) and exposes a Pivots.More.Url cursor for the remainder. This
change wires that cursor through the stack so users can load additional
results without leaving the search view.
- tuneInSearchSection now extracts Pivots.More.Url as bmx_next when
itemToken is present; absent for containers already at their limit
- TuneInSearchNext fetches the cursor URL, which returns a flat Items[]
(not nested containers), and maps Station/Program/Topic items using
the existing play/profile builders
- New GET /v1/search/next and /api/tunein/search/next endpoints with
matching handlers in both service paths
- TuneInBrowser: flat items state replaced with per-section sections
state; each section shows a header label and a Load more button when
a cursor is available; browse/navigate mode is unaffected
Relates to #336.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Refactors BMX service to support multiple radio providers
- Adds RadioBrowser.com API integration with search and browse
- Splits TuneIn logic into separate module for better organization
- Adds new web UI components for radio station discovery
- Includes new SVG icons for RadioBrowser branding
Ports app's commit b040c8a. Five new handlers + five new routes for
master/slave stereo-pair and multi-room management; the Zone.js
frontend was already shipped in the Preact swap.
HandleGetZone GET /api/zone/{id}
Returns zone info enriched with member names and role flags
(isMaster / isSlave / isStandalone) computed from the perspective
of the queried device. Each member carries IP, hwID, and friendly
name so the frontend can render readable rows.
HandleZoneAdd POST /api/zone/{id}/add/{slaveId}
Adds a slave to the zone where {id} is or becomes the master.
Standalone master gets a fresh ZoneRequest; existing zone is
extended via ToZoneRequest + AddMember.
HandleZoneRemove POST /api/zone/{id}/remove/{slaveId}
Removes a named slave from the master's existing zone.
HandleZoneDissolve POST /api/zone/{id}/dissolve
Issues a single-member ZoneRequest so the master goes standalone.
HandleZoneLeave POST /api/zone/{id}/leave
Slave-side leave: looks up the master via findIPByHwID using the
slave's current zone info, then dispatches RemoveMember against
the master's client (the speaker protocol requires the master to
own the SetZone call).
Translation notes:
- All handlers go through app.GetDevice(id) instead of direct
app.Devices[id] access — matches main's encapsulated-registry
refactor (post-base on main, see registry_test.go).
- findIPByHwID iterates via app.DeviceSnapshot() instead of ranging
over the raw map.
- pkg/client (GetZone/SetZone) and pkg/models (ZoneInfo/ZoneRequest/
Member/NewZoneRequest/AddMember/RemoveMember/IsStandalone/
ToZoneRequest) API surface confirmed unchanged from app's base —
verbatim function calls.
Risk recap (per the earlier audit): this was flagged medium-risk
because of pkg/client zone-API drift. Verified clean — all symbols
exist with the expected signatures on current main. The #252 stereo-
pair work that landed on main was in cmd/soundtouch-cli/cmd_group.go
(parallel POST to LEFT and RIGHT), which doesn't intersect with the
single-master SetZone pattern these handlers use.
go build ./... clean. go test ./... clean (only pre-existing
TestDocsConsistency fails, untracked-file issue, unrelated).
golangci-lint run ./pkg/service/soundtouchweb/... ./cmd/soundtouch-web/...
0 issues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports app's commit 3122c4e to the package layout. Two new handlers
and route registrations; the frontend was already shipped in the
Preact swap.
HandleDeviceRecents GET /api/device-recents/{id}
Returns the speaker's /recents list as APIResponse{Success,Data}.
Backs the Recents.js component (lazy-loaded list under the
device-detail view; hides itself when the device returns no
recents).
HandleDevicePlay POST /api/device-play/{id}
Generic content-item player. Decodes a {source,type,location,
sourceAccount,itemName,containerArt,isPresetable} JSON body into
a *models.ContentItem and runs Client.SelectContentItem. Used by
Recents.js to replay items the speaker reports, regardless of
source — TuneIn, Spotify, AUX, etc. Different from HandlePlayTuneIn
which is TuneIn-specific.
Translation note: app's bodies used app.Devices[id] directly; main's
registry is encapsulated behind GetDevice/AddDevice/TouchDevice (see
the post-base refactor that introduced registry_test.go), so this
commit uses app.GetDevice(id) instead. Same lookup, just through the
maintained API.
go build ./... clean. go test ./... clean (only pre-existing
TestDocsConsistency fails, untracked-file issue, unrelated).
golangci-lint run ./pkg/service/soundtouchweb/... 0 issues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical relocation only — zero semantic change. Sets up the package
layout that the future Preact-UI rewrite (branch `app`) wants, while
preserving every line of main's current logic. Subsequent commits will
land the additive parts (frontend rewrite, recents, zones, bass control)
on top of this clean base.
Moves (`git mv`, content unchanged except package decl):
cmd/soundtouch-web/handlers/handlers.go → pkg/service/soundtouchweb/handler.go
cmd/soundtouch-web/handlers/handlers_test.go → pkg/service/soundtouchweb/handler_test.go
cmd/soundtouch-web/handlers/websocket.go → pkg/service/soundtouchweb/websocket.go
cmd/soundtouch-web/handlers/registry_test.go → pkg/service/soundtouchweb/registry_test.go
cmd/soundtouch-web/webtypes/types.go → pkg/service/soundtouchweb/webtypes/types.go
cmd/soundtouch-web/webtypes/types_test.go → pkg/service/soundtouchweb/webtypes/types_test.go
cmd/soundtouch-web/webtypes/status_test.go → pkg/service/soundtouchweb/webtypes/status_test.go
cmd/soundtouch-web/static/img/tunein-{dark,mono}.svg → pkg/service/soundtouchweb/static/img/
Adjustments:
- `package handlers` → `package soundtouchweb` in the 4 moved handler-tier
files (plus their package-doc comments).
- Import paths rewritten in cmd/soundtouch-web/{main.go,spa_test.go} and
in the moved files themselves: cmd/soundtouch-web/{handlers,webtypes}
→ pkg/service/soundtouchweb/{,webtypes}.
- `handlers.` selector renamed to `soundtouchweb.` in the callers.
- `.golangci.yml` errcheck waiver extended from `cmd/.*\.go` to also
cover `pkg/service/soundtouchweb/.*\.go`. Same code that the
cmd-tier waiver applied to; same waiver follows it. Documented as
a carry-over with the intent to tighten in a follow-up review.
Not changed:
- `cmd/soundtouch-web/main.go` keeps the `//go:embed static` pointing at
the still-vanilla `cmd/soundtouch-web/static/`. The frontend rewrite
(Preact UI) lands in a later commit; this one is mechanical.
- `cmd/soundtouch-web/resolve_bind_addr_test.go` stays put — it tests
main.go-local flag plumbing.
go build ./... clean. go test ./... clean (only pre-existing
TestDocsConsistency fails, untracked-file issue, unrelated).
golangci-lint run ./... 0 issues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>