SoundHub is a local-network control application for smart audio devices, starting with Bose SoundTouch speakers. Built with a modern tech stack featuring Angular frontend, .NET 8 backend, and Docker deployment.
- Device Configuration: Add, edit, remove, and discover devices from the web UI
- Device Discovery: Automatic network scanning to find compatible devices
- Ping Verification: Audible connectivity test for devices
- Device Control: Power, volume, presets, and Bluetooth pairing
- Volume Control: Adjustable volume slider with mute toggle on device details page
- Now Playing Display: LCD-style now playing text with auto-scrolling and customizable theme/speed
- Web Interface: Modern landing page with device list, settings, and device control
- Internationalization: Runtime language switching (English and Polish)
- Vendor Abstraction: Extensible device adapter pattern for supporting multiple vendors
- Local Station Storage: Self-hosted station JSON files for
LOCAL_INTERNET_RADIOpresets, served via the API or a reverse proxy (e.g. Caddy) - Secure Secrets: AES-256-CBC encrypted secrets storage
- File-Based Configuration: Simple devices.json for device metadata
- REST API: Well-documented OpenAPI/Swagger endpoints
- Containerized: Docker-ready for easy deployment
- Architecture Overview - System design and diagrams
- API Reference - REST API documentation
- Device Configuration Guide - User guide for managing devices
- Testing Guide - Unit testing, E2E testing, and mocking patterns
- devices.json Schema - Configuration file format
| Route | Description |
|---|---|
/ |
Landing page β displays list of configured devices |
/settings |
Settings page β language selection and LCD display settings |
/settings/devices |
Device configuration page β manage configured devices |
/devices/:id |
Device details page β view and control a specific device (now playing, power, volume, presets) |
For a detailed architecture overview including layered diagrams, monorepo structure, device adapter pattern, and data flow, see docs/architecture.md.
Frontend:
- Angular (standalone components)
- Nx monorepo for code organization
- TypeScript, SCSS
- Jest for testing
Backend:
- .NET 8 Web API
- Clean Architecture (Domain, Application, Infrastructure, Presentation)
- Structured logging (JSON format)
- Health checks for Docker
- Swashbuckle/OpenAPI documentation
Deployment:
- Docker & Docker Compose
- Multi-stage builds for optimized images
- Volume mounts for persistent data
- Health checks and auto-restart
soundHub/
βββ frontend/ # Nx Angular workspace
β βββ src/ # Main Angular application
β βββ e2e/ # Playwright E2E tests
β βββ libs/frontend/ # Shared libraries
β βββ feature/ # Feature modules
β βββ data-access/ # Services & state management
β βββ ui/ # UI components
β βββ shared/ # Utilities & types
βββ services/ # .NET backend solution
β βββ SoundHub.Api/ # Web API controllers & startup
β βββ SoundHub.Application/ # Business logic & services
β βββ SoundHub.Domain/ # Entities & interfaces
β βββ SoundHub.Infrastructure/ # Adapters & persistence
β βββ tests/SoundHub.Tests/ # xUnit test project
βββ data/ # Volume mount for config & secrets
βββ docs/ # Documentation & diagrams
β βββ architecture.md # Architecture overview with Mermaid diagrams
βββ openspec/ # OpenSpec change proposals
βββ docker-compose.yml # Local development environment
βββ Dockerfile.api # API container
βββ Dockerfile.web # Web container
The core abstraction for vendor-specific device control:
public interface IDeviceAdapter
{
string VendorId { get; }
Task<IReadOnlySet<string>> GetCapabilitiesAsync(string deviceId);
Task<DeviceStatus> GetStatusAsync(string deviceId);
Task SetPowerAsync(string deviceId, bool on);
// ... other control methods
}Each vendor (e.g., Bose SoundTouch) implements this interface. The adapter registry resolves the correct implementation at runtime.
- Docker & Docker Compose
- (Optional) .NET 8 SDK for local API development
- (Optional) Node.js 20+ for local frontend development
-
Clone the repository
git clone https://github.com/yourusername/soundHub.git cd soundHub -
Set
PUBLIC_HOST_URLindocker-compose.ymlThe API needs to know the public base URL so it can generate correct URLs for locally-stored station files (used by SoundTouch devices to fetch internet radio definitions). Set it to the Caddy address that clients will use β either the hostname or the IP:
environment: - PUBLIC_HOST_URL=http://<your-host>.local/soundhub # or http://192.168.1.x/soundhub
If this is not set, the API falls back to
http://localhost:5001, which only works for local access. -
Start all services
docker-compose up --build
-
Access the application
- Web UI: http://localhost:5002
- API: http://localhost:5001
- Swagger UI: http://localhost:5001/swagger
-
Stop services
docker-compose down
Use Caddy to expose the app at http://<your-host>.local/soundhub/ instead of a bare port. This supports multiple apps running on the same host β each under its own path.
Replace <your-host> with your Mac's hostname (find it in System Settings β General β Sharing, or run hostname -s in the terminal).
-
Install Caddy
brew install caddy
-
Configure
/opt/homebrew/etc/Caddyfilehttp://<your-host>.local, http://<your-ip> { # SoundHub frontend β redirect bare path to trailing-slash version redir /soundhub /soundhub/ permanent # SoundHub local preset station files (served from disk) handle /soundhub/presets/* { uri strip_prefix /soundhub/presets root * /path/to/soundHub/data/presets file_server } handle /soundhub/* { uri strip_prefix /soundhub reverse_proxy localhost:5002 } # SoundHub API handle /soundhub/api/* { uri strip_prefix /soundhub reverse_proxy localhost:5001 } # Add more apps here: # redir /otherapp /otherapp/ permanent # handle /otherapp/* { # uri strip_prefix /otherapp # reverse_proxy localhost:XXXX # } } -
Start Caddy as a background service
brew services start caddy
To reload after config changes:
brew services restart caddy
-
Access the application (via hostname or IP)
- Web UI: http://.local/soundhub/ or http:///soundhub/
- API: http://.local/soundhub/api/
- Swagger UI: http://.local/soundhub/api/swagger
<your-host>.localresolves automatically via mDNS/Bonjour on any macOS, iOS, or Linux client on the local network β no/etc/hostschanges needed on client machines. Using the IP address directly works on all platforms including Windows.
Backend (.NET API):
cd soundHub
dotnet build
dotnet run --project src/SoundHub.ApiFrontend (Angular):
cd frontend
npm install
npx nx serve web| Variable | Default | Description |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Production |
ASP.NET Core environment |
DevicesFilePath |
/data/devices.json |
Path to device configuration |
SecretsFilePath |
/data/secrets.json |
Path to encrypted secrets |
MasterPasswordFile |
/run/secrets/master_password |
Path to Docker secret file containing master password |
MasterPassword |
default-dev-password |
Fallback master password (used when file not available) |
The master password for encrypting secrets is managed via Docker secrets:
Development Setup:
-
Create a secrets directory and password file:
mkdir -p secrets echo "your-secure-password" > secrets/master_password.txt
-
The
docker-compose.ymlis pre-configured to use this file as a Docker secret.
Production Setup (Docker Swarm):
# Create a Docker secret
echo "your-production-password" | docker secret create master_password -
# Reference in docker-compose.yml:
secrets:
master_password:
external: trueHow it works:
- Docker mounts the secret file at
/run/secrets/master_passwordinside the container - The API reads the password from this file (via
MasterPasswordFileconfiguration) - If the file doesn't exist, it falls back to the
MasterPasswordenvironment variable
data/devices.json - Device metadata (vendor-grouped):
{
"NetworkMask": "192.168.1.0/24",
"SoundTouch": {
"Devices": [
{
"Id": "...",
"Vendor": "bose-soundtouch",
"Name": "Living Room Speaker",
"IpAddress": "192.168.1.131",
"Capabilities": ["power", "volume", "presets", "ping"],
"DateTimeAdded": "2025-12-31T12:00:00.000Z"
}
]
}
}For detailed schema documentation, see docs/devices-schema.md.
data/secrets.json - Encrypted secrets (AES-256-CBC):
[
{
"SecretName": "SpotifyAccountPassword",
"SecretValue": "<encrypted-base64>"
}
]Backend:
cd services
dotnet testFrontend:
cd frontend
npx nx testFor integration testing against real Bose SoundTouch devices:
-
Ensure device is on the network - The device should be reachable via its IP address on port 8090.
-
Add the device via API:
curl -X POST http://localhost:5001/api/devices \ -H "Content-Type: application/json" \ -d '{"name": "Living Room", "ipAddress": "192.168.1.100", "vendor": "bose-soundtouch", "port": 8090}'
-
Test device endpoints:
# Get device info curl http://localhost:5001/api/devices/{id}/info # Get now playing curl http://localhost:5001/api/devices/{id}/nowPlaying # Get volume curl http://localhost:5001/api/devices/{id}/volume # Set volume to 30% curl -X POST http://localhost:5001/api/devices/{id}/volume \ -H "Content-Type: application/json" \ -d '{"level": 30}' # Play preset 1 curl -X POST http://localhost:5001/api/devices/{id}/presets/1/play
-
Device discovery - Scan your local network for SoundTouch devices:
curl http://localhost:5001/api/devices/discover?vendor=bose-soundtouch
Note: Integration tests require a real SoundTouch device on the network. Unit tests use mocked HTTP responses and don't require hardware.
Run all tests in CI:
# See .github/workflows/ci.ymlOnce the API is running, access interactive documentation:
- Swagger UI: http://localhost:5001/swagger
- OpenAPI JSON: http://localhost:5001/swagger/v1/swagger.json
For detailed API documentation, see docs/api-reference.md.
GET /api/devices- List all devicesPOST /api/devices- Add a deviceGET /api/devices/{id}- Get device by IDPUT /api/devices/{id}- Update a deviceDELETE /api/devices/{id}- Remove a devicePOST /api/devices/discover- Discover devices on LANGET /api/devices/{id}/ping- Ping device for connectivity
GET /api/config/network-mask- Get discovery network maskPUT /api/config/network-mask- Set discovery network maskGET /api/vendors- List supported vendors
GET /api/devices/{id}/status- Get device status (power, volume, source)GET /api/devices/{id}/info- Get detailed device info (name, type, MAC, software version)GET /api/devices/{id}/nowPlaying- Get current playback info (track, artist, album, source)
POST /api/devices/{id}/power- Set power state ({ "on": true|false })GET /api/devices/{id}/volume- Get volume info (target, actual, mute state)POST /api/devices/{id}/volume- Set volume ({ "level": 0-100 })POST /api/devices/{id}/bluetooth/pairing- Enter Bluetooth pairing mode
GET /api/devices/{id}/presets- List device presets (1-6)POST /api/devices/{id}/presets/{presetNumber}/play- Play a preset (1-6)
GET /health- Health check
- Secrets Encryption: AES-256-CBC with PBKDF2 key derivation
- Master Password: Retrieved from Docker secret file (
/run/secrets/master_password) with fallback to environment variable - Key Storage: SQLite-based NSS-style key4.db for encrypted key storage
- CORS: Configured for frontend origin only
- HTTPS: Enabled in production (configure certificates in appsettings)
Frontend Unit Tests:
cd frontend
npx nx run-many --target=test --all # Run all unit tests
npx nx affected --target=test # Run tests for affected projects
npx nx test feature # Run specific library tests
npx nx test feature --watch # Watch modeFrontend E2E Tests:
cd frontend
npx nx e2e e2e # Run Playwright tests
npx nx e2e e2e --headed # Run with visible browserBackend Tests:
cd services
dotnet test # Run all .NET tests
dotnet test --coverage # With coverage- Router Mocking: When testing components with
RouterLink, ensure your Router mock includes:navigate(),createUrlTree(),serializeUrl(), andeventsobservable - Service Mocking: Mock external dependencies using Jest for Angular and Moq for .NET
- Signal Testing: Verify both signal state changes and computed value updates
- E2E Testing: Use semantic selectors and test complete user journeys
For detailed testing patterns and troubleshooting, see docs/testing-guide.md.
See CONTRIBUTING.md for development guidelines, commit conventions, and testing practices.
[Your License Here]
- SoundTouch WebServices API
- Nx for monorepo tooling
- .NET community for clean architecture patterns
Questions or issues? Open an issue on GitHub!