Comparing against JRpersonal/streborn#587 surfaced two gaps: no test
pinned that a newly added source type renders the same element shape
as a known-good default (the firmware rejects the whole account
document if one source entry omits an expected element), and our DLNA
discovery only swept SSDP from the service host, missing servers only
visible from a paired speaker's own LAN segment.
Adds TestSourceXMLShapeConsistencyAcrossTypes in pkg/service/marge,
and has HandleDiscoverLibraryServers merge results from each paired
speaker's own /listMediaServers alongside the existing SSDP sweep,
deduped by UDN, with unreachable speakers skipped silently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adding a DLNA media server (setMusicServiceAccount) can leave the new
STORED_MUSIC source not fully registered on the speaker, so playing a track
fails with INVALID_SOURCE until a power-cycle. AfterTouch's health
diagnostic already recommends the no-reboot fix: a sourcesUpdated
notification makes the speaker re-fetch its account /full and re-register
its source list.
Fire that nudge automatically right after a successful registration, in
both the player (HandleAddLibraryServer) and the CLI (account add-nas), via
the existing client.NotifySourcesUpdated. It is best-effort: registration
already succeeded, so a failed nudge never fails the request (the handler
returns {account, refreshed}, the CLI prints a warning that a power-cycle
may still be needed). The handler resolves the Bose device ID from the
cached DeviceConnection.DeviceInfo, falling back to GetDeviceInfo.
Note: per the diagnostic, a power-cycle is still occasionally required, so
the nudge is an improvement, not a guarantee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DLNA UDN from discovery carries a "uuid:" prefix (e.g.
uuid:fa095ecc-...), but a SoundTouch STORED_MUSIC account is the bare UUID
plus /0 (the speaker's /sources reports the bare form). The mismatch made
the player Library tab show an "Add" button for an already-registered
server, and an Add via the UI would have registered a wrong "uuid:.../0"
account. Normalize (strip "uuid:") when mapping discovery results to the DTO
and when building the account in HandleAddLibraryServer, so the LAN list and
the registered list agree and Add builds the correct account. Verified live:
discover now returns the bare UDN, matching /sources.
Also populate the previously-unset MediaServer.Address from the
ContentDirectory control URL host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the soundtouch-player control API for browsing and playing DLNA
media-server content on a speaker, using the validated native STORED_MUSIC
path (the speaker is the DLNA control point; no AfterTouch proxy).
Routes (under /api/control):
- GET /providers/library/servers LAN-wide SSDP sweep (discovery.DiscoverMediaServers)
- GET /devices/{id}/library/servers STORED_MUSIC sources registered on this speaker (+ ready)
- POST /devices/{id}/library/servers register a server (setMusicServiceAccount; 1024 = already present)
- DELETE /devices/{id}/library/servers/{account} unregister
- GET /devices/{id}/library/browse speaker /navigate (root or container) -> location tokens
- POST /devices/{id}/library/play select a STORED_MUSIC ContentItem (type=track)
- GET /app/library SPA deep link
Browsing goes through the speaker's own /navigate so the returned location
tokens are the ones /select accepts; the raw DLNA ContentDirectory IDs are
not playable, so pkg/dlna is intentionally not on this path. All handlers
reuse existing client methods (Navigate, NavigateContainer, SelectContentItem,
GetSources, AddStoredMusicAccount, RemoveStoredMusicAccount) and the existing
APIResponse envelope. Unit tests cover play XML shape, browse mapping,
source filtering, idempotent register, and validation/404s.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>