Foundation for browsing DLNA media servers and playing tracks on a
SoundTouch speaker (https://github.com/gesellix/Bose-SoundTouch/discussions/213).
- pkg/discovery/ssdp.go: a target-agnostic UPnP SSDP core. SearchSSDP sweeps
multiple targets (a typed device URN plus ssdp:all, since some servers only
answer one), fans out across all routable IPv4 interfaces, and sends each
batch in two rounds spaced 80ms apart so slower NAS/router boxes that drop
back-to-back bursts still answer. FetchDescription parses a UPnP device
description into a generic device tree with FindService/FirstIcon that
recurse through sub-devices. The XML parse is a pure function for offline
unit testing.
- pkg/discovery/mediaserver.go: DiscoverMediaServers rides the core, keeps
only devices exposing a ContentDirectory service, and dedupes by UDN. The
description->MediaServer mapping is a pure, tested function.
- pkg/dlna: a ContentDirectory browse client (Browse + DIDL-Lite parse +
IsAudioItem), consuming discovery.MediaServer. Kept separate from discovery,
mirroring how pkg/client is separate from pkg/discovery. Track metadata maps
upnp:artist / upnp:album; the audio filter accepts audio/* MIME or an
audioItem/musicTrack class.
Existing SoundTouch speaker discovery (pkg/discovery/upnp.go) is untouched;
migrating it onto the shared core is a later, de-risked step. Tests cover the
description/DIDL parsers and run the browse client against an in-process
ContentDirectory server; the parse was checked against real minidlna and
FRITZ!Box output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a dependency-free Go DLNA/UPnP MediaServer used to develop and test
the upcoming "browse a DLNA server and play on a SoundTouch" feature
(https://github.com/gesellix/Bose-SoundTouch/discussions/213).
Two faces over one content core:
- pkg/dlna/dlnatest: an in-process server (httptest) serving rootDesc.xml
and ContentDirectory Browse for an injectable content tree, for fast
cross-platform unit tests with no Docker and no multicast.
- cmd/example-dlna-server: the same handlers behind a real http.Server
plus an SSDP responder (answers M-SEARCH, periodic NOTIFY), so a real
speaker on the LAN can discover it and fetch real (silent WAV) audio.
A Docker minidlna was unusable here: on macOS the container IP in the
DIDL <res> URL is unreachable from the LAN, and its SSDP never reaches the
speakers. A native Go server embeds the host LAN IP and is discoverable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>