diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c4d476 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Node +node_modules/ +*.npm +package-lock.json +yarn.lock + +# Build +dashboard/dist/ +*.tsbuildinfo + +# Dependencies +.env +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Temporary +*.log +tmp/ +temp/ + +# Docker volumes +data/ diff --git a/Makefile b/Makefile index 6e17da5..0cc6012 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ -.PHONY: dev boot stop logs rebuild status pipes +.PHONY: dev boot stop logs rebuild status pipes mac-audio spotify + +# ── Docker ───────────────────────────────────────────────────────────────────── dev: pipes docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d @@ -7,16 +9,44 @@ boot: pipes docker compose up -d stop: + docker compose -f docker-compose.yml -f docker-compose.dev.yml down + +stop-boot: docker compose down logs: - docker compose logs -f + docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f rebuild: - docker compose build --no-cache + docker compose -f docker-compose.yml -f docker-compose.dev.yml build --no-cache status: - docker compose ps + docker compose -f docker-compose.yml -f docker-compose.dev.yml ps pipes: @bash scripts/init-pipes.sh + +# ── Mac native audio (dev) ───────────────────────────────────────────────────── +# Runs a real Snapcast client on the Mac, connected to the Docker snapserver. +# Audio plays through Mac speakers. This is the "mac-audio" zone. +# Requires: brew install snapcast + +mac-audio: + @echo "Starting Snapcast client → Mac speakers (zone: mac-audio)" + @which snapclient > /dev/null || (echo "Installing snapcast via Homebrew..." && brew install snapcast) + snapclient --host localhost --port 1704 --hostID mac-audio --player default + +# ── Spotify Connect on Mac ───────────────────────────────────────────────────── +# Runs librespot natively on Mac. The Spotify app will see "Bordanlage" as device. +# Audio goes through Mac speakers directly (rodio backend). +# Requires: brew install librespot +# Note: For Snapcast multiroom, use the pipe backend instead (boot mode only). + +spotify: + @echo "Starting Spotify Connect (librespot) on Mac..." + @which librespot > /dev/null || (echo "Installing librespot via Homebrew..." && brew install librespot) + librespot \ + --name "$${SPOTIFY_NAME:-Bordanlage}" \ + --bitrate $${SPOTIFY_BITRATE:-320} \ + --backend rodio \ + --zeroconf-port 57621 diff --git a/README.md b/README.md index 91ecf0c..c0a3709 100644 --- a/README.md +++ b/README.md @@ -1,125 +1,250 @@ # Bordanlage – Boat Onboard System -A complete multiroom audio + navigation dashboard system for boats. -Runs on any Docker-capable computer – fully simulated in dev mode (no hardware needed). +Multiroom audio + navigation dashboard for boats. +Full Docker stack — works on any machine, no hardware required in dev mode. --- -## Prerequisites +## Stack -- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or Docker Engine + Compose) -- `make` -- For Spotify Connect: a Spotify Premium account +| Service | Image / Build | Purpose | +|---|---|---| +| SignalK | `signalk/signalk-server` | NMEA 0183/2000 gateway, WebSocket delta stream | +| Snapserver | custom build (v0.35.0) | Multiroom audio server, JSON-RPC on :1705 | +| zone-salon/cockpit/bug/heck | custom build (v0.35.0) | Audio zones (snapclient) | +| Mopidy + Iris | custom Dockerfile | Local music, TuneIn radio, HTTP control on :6680 | +| librespot | custom build (v0.5.0) | Spotify Connect receiver, pipe backend | +| shairport-sync | `mikebrady/shairport-sync` | AirPlay 2 receiver, pipe backend | +| Jellyfin | `jellyfin/jellyfin` | Media library, :8096 | +| SignalK | `signalk/signalk-server` | Navigation data, demo NMEA in dev mode | +| Portainer | `portainer/portainer-ce` | Docker management UI, :9000 | +| Dashboard | React 18 + Vite | Touch UI, built with Vite (dev: HMR on :8090) | + +Audio flow: `librespot / shairport / mopidy` → named pipe (PCM) → `snapserver` → `snapclient` zones --- -## Quick Start +## Quick Start (Dev) ```bash -cd bordanlage make dev ``` -Dashboard: **http://localhost:8080** +Dashboard with hot-reload: **http://localhost:8090** + +On first run, Docker builds the custom images (snapserver, snapclient, mopidy, librespot stub). Subsequent starts are instant. + +### Mac audio output + +```bash +make mac-audio # runs snapclient natively via Homebrew → Mac speakers +make spotify # runs librespot natively via Homebrew → Spotify Connect on Mac +``` + +Both commands require `brew install snapcast` / `brew install librespot` (auto-installed if missing). --- ## Service URLs -| Service | URL | Description | -|---------------|------------------------------|------------------------------| -| Dashboard | http://localhost:8080 | Main touch UI | -| SignalK | http://localhost:3000 | Navigation data + chart viewer | -| Mopidy/Iris | http://localhost:6680/iris/ | Music player UI | -| Snapcast Web | http://localhost:1780 | Multiroom audio control | -| Jellyfin | http://localhost:8096 | Media library | -| Portainer | http://localhost:9000 | Docker management | +| Service | URL | Notes | +|---|---|---| +| Dashboard (dev) | http://localhost:8090 | Vite HMR, live reload | +| Dashboard (prod) | http://localhost:8080 | nginx-served built bundle | +| SignalK | http://localhost:3000 | Admin: `admin` / `bordanlage` | +| Mopidy/Iris | http://localhost:6680/iris/ | Music player UI | +| Snapcast Web | http://localhost:1780 | Zone control (Snapweb) | +| Jellyfin | http://localhost:8096 | Media library | +| Portainer | http://localhost:9000 | Docker management | + +--- + +## Make Targets + +``` +make dev # start dev stack (Vite HMR, null audio, SignalK demo data) +make boot # start production stack (full audio pipeline) +make stop # stop dev stack +make stop-boot # stop production stack +make logs # tail logs (dev stack) +make rebuild # force rebuild all images (dev stack) +make status # show container status +make pipes # create audio named pipes in /tmp/audio (for boot mode) +make mac-audio # run snapclient natively on Mac (Homebrew) +make spotify # run librespot natively on Mac (Homebrew, Spotify Connect) +``` --- ## Spotify Connect -1. Run `make dev` (or `make boot` on the boat) -2. Open Spotify on your phone/computer -3. Tap the device icon (bottom right) → look for **"Bordanlage"** -4. If it doesn't appear automatically: Go to **Connect to a Device** → enter the IP of the host machine manually +**Dev mode (Mac):** run `make spotify` — shows as "Bordanlage" in Spotify app. -> On Linux (boat): set `network_mode: host` for the `librespot` service in `docker-compose.yml` for reliable mDNS discovery. +**Boot mode (boat):** librespot runs inside Docker, pipe backend, writes PCM to `/tmp/audio/spotify.pcm`. + +> On Linux (boat), set `network_mode: host` for `librespot` and `shairport` in `docker-compose.yml` for reliable mDNS/Bonjour discovery. --- ## AirPlay -1. Ensure the `shairport` container is running -2. On your iPhone/Mac: open Control Center → tap AirPlay → select **"Bordanlage AirPlay"** +The `shairport` container runs `shairport-sync` with pipe output. -> On Mac: AirPlay works natively via Bonjour. -> On Windows WSL2: the `avahi` container in `docker-compose.dev.yml` handles mDNS. +1. On iPhone/Mac: Control Center → AirPlay → select **"Bordanlage AirPlay"** +2. Audio goes through the snapserver multiroom pipeline + +Config: `config/shairport.conf` --- -## Adding Music +## Adding Music (Mopidy) -Drop audio files into `./music/`. Mopidy and Jellyfin both mount this directory. +Drop files into `./music/`. Trigger a library scan: -Trigger a Mopidy library scan: ```bash -curl -s http://localhost:6680/mopidy/rpc -d '{"jsonrpc":"2.0","id":1,"method":"local.scan"}' \ - -H "Content-Type: application/json" +curl -s http://localhost:6680/mopidy/rpc \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"local.scan"}' ``` -For Jellyfin: open http://localhost:8096 → Settings → Libraries → Scan. - --- -## Connecting Real NMEA Hardware +## Zones -Edit `docker-compose.yml` and uncomment the `signalk` device section: +Four zones are pre-configured: -```yaml -signalk: - devices: - - /dev/ttyUSB0:/dev/ttyUSB0 # NMEA 0183 via USB-Serial -``` +| Zone | hostID | Production use | +|---|---|---| +| Salon | zone-salon | Below-deck salon speakers | +| Cockpit | zone-cockpit | Cockpit/helm area | +| Bug | zone-bug | Forward cabin (Bug = bow in German) | +| Heck | zone-heck | Stern (Heck = stern in German) | -Then configure the NMEA connection in SignalK at http://localhost:3000 → Server → Connections. - -For NMEA 2000: use a Yacht Devices YDNU-02 or similar USB gateway. +Each zone runs `snapclient` connecting to `snapserver` on the Docker internal network. +In dev mode the zones use `--player file:filename=null` (audio discarded, no hardware needed). +In production, replace with `--player alsa --soundcard hw:N,0` and expose `/dev/snd`. --- -## Migration to Real Boat +## Environment Variables -Changes needed in `docker-compose.yml`: +Copy `.env.example` to `.env` and adjust: -1. **Audio output per zone**: add `--soundcard hw:N,0` to each `zone-*` command and uncomment `/dev/snd` -2. **Spotify/AirPlay discovery**: set `network_mode: host` for `librespot` and `shairport` -3. **Hardware video decoding** (optional): uncomment `/dev/dri` in `jellyfin` -4. **NMEA hardware**: uncomment `/dev/ttyUSB0` in `signalk` -5. Set `DEV=false` in `.env` +``` +SPOTIFY_NAME=Bordanlage +SPOTIFY_BITRATE=320 +MUSIC_PATH=./music +VITE_USE_MOCK=false # true = force mock data in dashboard, false = real APIs +``` -Run `make boot` instead of `make dev`. +--- + +## Dashboard API Configuration + +The dashboard reads hosts from environment at build time: + +| Variable | Default | Description | +|---|---|---| +| `VITE_SNAPCAST_HOST` | `localhost` | Snapserver hostname | +| `VITE_SIGNALK_HOST` | `localhost` | SignalK hostname | +| `VITE_MOPIDY_HOST` | `localhost` | Mopidy hostname | +| `VITE_JELLYFIN_HOST` | `localhost` | Jellyfin hostname | +| `VITE_USE_MOCK` | `false` | Force mock data (no real APIs) | + +--- + +## Architecture: Named Pipes + +In **boot mode**, audio flows through named pipes on a tmpfs volume: + +``` +spotify.pcm ← librespot (Spotify Connect) +airplay.pcm ← shairport (AirPlay 2) +mopidy.pcm ← mopidy (local files, radio) +``` + +Snapserver reads all three as streams and routes them to zones. + +Run `make pipes` once before `make boot` to create the pipes (or they are created automatically). + +In **dev mode**, pipes are not used (no audio hardware crosses the Docker VM boundary on Mac). + +--- + +## Migrating to the Boat + +Changes in `docker-compose.yml`: + +1. **Zone audio output** — replace `--player file:filename=null` with `--player alsa --soundcard hw:N,0`, uncomment `/dev/snd` device +2. **Spotify / AirPlay discovery** — set `network_mode: host` for `librespot` and `shairport` +3. **NMEA hardware** — uncomment the `devices` block under `signalk`, configure the connection at http://[boat-ip]:3000 +4. **Video decoding** (optional) — uncomment `/dev/dri` under `jellyfin` +5. Use `make boot` instead of `make dev` --- ## Troubleshooting -**Spotify device not showing up on Mac:** -- Ensure port 57621 (UDP+TCP) is accessible. Docker Desktop on Mac sometimes blocks UDP. -- Try connecting manually: Spotify → "Connect to a Device" → "Connect to [IP]" +**Zones stuck restarting:** +- Check `docker compose logs zone-salon`. With snapclient v0.35+, the server URI must be `tcp://snapserver` (not `--host snapserver`). +- If "No audio player support": snapclient needs `--player file:filename=null` in dev (null player not compiled in the bookworm .deb). -**AirPlay not visible on Windows:** -- The `avahi` container requires D-Bus. Run Docker Desktop with host networking or use WSL2. +**Dashboard crashes with exit 127 / npm not found:** +- The local `node:20-alpine` image was overwritten by the production dashboard build. Fix: `docker rmi node:20-alpine && make dev`. +- Root cause prevented by `dashboard/Dockerfile.dev` — dev uses its own build context. -**Snapcast zones show as offline:** -- Audio pipes must exist before snapserver starts. Run `make pipes` or `bash scripts/init-pipes.sh` +**Shairport config syntax error:** +- Boolean values in `config/shairport.conf` must be quoted: `"yes"` not `yes`. -**Mopidy won't start:** -- Check `docker compose logs mopidy`. The custom Dockerfile installs plugins on first build; rebuild with `make rebuild` +**Spotify device not visible:** +- On Mac, Docker Desktop bridges UDP poorly. Use `make spotify` (native librespot on Mac) for dev. +- On boat: use `network_mode: host` in `docker-compose.yml`. -**Dashboard shows "No signal":** -- In dev mode this is normal until mock data initializes (1–2 seconds) -- In production: check that SignalK is running and the WebSocket URL is correct in `.env` +**Librespot production build fails:** +- `cargo install librespot` (latest = v0.8.0) has a vergen_lib dependency conflict. +- Fixed by pinning to `--version "=0.5.0"` in `docker/librespot/Dockerfile`. -**Port conflicts:** -- Edit the port mappings in `docker-compose.yml` or `docker-compose.dev.yml` +**Snapcast arm64 .deb not found:** +- v0.27.0 has no arm64 release. Use v0.35.0 which ships `arm64_bookworm.deb`. + +**Port conflicts (e.g. 8080 taken by Supabase):** +- Dev dashboard is on **8090**, production on 8080. Change port mappings in `docker-compose.dev.yml` as needed. + +--- + +## Project Layout + +``` +. +├── Makefile +├── docker-compose.yml # production stack +├── docker-compose.dev.yml # dev overrides (Vite HMR, null zones, SignalK demo) +├── config/ +│ ├── snapserver.conf # pipe sources, HTTP API config +│ ├── mopidy.conf # music backends, HTTP server +│ └── shairport.conf # AirPlay name, pipe output +├── docker/ +│ ├── snapserver/Dockerfile # snapcast v0.35.0 from GitHub releases +│ ├── snapclient/Dockerfile # snapcast v0.35.0 from GitHub releases +│ ├── mopidy/Dockerfile # mopidy + iris + local + tunein +│ └── librespot/ +│ ├── Dockerfile # production: cargo install librespot v0.5.0 +│ └── Dockerfile.dev # dev stub: alpine + sleep +├── dashboard/ +│ ├── Dockerfile # production: Vite build → nginx +│ ├── Dockerfile.dev # dev: node:20-alpine base (avoid image overwrite) +│ ├── nginx.conf +│ └── src/ +│ ├── api/ # real API clients (snapcast, signalk, mopidy, jellyfin) +│ ├── mock/ # mock data (VITE_USE_MOCK=true) +│ ├── hooks/ # useNMEA, useZones, usePlayer, useDocker +│ └── components/ +│ ├── layout/ # TopBar, TabNav +│ ├── audio/ # ZoneCard, ZoneGrid, NowPlaying, SourcePicker +│ └── nav/ # InstrumentPanel, ChartPlaceholder +├── scripts/ +│ ├── init-pipes.sh # create named pipes in /tmp/audio +│ ├── setup-dev.sh +│ └── setup-boot.sh +└── music/ # drop audio files here (Mopidy + Jellyfin) +``` diff --git a/config/shairport.conf b/config/shairport.conf index 5b7ba5e..2b2dea8 100644 --- a/config/shairport.conf +++ b/config/shairport.conf @@ -10,6 +10,6 @@ pipe = { }; metadata = { - enabled = yes; - include_cover_art = yes; + enabled = "yes"; + include_cover_art = "yes"; }; diff --git a/dashboard/Dockerfile.dev b/dashboard/Dockerfile.dev new file mode 100644 index 0000000..f0e004a --- /dev/null +++ b/dashboard/Dockerfile.dev @@ -0,0 +1 @@ +FROM node:20-alpine diff --git a/dashboard/src/components/layout/TopBar.jsx b/dashboard/src/components/layout/TopBar.jsx index ce96224..c5a0cc8 100644 --- a/dashboard/src/components/layout/TopBar.jsx +++ b/dashboard/src/components/layout/TopBar.jsx @@ -1,7 +1,8 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useNMEA } from '../../hooks/useNMEA.js' -const isDev = import.meta.env.DEV +const isMock = import.meta.env.VITE_USE_MOCK === 'true' +const isDev = import.meta.env.DEV function formatTime() { return new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) @@ -12,16 +13,17 @@ export default function TopBar() { const [time, setTime] = useState(formatTime()) // Clock tick - useState(() => { + useEffect(() => { const t = setInterval(() => setTime(formatTime()), 5000) return () => clearInterval(t) - }) + }, []) return (
⚓ Bordanlage - {isDev && DEV · MOCK DATA} + {isMock && DEV · MOCK DATA} + {isDev && !isMock && DEV · LIVE}
diff --git a/dashboard/src/hooks/useDocker.js b/dashboard/src/hooks/useDocker.js index a0c1000..2b8ea9a 100644 --- a/dashboard/src/hooks/useDocker.js +++ b/dashboard/src/hooks/useDocker.js @@ -1,17 +1,22 @@ import { useState, useEffect } from 'react' const SERVICES = [ - { id: 'signalk', name: 'SignalK', url: 'http://localhost:3000/signalk' }, - { id: 'snapserver', name: 'Snapcast', url: 'http://localhost:1780' }, - { id: 'mopidy', name: 'Mopidy', url: 'http://localhost:6680' }, - { id: 'jellyfin', name: 'Jellyfin', url: 'http://localhost:8096' }, - { id: 'portainer', name: 'Portainer', url: 'http://localhost:9000' }, + { id: 'signalk', name: 'SignalK', host: import.meta.env.VITE_SIGNALK_HOST || 'localhost', port: 3000, path: '/signalk' }, + { id: 'snapserver', name: 'Snapcast', host: import.meta.env.VITE_SNAPCAST_HOST || 'localhost', port: 1780, path: '/' }, + { id: 'mopidy', name: 'Mopidy', host: import.meta.env.VITE_MOPIDY_HOST || 'localhost', port: 6680, path: '/' }, + { id: 'jellyfin', name: 'Jellyfin', host: import.meta.env.VITE_JELLYFIN_HOST || 'localhost', port: 8096, path: '/' }, + { id: 'portainer', name: 'Portainer', host: import.meta.env.VITE_PORTAINER_HOST || 'localhost', port: 9000, path: '/' }, ] -async function ping(url) { +async function ping(host, port, path) { try { - const res = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(2000) }) - return res.ok || res.status < 500 + // mode: 'no-cors' bypasses CORS blocks; any response (opaque) = server is up + await fetch(`http://${host}:${port}${path}`, { + method: 'GET', + mode: 'no-cors', + signal: AbortSignal.timeout(3000), + }) + return true } catch { return false } @@ -19,21 +24,24 @@ async function ping(url) { export function useDocker() { const [services, setServices] = useState( - SERVICES.map(s => ({ ...s, status: 'unknown' })) + SERVICES.map(s => ({ ...s, url: `http://${s.host}:${s.port}`, status: 'unknown' })) ) async function checkAll() { - const results = await Promise.all(SERVICES.map(async s => ({ - ...s, - status: await ping(s.url) ? 'online' : 'offline', - }))) + const results = await Promise.all( + SERVICES.map(async s => ({ + ...s, + url: `http://${s.host}:${s.port}`, + status: await ping(s.host, s.port, s.path) ? 'online' : 'offline', + })) + ) setServices(results) } useEffect(() => { checkAll() - const timer = setInterval(checkAll, 30000) - return () => clearInterval(timer) + const t = setInterval(checkAll, 30000) + return () => clearInterval(t) }, []) return { services, refresh: checkAll } diff --git a/dashboard/src/mock/index.js b/dashboard/src/mock/index.js index f7ce529..83e49c5 100644 --- a/dashboard/src/mock/index.js +++ b/dashboard/src/mock/index.js @@ -1,4 +1,4 @@ -// Mock router: returns real API clients in production, mock implementations in dev. +// API router – uses real services by default; set VITE_USE_MOCK=true to force mocks. import { createSignalKMock } from './signalk.mock.js' import { createSnapcastMock } from './snapcast.mock.js' import { createMopidyMock } from './mopidy.mock.js' @@ -6,10 +6,10 @@ import { createSignalKClient } from '../api/signalk.js' import { createSnapcastClient } from '../api/snapcast.js' import { createMopidyClient } from '../api/mopidy.js' -const isDev = import.meta.env.DEV +const forceMock = import.meta.env.VITE_USE_MOCK === 'true' export function createApi() { - if (isDev) { + if (forceMock) { return { signalk: createSignalKMock(), snapcast: createSnapcastMock(), @@ -30,7 +30,6 @@ export function createApi() { } } -// Singleton – one API instance for the whole app let _api = null export function getApi() { if (!_api) _api = createApi() diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2898093..3eb3cda 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,7 @@ -# Development override – run without any hardware. +# Development override – real services, no hardware needed. # Usage: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d +# Mac audio output: make mac-audio +# Spotify Connect: make spotify services: @@ -7,43 +9,36 @@ services: environment: - SIGNALK_DEMO=true # Built-in demo NMEA data generator - # Librespot disabled in dev – dashboard uses mock Spotify data + # Librespot: stub in dev (pipe backend doesn't cross VM boundary). + # For real Spotify on Mac run: make spotify librespot: - image: alpine:latest + build: + context: ./docker/librespot + dockerfile: Dockerfile.dev entrypoint: [] - command: ["sh", "-c", "echo 'librespot: dev mode, mock active' && sleep infinity"] + command: ["sh", "-c", "echo 'librespot stub: run make spotify for Mac audio' && sleep infinity"] restart: "no" volumes: [] ports: [] - # Zones disabled in dev – dashboard uses Snapcast mock + # Zones: real snapclient containers with null player (v0.35+, URI format) zone-salon: - image: alpine:latest - entrypoint: [] - command: ["sleep", "infinity"] - restart: "no" + command: ["--hostID", "zone-salon", "--player", "file:filename=null", "tcp://snapserver"] zone-cockpit: - image: alpine:latest - entrypoint: [] - command: ["sleep", "infinity"] - restart: "no" + command: ["--hostID", "zone-cockpit", "--player", "file:filename=null", "tcp://snapserver"] zone-bug: - image: alpine:latest - entrypoint: [] - command: ["sleep", "infinity"] - restart: "no" + command: ["--hostID", "zone-bug", "--player", "file:filename=null", "tcp://snapserver"] zone-heck: - image: alpine:latest - entrypoint: [] - command: ["sleep", "infinity"] - restart: "no" + command: ["--hostID", "zone-heck", "--player", "file:filename=null", "tcp://snapserver"] - # Vite dev server with HMR instead of built nginx image + # Vite dev server with HMR — uses Dockerfile.dev to avoid overwriting node:20-alpine dashboard: - image: node:20-alpine + build: + context: ./dashboard + dockerfile: Dockerfile.dev entrypoint: [] working_dir: /app volumes: @@ -57,3 +52,4 @@ services: - VITE_SIGNALK_HOST=localhost - VITE_MOPIDY_HOST=localhost - VITE_JELLYFIN_HOST=localhost + - VITE_USE_MOCK=false # Use real APIs; set to true to force mock data diff --git a/docker-compose.yml b/docker-compose.yml index f5c3d58..16b9ac0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -129,10 +129,10 @@ services: restart: unless-stopped depends_on: - snapserver - # On boat: add --soundcard hw:0,0 and device /dev/snd + # On boat: add --player alsa --soundcard hw:0,0 and device /dev/snd # devices: # - /dev/snd:/dev/snd - command: ["snapclient", "--host", "snapserver", "--hostID", "zone-salon", "--player", "null"] + command: ["--hostID", "zone-salon", "--player", "file:filename=null", "tcp://snapserver"] networks: - bordanlage @@ -141,7 +141,7 @@ services: restart: unless-stopped depends_on: - snapserver - command: ["snapclient", "--host", "snapserver", "--hostID", "zone-cockpit", "--player", "null"] + command: ["--hostID", "zone-cockpit", "--player", "file:filename=null", "tcp://snapserver"] networks: - bordanlage @@ -150,7 +150,7 @@ services: restart: unless-stopped depends_on: - snapserver - command: ["snapclient", "--host", "snapserver", "--hostID", "zone-bug", "--player", "null"] + command: ["--hostID", "zone-bug", "--player", "file:filename=null", "tcp://snapserver"] networks: - bordanlage @@ -159,7 +159,7 @@ services: restart: unless-stopped depends_on: - snapserver - command: ["snapclient", "--host", "snapserver", "--hostID", "zone-heck", "--player", "null"] + command: ["--hostID", "zone-heck", "--player", "file:filename=null", "tcp://snapserver"] networks: - bordanlage diff --git a/docker/librespot/Dockerfile b/docker/librespot/Dockerfile index 2739460..b8766ca 100644 --- a/docker/librespot/Dockerfile +++ b/docker/librespot/Dockerfile @@ -1,9 +1,10 @@ # Stage 1: Build librespot from source +# Pin to v0.5.0 — v0.8.0 has a vergen_lib dependency conflict FROM rust:slim-bookworm AS builder RUN apt-get update \ && apt-get install -y --no-install-recommends pkg-config libssl-dev \ && rm -rf /var/lib/apt/lists/* -RUN cargo install librespot +RUN cargo install librespot --version "=0.5.0" # Stage 2: Minimal runtime image FROM debian:bookworm-slim diff --git a/docker/librespot/Dockerfile.dev b/docker/librespot/Dockerfile.dev new file mode 100644 index 0000000..4bfc281 --- /dev/null +++ b/docker/librespot/Dockerfile.dev @@ -0,0 +1,2 @@ +FROM alpine:latest +CMD ["sh", "-c", "echo 'librespot stub: run make spotify for Mac audio' && sleep infinity"] diff --git a/docker/snapclient/Dockerfile b/docker/snapclient/Dockerfile index bed1168..d0cb1c4 100644 --- a/docker/snapclient/Dockerfile +++ b/docker/snapclient/Dockerfile @@ -1,5 +1,13 @@ FROM debian:bookworm-slim +ARG VERSION=0.35.0 + RUN apt-get update \ - && apt-get install -y --no-install-recommends snapclient \ + && apt-get install -y --no-install-recommends ca-certificates wget \ + && ARCH=$(dpkg --print-architecture) \ + && wget -q "https://github.com/badaix/snapcast/releases/download/v${VERSION}/snapclient_${VERSION}-1_${ARCH}_bookworm.deb" \ + -O /tmp/snapclient.deb \ + && dpkg -i /tmp/snapclient.deb || apt-get install -fy \ + && rm /tmp/snapclient.deb \ && rm -rf /var/lib/apt/lists/* + ENTRYPOINT ["snapclient"] diff --git a/docker/snapserver/Dockerfile b/docker/snapserver/Dockerfile index 9695008..2559646 100644 --- a/docker/snapserver/Dockerfile +++ b/docker/snapserver/Dockerfile @@ -1,6 +1,14 @@ FROM debian:bookworm-slim +ARG VERSION=0.35.0 + RUN apt-get update \ - && apt-get install -y --no-install-recommends snapserver \ + && apt-get install -y --no-install-recommends ca-certificates wget \ + && ARCH=$(dpkg --print-architecture) \ + && wget -q "https://github.com/badaix/snapcast/releases/download/v${VERSION}/snapserver_${VERSION}-1_${ARCH}_bookworm.deb" \ + -O /tmp/snapserver.deb \ + && dpkg -i /tmp/snapserver.deb || apt-get install -fy \ + && rm /tmp/snapserver.deb \ && rm -rf /var/lib/apt/lists/* + EXPOSE 1704 1705 1780 CMD ["snapserver"]