From 60c2e144f04ad31bab469704df0c1149e409f128 Mon Sep 17 00:00:00 2001 From: Paul Robertson Date: Tue, 29 Sep 2026 14:17:14 +1300 Subject: [PATCH] fix(sendspin): record a declared aiosendspin override so MA controller state parses sendspin 7.5.0 declares aiosendspin[server]~=6.0.1, i.e. >=6.0.1,<6.1.0, but aiosendspin 6.1.0 added seek/seek_relative to MediaCommand and servers now advertise them (Music Assistant 2.10.x, Loxone/Sonn Core 4.0.0-beta.21). Since supported_commands is typed list[MediaCommand], one unknown member discards the whole server/state message: mashumaro.exceptions.InvalidFieldValue: Field "supported_commands" ... Audio keeps playing, so the client looks alive while controller state (volume/mute/repeat/shuffle) silently stops syncing. Reported upstream as Sendspin-Protocol/sendspin#280 and #278; no fix released. This does not fix upstream, it keeps the moOde install correct meanwhile. The installer now records a declared --overrides entry (aiosendspin>=6.1.1,<6.2) in the uv tool receipt. Unlike a hand-applied uv pip install, the recorded override is part of the resolution and survives uv tool upgrade, which would otherwise re-resolve back to 6.0.5. The bound is narrow on purpose: >=6.1.1 alone resolves to 9.1.1, and upstream PR #276 shows 9.1 needed code migration, so 6.1.x is the verified-safe range. Remove once upstream relaxes the pin. Verified by running the new functions verbatim on the Pi: 6.0.5 -> 6.1.1, second call a no-op, override present in the receipt, 0 deserialisation exceptions with a live Music Assistant stream. Installer bumped to v4.1.6. --- README-sendspin.md | 23 ++++++++++++++- moode-sendspin-installer.sh | 57 +++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/README-sendspin.md b/README-sendspin.md index 9ac235133..01a123e45 100644 --- a/README-sendspin.md +++ b/README-sendspin.md @@ -32,7 +32,7 @@ The installer automatically installs Python 3, `uv` (Python package manager), an ## Installer -**`moode-sendspin-installer.sh`** — Full-featured installer with backup, uninstall, 19-component detection, and all features. **Current version: v4.1.5** (moOde 10.3.4 / r1034 support; idempotent re-runs — safe to run repeatedly, no duplicate DB rows; partial installations detected and repaired automatically; boot-time auto-start honors the UI toggle). +**`moode-sendspin-installer.sh`** — Full-featured installer with backup, uninstall, 19-component detection, and all features. **Current version: v4.1.6** (moOde 10.3.4 / r1034 support; idempotent re-runs — safe to run repeatedly, no duplicate DB rows; partial installations detected and repaired automatically; boot-time auto-start honors the UI toggle). ### Installation @@ -171,6 +171,27 @@ sudo reboot The moOde worker is a long-running PHP daemon started from `/etc/rc.local` that loads `renderer.php` **only at startup**. Until it restarts, the SendSpin service controls and the Resume MPD feature are inert even though every file is correctly installed — the installer patches the files, not the running process. moOde's own updater behaves the same way ("Update installed, restart required"). A reboot also restarts PHP-FPM, so it replaces the PHP restart step entirely. +### Known upstream issue: aiosendspin pinned below 6.1 + +`sendspin` 7.5.0 declares `aiosendspin[server]~=6.0.1`, which PEP 440 resolves to `>=6.0.1,<6.1.0`. `aiosendspin` 6.1.0 added `seek` and `seek_relative` to `MediaCommand`, and servers now advertise them — **Music Assistant 2.10.x** sends `seek_relative` in `supported_commands`, as does Loxone/Sonn Core 4.0.0-beta.21. On 6.0.x that value cannot be parsed, and because `supported_commands` is typed `list[MediaCommand]` the **entire `server/state` message is discarded**: + +``` +ERROR:aiosendspin.client.client:Failed to parse server message: {"type":"server/state",...} +mashumaro.exceptions.InvalidFieldValue: Field "supported_commands" of type list[MediaCommand] has invalid value [...] +``` + +Audio keeps playing (a different message), so the client still looks healthy while controller state — volume, mute, repeat, shuffle — silently stops syncing. Streams with no duration are unaffected, which makes it look source-specific rather than protocol-specific. + +Tracked upstream as **Sendspin-Protocol/sendspin#280** and **#278**; no fix released yet. + +**What the installer does:** it records an `--overrides` entry for `aiosendspin>=6.1.1,<6.2` in the uv tool receipt. This is a *declared* override, not a hand-patched environment, so it also survives `uv tool upgrade` (which otherwise re-resolves straight back to 6.0.x). Check it with: + +```bash +grep overrides /root/.local/share/uv/tools/sendspin/uv-receipt.toml +``` + +The upper bound is deliberately narrow: `aiosendspin` 9.x required code migration in `sendspin` (see upstream PR #276), so 6.1.x — the additive delta verified in #280 — is the safe range. Drop the override once upstream relaxes the pin. + ## Uninstall ```bash diff --git a/moode-sendspin-installer.sh b/moode-sendspin-installer.sh index 48db61695..ce83caa10 100644 --- a/moode-sendspin-installer.sh +++ b/moode-sendspin-installer.sh @@ -19,11 +19,15 @@ # CONFIGURATION # ============================================================================ -SCRIPT_VERSION="4.1.5" +SCRIPT_VERSION="4.1.6" REPO_OWNER="kiwipaulrob" REPO_NAME="moode" BRANCH="sendspin-advanced" SENDSPIN_VERSION="7.5.0" +# aiosendspin workaround — see ensure_aiosendspin_override() and README (upstream sendspin#280) +AIOSENDSPIN_OVERRIDE_SPEC=">=6.1.1,<6.2" +AIOSENDSPIN_MIN_VERSION="6.1.1" +AIOSENDSPIN_OVERRIDE_FILE="/etc/moode-sendspin-aiosendspin.overrides.txt" BASE_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}" # File locations on moOde @@ -284,6 +288,13 @@ detect_header_php_meta() { [[ -f "${WWW_DIR}/header.php" ]] && grep -q "sendspin-display.js" "${WWW_DIR}/header.php" } +detect_aiosendspin_override() { + local sp cur + sp=$(ls -d /root/.local/share/uv/tools/sendspin/lib/python*/site-packages 2>/dev/null | head -1) + cur=$(ls "$sp" 2>/dev/null | sed -n 's/^aiosendspin-\([0-9][0-9.]*\)\.dist-info$/\1/p' | head -1) + [[ -n "$cur" ]] && [[ "$(printf '%s\n%s\n' "$AIOSENDSPIN_MIN_VERSION" "$cur" | sort -V | head -1)" == "$AIOSENDSPIN_MIN_VERSION" ]] +} + # ============================================================================ # CHECK / STATUS FUNCTION # ============================================================================ @@ -346,6 +357,45 @@ check_installation() { # PREREQUISITES (Python, uv, sendspin CLI) # ============================================================================ +# Apply the aiosendspin override (upstream Sendspin-Protocol/sendspin#280). +# sendspin 7.5.0 declares aiosendspin[server]~=6.0.1, i.e. >=6.0.1,<6.1.0. +# aiosendspin 6.1.0 added seek/seek_relative to MediaCommand, and servers now +# advertise them (Music Assistant 2.10.x sends seek_relative). On 6.0.x the +# value cannot be parsed and, because supported_commands is typed +# list[MediaCommand], the WHOLE server/state message is discarded — so +# controller state (volume/mute/repeat/shuffle) silently stops syncing while +# audio keeps playing and the client still looks alive. +# +# The override is recorded in the uv tool receipt, which makes it a declared +# part of the resolution rather than a hand-patched environment — so it also +# survives `uv tool upgrade` (which would otherwise re-resolve back to 6.0.x). +# Remove it once upstream relaxes the pin. +ensure_aiosendspin_override() { + local sp cur new + sp=$(ls -d /root/.local/share/uv/tools/sendspin/lib/python*/site-packages 2>/dev/null | head -1) + cur=$(ls "$sp" 2>/dev/null | sed -n 's/^aiosendspin-\([0-9][0-9.]*\)\.dist-info$/\1/p' | head -1) + + if detect_aiosendspin_override; then + log_info " aiosendspin ${cur} satisfies the override (${AIOSENDSPIN_OVERRIDE_SPEC})" + return 0 + fi + + log_info " Applying aiosendspin override ${AIOSENDSPIN_OVERRIDE_SPEC} (upstream sendspin#280)" + printf 'aiosendspin%s\n' "$AIOSENDSPIN_OVERRIDE_SPEC" > "$AIOSENDSPIN_OVERRIDE_FILE" || { + log_warn " Could not write ${AIOSENDSPIN_OVERRIDE_FILE}" + return 1 + } + + if uv tool install "sendspin==${SENDSPIN_VERSION}" --overrides "$AIOSENDSPIN_OVERRIDE_FILE" -q; then + new=$(ls "$sp" 2>/dev/null | sed -n 's/^aiosendspin-\([0-9][0-9.]*\)\.dist-info$/\1/p' | head -1) + log_success " aiosendspin override applied (${cur:-none} -> ${new:-unknown})" + return 0 + fi + + log_warn " Could not apply the aiosendspin override" + return 1 +} + install_prerequisites() { log_info "Checking and installing prerequisites..." @@ -387,7 +437,9 @@ install_prerequisites() { log_info " sendspin CLI already installed (${installed_version})" fi fi - + + ensure_aiosendspin_override || log_warn " aiosendspin override not applied - Music Assistant controller state may not sync" + # Tune PHP-FPM pool for better responsiveness with SendSpin metadata polling local php_ver php_ver=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null) @@ -2070,6 +2122,7 @@ run_installation() { detect_sendspin_meta_php || { log_error "sendspin-meta.php verification failed"; verify_passed=false; } detect_sendspin_display_js || { log_error "sendspin-display.js verification failed"; verify_passed=false; } detect_header_php_meta || { log_warn "header.php JS include may be missing"; } + detect_aiosendspin_override || { log_warn "aiosendspin override missing - MA controller state may not sync (upstream sendspin#280)"; } detect_sendspin_metadata_sink || { log_warn "sendspin-metadata-sink.py not deployed (HA metadata requires it)"; } detect_sendspin_metadata_sink_service || { log_warn "sendspin-metadata-sink.service not installed"; } fi