From 833c77a22bc62af8c65a467a25e101f5befac923 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Sat, 3 Jan 2026 16:04:23 +0100 Subject: [PATCH 01/11] add experimental Sendspin CLI renderer --- README-SENDSPIN.md | 41 +++++++++++++++++++++++++++ etc/systemd/system/sendspin.service | 13 +++++++++ usr/local/bin/moodeutl | 28 +++++++++++++----- var/local/www/db/moode-sqlite3.db.sql | 2 ++ www/daemon/worker.php | 37 +++++++++++++++++++++++- www/inc/constants.php | 5 ++-- www/inc/renderer.php | 13 ++++++++- www/js/playerlib.js | 5 ++-- www/ren-config.php | 19 +++++++++++++ www/templates/ren-config.html | 22 ++++++++++++++ www/util/renderer-onoff.php | 16 ++++++++++- www/util/restart-renderer.php | 15 ++++++++-- www/util/sysinfo.sh | 9 ++++++ 13 files changed, 209 insertions(+), 16 deletions(-) create mode 100644 README-SENDSPIN.md create mode 100644 etc/systemd/system/sendspin.service diff --git a/README-SENDSPIN.md b/README-SENDSPIN.md new file mode 100644 index 000000000..ecca49885 --- /dev/null +++ b/README-SENDSPIN.md @@ -0,0 +1,41 @@ +# Experiment: moOde + Sendspin + +Experiment: extending moOde with a renderer for Sendspin. + +Start a Sendspin server; e.g. + +```bash +sudo docker run -v /home/your-login-name/music-assistant-data:/data --network host --cap-add=DAC_READ_SEARCH --cap-add=SYS_ADMIN --security-opt apparmor:unconfined ghcr.io/music-assistant/server:beta +``` + +Install [sendspin-cli](https://github.com/Sendspin/sendspin-cli) in moOde; e.g. + +```bash +sudo pip install uv --break-system-packages +sudo uv tool install sendspin +``` + +Test via SSH if the Sendspin server can be reached; and desired audio device is available: + +```bash +sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-servers +sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-audio-devices +``` + +Change the audio device by modifying `/etc/systemd/system/sendspin.service` : + +```service +[Unit] +Description=Sendspin CLI +After=network-online.target +Requires=network-online.target + +[Service] +Type=simple +ExecStart=/root/.local/share/uv/tools/sendspin/bin/sendspin --audio-device snd_rpi_hifiberry_dacplus --headless +Restart=on-failure +User=root + +[Install] +WantedBy=multi-user.target +``` diff --git a/etc/systemd/system/sendspin.service b/etc/systemd/system/sendspin.service new file mode 100644 index 000000000..32d32722e --- /dev/null +++ b/etc/systemd/system/sendspin.service @@ -0,0 +1,13 @@ +[Unit] +Description=Sendspin CLI +After=network-online.target +Requires=network-online.target + +[Service] +Type=simple +ExecStart=/root/.local/share/uv/tools/sendspin/bin/sendspin --headless +Restart=on-failure +User=root + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/usr/local/bin/moodeutl b/usr/local/bin/moodeutl index 98f50a552..8a0d1cdd7 100755 --- a/usr/local/bin/moodeutl +++ b/usr/local/bin/moodeutl @@ -36,7 +36,8 @@ $features = array( FEAT_BLUETOOTH => 'Bluetooth renderer', FEAT_DEVTWEAKS => 'Developer tweaks', FEAT_MULTIROOM => 'Multiroom audio', - FEAT_PEPPYDISPLAY => 'PeppyMeter display' + FEAT_PEPPYDISPLAY => 'PeppyMeter display', + FEAT_SENDSPIN => 'Sendspin renderer' ); $featBitmask = trim(shell_exec('sqlite3 ' . SQLDB_PATH . " \"SELECT value FROM cfg_system WHERE param='feat_bitmask'\"")); @@ -160,7 +161,7 @@ switch ($option) { echo VERSION . "\n"; break; case '--help': - //[--bluetooth | --airplay | --spotify | --deezer | --upnp | --squeezelite | --plexamp | --roonbridge] + //[--bluetooth | --airplay | --spotify | --deezer | --upnp | --squeezelite | --plexamp | --roonbridge | --sendspin] $btArg = $featBitmask & FEAT_BLUETOOTH ? '--bluetooth | ' : ''; $apArg = $featBitmask & FEAT_AIRPLAY ? '--airplay | ' : ''; $spArg = $featBitmask & FEAT_SPOTIFY ? '--spotify | ' : ''; @@ -169,7 +170,8 @@ switch ($option) { $slArg = $featBitmask & FEAT_SQUEEZELITE ? '--squeezelite | ' : ''; $paArg = $featBitmask & FEAT_PLEXAMP ? '--plexamp | ' : ''; $rbArg = $featBitmask & FEAT_ROONBRIDGE ? '--roonbridge | ' : ''; - $rendererList = rtrim($btArg . $apArg . $spArg . $dzArg . $upArg . $slArg . $paArg . $rbArg, ' | '); + $ssArg = $featBitmask & FEAT_SENDSPIN ? '--sendspin | ' : ''; + $rendererList = rtrim($btArg . $apArg . $spArg . $dzArg . $upArg . $slArg . $paArg . $rbArg . $ssArg, ' | '); echo "Usage: moodeutl [OPTION] Moode utility functions @@ -545,12 +547,24 @@ function stopAllRenderers($featBitmask) { } else { echo "- roonbridge\t\tfeature disabled\n"; } + + if ($featBitmask & FEAT_SENDSPIN) { + $sendspinSvc = trim(shell_exec('sqlite3 ' . SQLDB_PATH . " \"SELECT value FROM cfg_system WHERE param='sendspinsvc'\"")); + if ($sendspinSvc == '1') { + sysCmd('/var/www/util/restart-renderer.php --sendspin --stop'); + echo "- sendspin\t\tstopped\n"; + } else { + echo "- sendspin\t\tnot on\n"; + } + } else { + echo "- sendspin\t\tfeature disabled\n"; + } } function restartRenderer($argv) { $renderers = array('--bluetooth' => 'btsvc', '--airplay' => 'airplaysvc', '--spotify' => 'spotifysvc', '--deezer' => 'deezersvc', '--upnp' => 'upnpsvc', '--squeezelite' => 'slsvc', '--plexamp' => 'pasvc', - '--roonbridge' => 'rbsvc'); + '--roonbridge' => 'rbsvc', '--sendspin' => 'sendspinsvc'); if (!isset($argv[2])) { echo 'Missing 2nd argument [renderer name]' . "\n"; @@ -564,7 +578,7 @@ function restartRenderer($argv) { } } else { echo 'Invalid renderer name' . "\n"; - echo 'Valid names are: --bluetooth, --airplay, --spotify, --deezer, --upnp, --squeezelite, --plexamp, --roonbridge' . "\n"; + echo 'Valid names are: --bluetooth, --airplay, --spotify, --deezer, --upnp, --squeezelite, --plexamp, --roonbridge, --sendspin' . "\n"; return; } @@ -575,14 +589,14 @@ function restartRenderer($argv) { function rendererOnoff($argv) { $renderers = array('--bluetooth' => 'btsvc', '--airplay' => 'airplaysvc', '--spotify' => 'spotifysvc', '--deezer' => 'deezersvc', '--upnp' => 'upnpsvc', '--squeezelite' => 'slsvc', '--plexamp' => 'pasvc', - '--roonbridge' => 'rbsvc'); + '--roonbridge' => 'rbsvc', '--sendspin' => 'sendspinsvc'); if (!isset($argv[2])) { echo 'Missing 2nd argument [renderer name]' . "\n"; return; } else if (!array_key_exists($argv[2], $renderers)) { echo 'Invalid renderer name' . "\n"; - echo 'Valid names are: --bluetooth, --airplay, --spotify, --deezer, --upnp, --squeezelite, --plexamp, --roonbridge' . "\n"; + echo 'Valid names are: --bluetooth, --airplay, --spotify, --deezer, --upnp, --squeezelite, --plexamp, --roonbridge, --sendspin' . "\n"; return; } else if (!isset($argv[3])) { echo 'Missing 3nd argument [on|off]' . "\n"; diff --git a/var/local/www/db/moode-sqlite3.db.sql b/var/local/www/db/moode-sqlite3.db.sql index 14e4410ce..21d1c637b 100644 --- a/var/local/www/db/moode-sqlite3.db.sql +++ b/var/local/www/db/moode-sqlite3.db.sql @@ -706,6 +706,8 @@ INSERT INTO cfg_system (id, param, value) VALUES (172, 'library_onetouch_pl', 'S INSERT INTO cfg_system (id, param, value) VALUES (173, 'scnsaver_mode', 'Cover art'); INSERT INTO cfg_system (id, param, value) VALUES (174, 'scnsaver_layout', 'Default'); INSERT INTO cfg_system (id, param, value) VALUES (175, 'scnsaver_xmeta', 'Yes'); +INSERT INTO cfg_system (id, param, value) VALUES (176, 'sendspinsvc', '0'); +INSERT INTO cfg_system (id, param, value) VALUES (177, 'sendspin_installed', 'no'); -- Table: cfg_theme CREATE TABLE cfg_theme (id INTEGER PRIMARY KEY, theme_name CHAR (32), tx_color CHAR (32), bg_color CHAR (32), mbg_color CHAR (32)); diff --git a/www/daemon/worker.php b/www/daemon/worker.php index 8718b78c5..f29cca44b 100755 --- a/www/daemon/worker.php +++ b/www/daemon/worker.php @@ -604,6 +604,20 @@ } workerLog('worker: RoonBridge: ' . $msg); +// Sendspin +// To install +// - sudo pip install uv --break-system-packages +// - sudo uv tool install sendspin +if (file_exists('/root/.local/share/uv/tools/sendspin/bin/sendspin') === true) { + $msg = 'installed'; + $_SESSION['sendspin_installed'] = 'yes'; + +} else { + $_SESSION['sendspin_installed'] = 'no'; + $msg = 'not installed'; +} +workerLog('worker: Sendspin: ' . $msg); + // Allo Boss 2 // OLED: The Allo installer adds lines to rc.local which are not needed because we start/stop it via systemd unit if (!empty(sysCmd('grep "boss2" /etc/rc.local')[0])) { @@ -1175,6 +1189,22 @@ } workerLog('worker: RoonBridge: ' . $status); +// Start Sendspin renderer +if ($_SESSION['feat_bitmask'] & FEAT_SENDSPIN) { + if (isset($_SESSION['sendspinsvc']) && $_SESSION['sendspinsvc'] == 1) { + $status = 'started'; + startSendspin(); + } else { + $status = 'available'; + } + } else { + $status = 'not installed'; + } +} else { + $status = 'n/a'; +} +workerLog('worker: Sendspin: ' . $status); + // Start Multiroom audio if ($_SESSION['feat_bitmask'] & FEAT_MULTIROOM) { // Sender @@ -3197,7 +3227,12 @@ function runQueuedJob() { } } break; - + case 'sendspinsvc': + stopSendspin(); + if ($_SESSION['sendspinsvc'] == 1) { + startSendspin(); + } + break; case 'multiroom_tx': if ($_SESSION['multiroom_tx'] == 'On') { // Reconfigure to Dummy sound driver diff --git a/www/inc/constants.php b/www/inc/constants.php index 8f4086284..b40d51242 100755 --- a/www/inc/constants.php +++ b/www/inc/constants.php @@ -202,8 +202,9 @@ const FEAT_DEVTWEAKS = 32768; // Developer tweaks const FEAT_MULTIROOM = 65536; // y Multiroom audio const FEAT_PEPPYDISPLAY = 131072; // y Peppy display -// ------- -// 228279 +const FEAT_SENDSPIN = 262144; // y Sendspin renderer +// ------ +// 524287 // Selective resampling bitmask const SOX_UPSAMPLE_ALL = 3; // Upsample if source < target rate diff --git a/www/inc/renderer.php b/www/inc/renderer.php index 83ad6383e..f490f9181 100644 --- a/www/inc/renderer.php +++ b/www/inc/renderer.php @@ -369,6 +369,16 @@ function stopRoonBridge() { sendFECmd('rbactive0'); } +// Sendspin +function startSendspin() { + sysCmd('mpc stop'); + sysCmd('systemctl start sendspin'); +} + +function stopSendspin() { + sysCmd('systemctl stop sendspin'); +} + // Stop all renderers function stopAllRenderers() { $renderers = array( @@ -379,7 +389,8 @@ function stopAllRenderers() { 'upnpsvc' => 'stopUPnP', 'slsvc' => 'stopSqueezeLite', 'pasvc' => 'stopPlexamp', - 'rbsvc' => 'stopRoonBridge' + 'rbsvc' => 'stopRoonBridge', + 'sendspinsvc' => 'stopSendspin' ); // Watchdog (so monitored renderers are not auto restarted) diff --git a/www/js/playerlib.js b/www/js/playerlib.js index 43dd4b69d..8505bb24b 100755 --- a/www/js/playerlib.js +++ b/www/js/playerlib.js @@ -23,8 +23,9 @@ const FEAT_BLUETOOTH = 16384; // y Bluetooth renderer const FEAT_DEVTWEAKS = 32768; // Developer tweaks const FEAT_MULTIROOM = 65536; // y Multiroom audio const FEAT_PEPPYDISPLAY = 131072; // y Peppy display +const FEAT_SENDSPIN = 262144; // y Sendspin renderer // ------- -// 228279 +// 524287 // Notifications const NOTIFY_TITLE_INFO = ' Info'; @@ -3697,7 +3698,7 @@ $('#btn-preferences-update').click(function(e){ // CoverView 'scnsaver_timeout': SESSION.json['scnsaver_timeout'], - 'scnsaver_whenplaying' = SESSION.json['scnsaver_whenplaying'], + 'scnsaver_whenplaying' : SESSION.json['scnsaver_whenplaying'], 'auto_coverview': SESSION.json['auto_coverview'], 'scnsaver_style': SESSION.json['scnsaver_style'], 'scnsaver_mode': SESSION.json['scnsaver_mode'], diff --git a/www/ren-config.php b/www/ren-config.php index 8741e4cc7..60e3d107a 100644 --- a/www/ren-config.php +++ b/www/ren-config.php @@ -200,6 +200,17 @@ submitJob('rbrestart', '', NOTIFY_TITLE_INFO, NAME_ROONBRIDGE . NOTIFY_MSG_SVC_MANUAL_RESTART); } +// Sendspin +if (isset($_POST['update_sendspin_settings'])) { + if (isset($_POST['sendspinsvc']) && $_POST['sendspinsvc'] != $_SESSION['sendspinsvc']) { + $update = true; + phpSession('write', 'sendspinsvc', $_POST['sendspinsvc']); + } + if (isset($update)) { + submitJob('sendspinsvc'); + } +} + phpSession('close'); // Bluetooth @@ -356,6 +367,14 @@ $_feat_roonbridge = 'hide'; } +// Sendspin +$_feat_sendspin = $_SESSION['feat_bitmask'] & FEAT_SENDSPIN ? '' : 'hide'; +$_SESSION['sendspinsvc'] == '1' ? $_sendspin_btn_disable = '' : $_sendspin_btn_disable = 'disabled'; +$_SESSION['sendspinsvc'] == '1' ? $_sendspin_link_disable = '' : $_sendspin_link_disable = 'onclick="return false;"'; +$autoClick = " onchange=\"autoClick('#btn-set-sendspinsvc');\""; +$_select['sendspinsvc_on'] .= "\n"; +$_select['sendspinsvc_off'] .= "\n"; + waitWorker('ren-config'); $tpl = "ren-config.html"; diff --git a/www/templates/ren-config.html b/www/templates/ren-config.html index aeda9c08a..d952cdebb 100644 --- a/www/templates/ren-config.html +++ b/www/templates/ren-config.html @@ -414,6 +414,28 @@

Renderers

RoonBridge + +
+ Sendspin +

Sendspin must be installed manually otherwise sendspin-cli cannot start.

+ +
+
+ $_select[sendspinsvc_on] + $_select[sendspinsvc_off] +
+ + + + Sendspin is a project from the Open Home Foundation. + +
+ +
+ + Sendspin control +
+
diff --git a/www/util/renderer-onoff.php b/www/util/renderer-onoff.php index 5b2edffaa..a9d8cffe8 100755 --- a/www/util/renderer-onoff.php +++ b/www/util/renderer-onoff.php @@ -40,6 +40,8 @@ case '--roonbridge': onoffRoonBridge($onoff); break; + case '--sendspin': + onoffSendspin($onoff); case '--upnp': onoffUPnP($onoff); break; @@ -57,7 +59,8 @@ $slArg = $_SESSION['feat_bitmask'] & FEAT_SQUEEZELITE ? " --squeezelite\tTurn Squeezelite On/Off\n" : ""; $paArg = $_SESSION['feat_bitmask'] & FEAT_PLEXAMP ? " --plexamp\tTurn Plexamp On/Off\n" : ""; $rbArg = $_SESSION['feat_bitmask'] & FEAT_ROONBRIDGE ? " --roonbridge\tTurn RoonBridge On/Off\n" : ""; - $rendererList = ' '. $btArg . $apArg . $spArg . $dzArg . $upArg . $slArg . $paArg . $rbArg . + $ssArg = $_SESSION['feat_bitmask'] & FEAT_SENDSPIN ? " --sendspin\tTurn Sendspin On/Off\n" : ""; + $rendererList = ' '. $btArg . $apArg . $spArg . $dzArg . $upArg . $slArg . $paArg . $rbArg . $ssArg . " --help\t\tPrint this help text\n"; echo "Usage: renderer-onoff [OPTION] [on|off] @@ -162,3 +165,14 @@ function onoffRoonBridge($onoff) { stopRoonBridge(); } } + +function onoffSendspin($onoff) { + if ($onoff == 'on' && $_SESSION['sendspinsvc'] == '0') { + phpSession('write', 'sendspinsvc', '1'); + startSendspin(); + } else if ($onoff == 'off' && $_SESSION['sendspinsvc'] == '1') { + phpSession('write', 'sendspinsvc', '0'); + stopSendspin(); + } +} + diff --git a/www/util/restart-renderer.php b/www/util/restart-renderer.php index 845f1675b..21255d006 100755 --- a/www/util/restart-renderer.php +++ b/www/util/restart-renderer.php @@ -42,6 +42,9 @@ case '--roonbridge': restartRoonBridge($stopOnly); break; + case '--sendspin': + restartSendspin($stopOnly); + break; case '--upnp': restartUPnP($stopOnly); break; @@ -50,7 +53,7 @@ fwrite(STDERR, "This command requires sudo to print the help\n"); return; } - //[--bluetooth | --airplay | --spotify | --deezer | --upnp | --squeezelite | --plexamp | --roonbridge] + //[--bluetooth | --airplay | --spotify | --deezer | --upnp | --squeezelite | --plexamp | --roonbridge | --sendspin] $btArg = $_SESSION['feat_bitmask'] & FEAT_BLUETOOTH ? "--bluetooth\tRestart Bluetooth\n" : ""; $apArg = $_SESSION['feat_bitmask'] & FEAT_AIRPLAY ? " --airplay\tRestart AirPlay\n" : ""; $spArg = $_SESSION['feat_bitmask'] & FEAT_SPOTIFY ? " --spotify\tRestart Spotify Connect\n" : ""; @@ -59,7 +62,8 @@ $slArg = $_SESSION['feat_bitmask'] & FEAT_SQUEEZELITE ? " --squeezelite\tRestart Squeezelite\n" : ""; $paArg = $_SESSION['feat_bitmask'] & FEAT_PLEXAMP ? " --plexamp\tRestart Plexamp\n" : ""; $rbArg = $_SESSION['feat_bitmask'] & FEAT_ROONBRIDGE ? " --roonbridge\tRestart RoonBridge\n" : ""; - $rendererList = ' '. $btArg . $apArg . $spArg . $dzArg . $upArg . $slArg . $paArg . $rbArg . + $ssArg = $_SESSION['feat_bitmask'] & FEAT_SENDSPIN ? " --sendspin\tRestart Sendspin\n" : ""; + $rendererList = ' '. $btArg . $apArg . $spArg . $dzArg . $upArg . $slArg . $paArg . $rbArg . $ssArg . " --help\t\tPrint this help text\n"; echo "Usage: restart-renderer [OPTION] [--stop] @@ -141,3 +145,10 @@ function restartRoonBridge($stopOnly) { startRoonBridge(); } } + +function restartSendspin($stopOnly) { + stopSendspin(); + if ($stopOnly === false) { + startSendspin(); + } +} diff --git a/www/util/sysinfo.sh b/www/util/sysinfo.sh index 19a1953dc..f054825c7 100755 --- a/www/util/sysinfo.sh +++ b/www/util/sysinfo.sh @@ -140,6 +140,9 @@ AUDIO_PARAMETERS() { if [ $(($feat_bitmask & $FEAT_ROONBRIDGE)) -ne 0 ]; then echo -e "\nRoonBridge\t\t= $rbsvc\c" fi + if [ $(($feat_bitmask & $FEAT_SENDSPIN)) -ne 0 ]; then + echo -e "\nSendspin server\t= $sendspinsvc\c" + fi if [ $(($feat_bitmask & $FEAT_GPIO)) -ne 0 ]; then echo -e "\nGPIO button handler\t= $gpio_svc\c" fi @@ -383,6 +386,11 @@ RENDERER_SETTINGS() { fi fi + if [ $(($feat_bitmask & $FEAT_SENDSPIN)) -ne 0 ]; then + echo -e "S E N D S P I N" + echo -e "\nSendspin information here\c" + fi + if [ $(($feat_bitmask & $FEAT_LOCALDISPLAY)) -ne 0 ]; then echo -e "A T T A C H E D D I S P L A Y" echo -e "\nScreen blank\t\t= $scn_blank\c" @@ -445,6 +453,7 @@ FEAT_PLEXAMP=8192 FEAT_BLUETOOTH=16384 FEAT_MULTIROOM=65536 FEAT_PEPPYDISPLAY=131072 +FEAT_SENDSPIN=262144 # Selective resampling bitmask SOX_UPSAMPLE_ALL=3 # Upsample if source < target rate From ebb399c77c5dd390f70178af8503ad6ab567f2fd Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:50:15 +0100 Subject: [PATCH 02/11] fix font corruption for Gulp v5 --- gulpfile.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b48ce210a..ae49e3e5d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -458,11 +458,11 @@ gulp.task('minifyhtml', function (done) { }); gulp.task('artwork', function(done) { - gulp.src([ pkg.app.src+'/webfonts/**/*' - ,pkg.app.src+'/fonts/**/*' - ,pkg.app.src+'/images/**/*' ], {base:pkg.app.src}) - .pipe($.if(!mode.force(), $.newer( { dest: pkg.app.dest}))) -// .pipe($.size({showFiles: true, total: true})) + gulp.src([ pkg.app.src+'/webfonts/**/*', + pkg.app.src+'/fonts/**/*', + pkg.app.src+'/images/**/*' ], {base: pkg.app.src, encoding: false}) + .pipe($.if(!mode.force(), $.newer( { dest: pkg.app.dest }))) +// .pipe($.size({showFiles: true, total: true})) .pipe(gulp.dest(pkg.app.dest)); done(); }); @@ -511,7 +511,7 @@ gulp.task('deployback', gulp.series(['patchheader','patchfooter', 'patchindex', ,'!'+pkg.app.src+'/*-config.php' ,pkg.app.src+'/css/shellinabox*.css' ], - {base: pkg.app.src}) + {base: pkg.app.src, encoding: false}) // optional headers fields can be update and or added: //.pipe( $.replaceTask({ patterns: REPLACEMENT_PATTERNS })) //.pipe($.if('*.html', $.header(banner_html, {pkg: pkg}) )) @@ -525,7 +525,7 @@ gulp.task('deployback', gulp.series(['patchheader','patchfooter', 'patchindex', })); gulp.task('deployfront', function (done) { - return gulp.src( [pkg.app.dest+'/**/*', '!'+pkg.app.dest+'/index.html'] ) + return gulp.src( [pkg.app.dest+'/**/*', '!'+pkg.app.dest+'/index.html'], { encoding: false } ) .pipe($.if(!mode.force(), $.newer( { dest: DEPLOY_LOCATION}))) .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) .pipe(gulp.dest(DEPLOY_LOCATION)) From 2993e019dc47b68f983db5ed6d59d8abc9461c94 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:52:24 +0100 Subject: [PATCH 03/11] fix Sendspin installed check for moodeutl --- www/daemon/worker.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/www/daemon/worker.php b/www/daemon/worker.php index 22473abec..7fe33055c 100755 --- a/www/daemon/worker.php +++ b/www/daemon/worker.php @@ -1191,12 +1191,13 @@ // Start Sendspin renderer if ($_SESSION['feat_bitmask'] & FEAT_SENDSPIN) { - if (isset($_SESSION['sendspinsvc']) && $_SESSION['sendspinsvc'] == 1) { - $status = 'started'; - startSendspin(); - } else { - $status = 'available'; - } + if ($_SESSION['sendspin_installed'] == 'yes') { + if (isset($_SESSION['sendspinsvc']) && $_SESSION['sendspinsvc'] == 1) { + $status = 'started'; + startSendspin(); + } else { + $status = 'available'; + } } else { $status = 'not installed'; } From e4abd9675475a761e56eedee9a8dc02c834c1ffc Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:05:26 +0100 Subject: [PATCH 04/11] add Pi5 16GB --- www/util/pirev.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/util/pirev.py b/www/util/pirev.py index 4338f5555..9221b8d43 100755 --- a/www/util/pirev.py +++ b/www/util/pirev.py @@ -70,7 +70,8 @@ 2: "1GB", 3: "2GB", 4: "4GB", - 5: "8GB" + 5: "8GB", + 6: "16GB" } PI_PROC = { From 863563a4bbe14c56d592b09f4df5aa748fdabaff Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:01:44 +0100 Subject: [PATCH 05/11] update syntax compatible with sendspin-4.0.0 --- etc/systemd/system/sendspin.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/systemd/system/sendspin.service b/etc/systemd/system/sendspin.service index 32d32722e..0f909f63c 100644 --- a/etc/systemd/system/sendspin.service +++ b/etc/systemd/system/sendspin.service @@ -1,11 +1,11 @@ [Unit] -Description=Sendspin CLI +Description=Sendspin Multi Room Audio Client After=network-online.target Requires=network-online.target [Service] Type=simple -ExecStart=/root/.local/share/uv/tools/sendspin/bin/sendspin --headless +ExecStart=/root/.local/share/uv/tools/sendspin/bin/sendspin daemon Restart=on-failure User=root From e3690ce569b814781e9db380df185801071f81e4 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:07:08 +0100 Subject: [PATCH 06/11] update for sendspin 4.0.0 --- var/local/www/db/moode-sqlite3.db.sql | 11 ++++++ www/daemon/worker.php | 7 ++++ www/inc/constants.php | 1 + www/inc/renderer.php | 17 ++++++++- www/inc/sql.php | 27 +++++++++++++- www/sen-config.php | 49 ++++++++++++++++++++++++ www/setup_3rdparty_sendspin.txt | 42 +++++++++++++++++++++ www/setup_renderers.txt | 9 ++++- www/templates/ren-config.html | 9 +++-- www/templates/sen-config.html | 54 +++++++++++++++++++++++++++ 10 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 www/sen-config.php create mode 100644 www/setup_3rdparty_sendspin.txt create mode 100644 www/templates/sen-config.html diff --git a/var/local/www/db/moode-sqlite3.db.sql b/var/local/www/db/moode-sqlite3.db.sql index 21d1c637b..078e63e65 100644 --- a/var/local/www/db/moode-sqlite3.db.sql +++ b/var/local/www/db/moode-sqlite3.db.sql @@ -491,6 +491,17 @@ INSERT INTO cfg_sl (id, param, value) VALUES (5, 'TASKPRIORITY', '45'); INSERT INTO cfg_sl (id, param, value) VALUES (6, 'CODECS', 'flac,pcm,mp3,ogg,aac,alac,dsd'); INSERT INTO cfg_sl (id, param, value) VALUES (7, 'OTHEROPTIONS', '-W -D 500 -R E -S /var/local/www/commandw/slpower.sh'); +-- Table: cfg_sendspin +CREATE TABLE cfg_sendspin (id INTEGER PRIMARY KEY, param CHAR (10), value CHAR (128)); +INSERT INTO cfg_sendspin (id, param, value) VALUES (1, 'last_server_url', ''); -- default: discover via mDNS, otherwise WebSocket URL of Sendspin server +INSERT INTO cfg_sendspin (id, param, value) VALUES (2, 'name', 'moOde Sendspin Audio Client'); -- default: +INSERT INTO cfg_sendspin (id, param, value) VALUES (3, 'client_id', 'moode-sendspin'); -- default: sendspin-cli- +INSERT INTO cfg_sendspin (id, param, value) VALUES (4, 'log_level','INFO'); -- options: DEBUG, INFO, WARNING, ERROR or CRITICAL +INSERT INTO cfg_sendspin (id, param, value) VALUES (5, 'static_delay_ms', '0.0'); -- extra playback delay in milliseconds (applied after clock sync) +INSERT INTO cfg_sendspin (id, param, value) VALUES (6, 'audio_device', ''); -- audio output device index (e.g., 0, 1, 2) or name prefix (e.g., 'vc4-hdmi-1' or 'snd_rpi_hifiberry_dacplus') +INSERT INTO cfg_sendspin (id, param, value) VALUES (7, 'use_mpris', 'false'); -- Media Player Remote Interfacing Specification - see https://github.com/abmantis/aiosendspin-mpris + + -- Table: cfg_source CREATE TABLE cfg_source ( id INTEGER PRIMARY KEY, diff --git a/www/daemon/worker.php b/www/daemon/worker.php index 7fe33055c..0bc3bc049 100755 --- a/www/daemon/worker.php +++ b/www/daemon/worker.php @@ -3236,6 +3236,13 @@ function runQueuedJob() { startSendspin(); } break; + case 'sendspincfgupdate': + cfgSendspin(); + if ($_SESSION['sendspinsvc'] == '1') { + sysCmd('systemctl stop sendspin'); + startSendspin(); + } + break; case 'multiroom_tx': if ($_SESSION['multiroom_tx'] == 'On') { // Reconfigure to Dummy sound driver diff --git a/www/inc/constants.php b/www/inc/constants.php index b40d51242..e60979c4b 100755 --- a/www/inc/constants.php +++ b/www/inc/constants.php @@ -128,6 +128,7 @@ const NAME_DLNA = 'DLNA'; const NAME_PLEXAMP = 'Plexamp'; const NAME_ROONBRIDGE = 'RoonBridge'; +const NAME_SENDSPIN = 'Sendspin'; const NAME_GPIO = 'GPIO Controller'; const NAME_LOCALDISPLAY = 'Local Display'; const NAME_PEPPYDISPLAY = 'Peppy Display'; diff --git a/www/inc/renderer.php b/www/inc/renderer.php index f490f9181..53163bf4f 100644 --- a/www/inc/renderer.php +++ b/www/inc/renderer.php @@ -374,10 +374,25 @@ function startSendspin() { sysCmd('mpc stop'); sysCmd('systemctl start sendspin'); } - function stopSendspin() { sysCmd('systemctl stop sendspin'); } +function cfgSendspin() { + // example, minimal 'required config' for /root/.config/sendspin/settings-daemon.json + // + // { + // "static_delay_ms": 0.0, + // "name": "moOde Sendspin Audio Client", + // "client_id": "moode-sendspin", + // "audio_device": "snd_rpi_hifiberry_dacplus", + // "use_mpris": false + // } + + $filename = '/root/.config/sendspin/settings-daemon.json'; + $data = sqlRead(table:'cfg_sendspin', dbh: sqlConnect(), format: 'json'); + + file_put_contents($filename, $data); +} // Stop all renderers function stopAllRenderers() { diff --git a/www/inc/sql.php b/www/inc/sql.php index 81a6fffa0..956496ee9 100644 --- a/www/inc/sql.php +++ b/www/inc/sql.php @@ -16,7 +16,7 @@ function sqlConnect() { } } -function sqlRead($table, $dbh, $param = '', $id = '') { +function sqlRead($table, $dbh, $param = '', $id = '', $format = 'array') { if (empty($param) && empty($id)) { $queryStr = 'SELECT * FROM ' . $table; } else if (!empty($id)) { @@ -37,7 +37,30 @@ function sqlRead($table, $dbh, $param = '', $id = '') { $queryStr = 'SELECT value FROM ' . $table . " WHERE param='" . $param . "'"; } - return sqlQuery($queryStr, $dbh); + $rows = sqlQuery($queryStr, $dbh); + if ($format === 'json') { + $clean = []; + foreach ($rows as $row) { + $param = $row['param']; + $value = $row['value']; + + // Universal type detection and conversion + if ($value === '' || $value === null) { + $clean[$param] = null; + } elseif (is_numeric($value) && strpos($value, '.') !== false) { + $clean[$param] = (float)$value; // "0.0" → 0.0 + } elseif (is_numeric($value)) { + $clean[$param] = (int)$value; // "123" → 123 + } elseif (in_array(strtolower($value), ['true', 'false', 'yes', 'no', '1', '0'])) { + $clean[$param] = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== false; + } else { + $clean[$param] = $value; // String + } + } + return json_encode($clean, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + return $rows; } function sqlUpdate($table, $dbh, $key = '', $value) { diff --git a/www/sen-config.php b/www/sen-config.php new file mode 100644 index 000000000..7b3f9d318 --- /dev/null +++ b/www/sen-config.php @@ -0,0 +1,49 @@ + $value) { + chkValue($key, $value); + sqlUpdate('cfg_sendspin', $dbh, $key, $value); + } + $notify = $_SESSION['sendspinsvc'] == '1' ? + array('title' => NOTIFY_TITLE_INFO, 'msg' => NAME_SENDSPIN . NOTIFY_MSG_SVC_RESTARTED) : + array('title' => '', 'msg' => ''); + submitJob('sendspincfgupdate', '', $notify['title'], $notify['msg']); +} + +phpSession('close'); + +$result = sqlRead(table: 'cfg_sendspin', dbh: $dbh, format: 'array'); +$cfgSENDSPIN = array(); + +foreach ($result as $row) { + $cfgSENDSPIN[$row['param']] = $row['value']; +} + +$_sendspin_select['last_server_url'] = $cfgSENDSPIN['last_server_url']; // Sendspin Server URL, empty for auto-detect +// $_sendspin_select['static_delay_ms'] = $cfgSENDSPIN['static_delay_ms']; +$_sendspin_select['name'] = $cfgSENDSPIN['name']; +// $_sendspin_select['client_id'] = $cfgSENDSPIN['client_id']; +$_sendspin_select['audio_device'] = $cfgSENDSPIN['audio_device']; + +waitWorker('sendspin_config'); + +$tpl = "sen-config.html"; +$section = basename(__FILE__, '.php'); +storeBackLink($section, $tpl); + +include('header.php'); +eval("echoTemplate(\"" . getTemplate("templates/$tpl") . "\");"); +include('footer.php'); diff --git a/www/setup_3rdparty_sendspin.txt b/www/setup_3rdparty_sendspin.txt new file mode 100644 index 000000000..0d62b3bb9 --- /dev/null +++ b/www/setup_3rdparty_sendspin.txt @@ -0,0 +1,42 @@ +################################################################################ +# +# Setup Guide for sendspin-cli 3rd party component +# +# Version: 1.0 2026-01-16 +# +################################################################################ + + +OVERVIEW + +This document provides some information on using moOde with Sendspin. + +Sendspin CLI is a synchronized audio client supporting Sendspin servers. + +In moOde, it can be setup as a player; rendering it a multiroom audio endpoint for Sendspin servers such as Music Assistant. + +See https://github.com/Sendspin/sendspin-cli for more information. + +INSTALLATION + +To install sendspin-cli in moOde, SSH to your Raspberry Pi and install uv / sendspin: + +$ sudo pip install uv --break-system-packages +$ sudo uv tool install sendspin@4.0.0 + +To check if the Sendspin server is listed and the desired audio device is available: + +$ sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-servers +$ sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-audio-devices + +TROUBLESHOOTING + +Trying to playing both from moOde via MPD and to the Sendspin renderer at the same moment may crash/loop sendspin and trigger [PaErrorCode -9985] + +Change moOde's audio output to a different device while playing through the Sendspin renderer prevents this error. + +This issue might be fixed in a future release of moOde. + +################################################################################ +# Post questions regarding this guide to http://moodeaudio.org/forum +################################################################################ diff --git a/www/setup_renderers.txt b/www/setup_renderers.txt index 7e4ee3a7c..4c8ea377f 100644 --- a/www/setup_renderers.txt +++ b/www/setup_renderers.txt @@ -2,7 +2,7 @@ # # Setup Guide for Audio Renderers # -# Version: 1.2 2025-07-21 +# Version: 1.2 2026-01-16 # # (C) Tim Curtis 2024 http://moodeaudio.org # @@ -44,6 +44,7 @@ from the client and whether CamillaDSP is supported in the playback chain. +------------------------------------------------------------------------------- | RoonBridge | Roon server | No | No +------------------------------------------------------------------------------- +| Sendspin | Music Assistant Server (2) | No | No (1) On 2025-05-06 Deezer officially cancelled support for Deezer Connect but it apparantly is still available on iOS when you enable Remote Control under @@ -51,6 +52,11 @@ Settings > Music > Deezer Lab, and on Android when you use an older APK version. This may stop working at any time. Official notice of cancellation: https://en.deezercommunity.com/product-updates/say-goodbye-to-deezer-connect-80661 +(2) On 2026-01-16, Sendspin is currently in technical preview. While functional, the +protocol and its implementation may change (see https://www.sendspin-audio.com/ ) + +To use the Sendspin renderer in moOde, see setup_3rdparty_sendspin.txt. + MULTIPLE RENDERERS ON The following audio renderers can be ON at the same time waiting for connections @@ -72,6 +78,7 @@ when they are turned ON and thus no other renderer can be also be ON. - Squeezelite (if -c option is not specified) - Plexamp - RoonBridge +- Sendspin RENDERER PROTOCOL TYPES diff --git a/www/templates/ren-config.html b/www/templates/ren-config.html index d952cdebb..bfb41488e 100644 --- a/www/templates/ren-config.html +++ b/www/templates/ren-config.html @@ -417,7 +417,10 @@

Renderers

Sendspin -

Sendspin must be installed manually otherwise sendspin-cli cannot start.

+

+ Requires sendspin-cli, view the Setup guide. +

+
@@ -432,8 +435,8 @@

Renderers

- - Sendspin control + + Sendspin settings
diff --git a/www/templates/sen-config.html b/www/templates/sen-config.html new file mode 100644 index 000000000..14b4f0cb2 --- /dev/null +++ b/www/templates/sen-config.html @@ -0,0 +1,54 @@ + +
+
+

Sendspin

+ +
+ Settings + + + +

+ For a full list of sendspin-cli options visit https://github.com/Sendspin/sendspin-cli +

+ +
+ +
+ + + + Friendly name that will appear at the Sendspin Players list + +
+ + +
+ + + + Audio output device index (e.g., 0, 1, 2) or name prefix (e.g., 'vc4-hdmi-1' or 'snd_rpi_hifiberry_dacplus')
+ Use sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-audio-devices to see available devices.
+
+ IMPORTANT: DO NOT use MPD while executing this cmd! +
+
+ + +
+ + + + When empty, the URL will discover via mDNS, otherwise WebSocket URL of Sendspin server. + +
+ +
+
+
+
From 9599041eb2ccd1a13621a040d68df08bc8b6e154 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:13:05 +0100 Subject: [PATCH 07/11] remove temporary readme --- README-SENDSPIN.md | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 README-SENDSPIN.md diff --git a/README-SENDSPIN.md b/README-SENDSPIN.md deleted file mode 100644 index ecca49885..000000000 --- a/README-SENDSPIN.md +++ /dev/null @@ -1,41 +0,0 @@ -# Experiment: moOde + Sendspin - -Experiment: extending moOde with a renderer for Sendspin. - -Start a Sendspin server; e.g. - -```bash -sudo docker run -v /home/your-login-name/music-assistant-data:/data --network host --cap-add=DAC_READ_SEARCH --cap-add=SYS_ADMIN --security-opt apparmor:unconfined ghcr.io/music-assistant/server:beta -``` - -Install [sendspin-cli](https://github.com/Sendspin/sendspin-cli) in moOde; e.g. - -```bash -sudo pip install uv --break-system-packages -sudo uv tool install sendspin -``` - -Test via SSH if the Sendspin server can be reached; and desired audio device is available: - -```bash -sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-servers -sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-audio-devices -``` - -Change the audio device by modifying `/etc/systemd/system/sendspin.service` : - -```service -[Unit] -Description=Sendspin CLI -After=network-online.target -Requires=network-online.target - -[Service] -Type=simple -ExecStart=/root/.local/share/uv/tools/sendspin/bin/sendspin --audio-device snd_rpi_hifiberry_dacplus --headless -Restart=on-failure -User=root - -[Install] -WantedBy=multi-user.target -``` From 396ae3c5076769ded7731bfb18b1faeb83d4f0a5 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:32:25 +0100 Subject: [PATCH 08/11] remove Sendspin config --- var/local/www/db/moode-sqlite3.db.sql | 11 ------ www/daemon/worker.php | 13 +------ www/sen-config.php | 49 ------------------------ www/setup_3rdparty_sendspin.txt | 20 +++++----- www/templates/ren-config.html | 8 +--- www/templates/sen-config.html | 54 --------------------------- 6 files changed, 14 insertions(+), 141 deletions(-) delete mode 100644 www/sen-config.php delete mode 100644 www/templates/sen-config.html diff --git a/var/local/www/db/moode-sqlite3.db.sql b/var/local/www/db/moode-sqlite3.db.sql index c933e960a..b079e91f6 100644 --- a/var/local/www/db/moode-sqlite3.db.sql +++ b/var/local/www/db/moode-sqlite3.db.sql @@ -491,17 +491,6 @@ INSERT INTO cfg_sl (id, param, value) VALUES (5, 'TASKPRIORITY', '45'); INSERT INTO cfg_sl (id, param, value) VALUES (6, 'CODECS', 'flac,pcm,mp3,ogg,aac,alac,dsd'); INSERT INTO cfg_sl (id, param, value) VALUES (7, 'OTHEROPTIONS', '-W -D 500 -R E -S /var/local/www/commandw/slpower.sh'); --- Table: cfg_sendspin -CREATE TABLE cfg_sendspin (id INTEGER PRIMARY KEY, param CHAR (10), value CHAR (128)); -INSERT INTO cfg_sendspin (id, param, value) VALUES (1, 'last_server_url', ''); -- default: discover via mDNS, otherwise WebSocket URL of Sendspin server -INSERT INTO cfg_sendspin (id, param, value) VALUES (2, 'name', 'moOde Sendspin Audio Client'); -- default: -INSERT INTO cfg_sendspin (id, param, value) VALUES (3, 'client_id', 'moode-sendspin'); -- default: sendspin-cli- -INSERT INTO cfg_sendspin (id, param, value) VALUES (4, 'log_level','INFO'); -- options: DEBUG, INFO, WARNING, ERROR or CRITICAL -INSERT INTO cfg_sendspin (id, param, value) VALUES (5, 'static_delay_ms', '0.0'); -- extra playback delay in milliseconds (applied after clock sync) -INSERT INTO cfg_sendspin (id, param, value) VALUES (6, 'audio_device', ''); -- audio output device index (e.g., 0, 1, 2) or name prefix (e.g., 'vc4-hdmi-1' or 'snd_rpi_hifiberry_dacplus') -INSERT INTO cfg_sendspin (id, param, value) VALUES (7, 'use_mpris', 'false'); -- Media Player Remote Interfacing Specification - see https://github.com/abmantis/aiosendspin-mpris - - -- Table: cfg_source CREATE TABLE cfg_source ( id INTEGER PRIMARY KEY, diff --git a/www/daemon/worker.php b/www/daemon/worker.php index 1e70a9046..8f8e1c428 100755 --- a/www/daemon/worker.php +++ b/www/daemon/worker.php @@ -616,10 +616,8 @@ workerLog('worker: RoonBridge: ' . $msg); // Sendspin -// To install -// - sudo pip install uv --break-system-packages -// - sudo uv tool install sendspin -if (file_exists('/root/.local/share/uv/tools/sendspin/bin/sendspin') === true) { +// Installer: https://github.com/Sendspin/sendspin-cli/blob/main/scripts/systemd/install-systemd.sh +if (file_exists('/home/sendspin/.local/share/uv/tools/sendspin/bin/sendspin') === true) { $msg = 'installed'; $_SESSION['sendspin_installed'] = 'yes'; @@ -3299,13 +3297,6 @@ function runQueuedJob() { startSendspin(); } break; - case 'sendspincfgupdate': - cfgSendspin(); - if ($_SESSION['sendspinsvc'] == '1') { - sysCmd('systemctl stop sendspin'); - startSendspin(); - } - break; case 'multiroom_tx': if ($_SESSION['multiroom_tx'] == 'On') { // Reconfigure to Dummy sound driver diff --git a/www/sen-config.php b/www/sen-config.php deleted file mode 100644 index 7b3f9d318..000000000 --- a/www/sen-config.php +++ /dev/null @@ -1,49 +0,0 @@ - $value) { - chkValue($key, $value); - sqlUpdate('cfg_sendspin', $dbh, $key, $value); - } - $notify = $_SESSION['sendspinsvc'] == '1' ? - array('title' => NOTIFY_TITLE_INFO, 'msg' => NAME_SENDSPIN . NOTIFY_MSG_SVC_RESTARTED) : - array('title' => '', 'msg' => ''); - submitJob('sendspincfgupdate', '', $notify['title'], $notify['msg']); -} - -phpSession('close'); - -$result = sqlRead(table: 'cfg_sendspin', dbh: $dbh, format: 'array'); -$cfgSENDSPIN = array(); - -foreach ($result as $row) { - $cfgSENDSPIN[$row['param']] = $row['value']; -} - -$_sendspin_select['last_server_url'] = $cfgSENDSPIN['last_server_url']; // Sendspin Server URL, empty for auto-detect -// $_sendspin_select['static_delay_ms'] = $cfgSENDSPIN['static_delay_ms']; -$_sendspin_select['name'] = $cfgSENDSPIN['name']; -// $_sendspin_select['client_id'] = $cfgSENDSPIN['client_id']; -$_sendspin_select['audio_device'] = $cfgSENDSPIN['audio_device']; - -waitWorker('sendspin_config'); - -$tpl = "sen-config.html"; -$section = basename(__FILE__, '.php'); -storeBackLink($section, $tpl); - -include('header.php'); -eval("echoTemplate(\"" . getTemplate("templates/$tpl") . "\");"); -include('footer.php'); diff --git a/www/setup_3rdparty_sendspin.txt b/www/setup_3rdparty_sendspin.txt index 0d62b3bb9..47cdd3672 100644 --- a/www/setup_3rdparty_sendspin.txt +++ b/www/setup_3rdparty_sendspin.txt @@ -2,7 +2,7 @@ # # Setup Guide for sendspin-cli 3rd party component # -# Version: 1.0 2026-01-16 +# Version: 1.0 2026-02-28 # ################################################################################ @@ -19,15 +19,9 @@ See https://github.com/Sendspin/sendspin-cli for more information. INSTALLATION -To install sendspin-cli in moOde, SSH to your Raspberry Pi and install uv / sendspin: +To install sendspin-cli in moOde, SSH to your Raspberry Pi and start the installer script to configure Sendspin as daemon. -$ sudo pip install uv --break-system-packages -$ sudo uv tool install sendspin@4.0.0 - -To check if the Sendspin server is listed and the desired audio device is available: - -$ sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-servers -$ sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-audio-devices +$ curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cli/refs/heads/main/scripts/systemd/install-systemd.sh | sudo bash TROUBLESHOOTING @@ -35,7 +29,13 @@ Trying to playing both from moOde via MPD and to the Sendspin renderer at the sa Change moOde's audio output to a different device while playing through the Sendspin renderer prevents this error. -This issue might be fixed in a future release of moOde. +This issue might be fixed in the future release. + +To check if the Sendspin server is listed and the desired audio device is available: + +$ sudo /home/sendspin/.local/share/uv/tools/sendspin/bin/sendspin --list-servers +$ sudo /home/sendspin/.local/share/uv/tools/sendspin/bin/sendspin --list-audio-devices + ################################################################################ # Post questions regarding this guide to http://moodeaudio.org/forum diff --git a/www/templates/ren-config.html b/www/templates/ren-config.html index bfb41488e..7906278bf 100644 --- a/www/templates/ren-config.html +++ b/www/templates/ren-config.html @@ -418,7 +418,7 @@

Renderers

Sendspin

- Requires sendspin-cli, view the Setup guide. + Requires sendspin-cli, view the setup guide.

@@ -430,14 +430,10 @@

Renderers

- Sendspin is a project from the Open Home Foundation. + Sendspin is a project from the Open Home Foundation.
-
- - Sendspin settings -
diff --git a/www/templates/sen-config.html b/www/templates/sen-config.html deleted file mode 100644 index 14b4f0cb2..000000000 --- a/www/templates/sen-config.html +++ /dev/null @@ -1,54 +0,0 @@ - -
-
-

Sendspin

- -
- Settings - - - -

- For a full list of sendspin-cli options visit https://github.com/Sendspin/sendspin-cli -

- -
- -
- - - - Friendly name that will appear at the Sendspin Players list - -
- - -
- - - - Audio output device index (e.g., 0, 1, 2) or name prefix (e.g., 'vc4-hdmi-1' or 'snd_rpi_hifiberry_dacplus')
- Use sudo /root/.local/share/uv/tools/sendspin/bin/sendspin --list-audio-devices to see available devices.
-
- IMPORTANT: DO NOT use MPD while executing this cmd! -
-
- - -
- - - - When empty, the URL will discover via mDNS, otherwise WebSocket URL of Sendspin server. - -
- -
-
-
-
From 1b35d61fb19b7a79b4e0a1caeee3148d7d0a1ee7 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:33:57 +0100 Subject: [PATCH 09/11] fix gulp-chown io_uring error --- gulpfile.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index ae49e3e5d..1ba79a748 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -399,7 +399,7 @@ gulp.task('patchheader', function (done) { .pipe($.if('header.php', $.cacheBust({ type: 'timestamp' })) ) - .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) + .pipe($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe($.size({showFiles: true, total: false})) .pipe(gulp.dest(DEPLOY_LOCATION)) .on('end', done); @@ -415,7 +415,7 @@ gulp.task('patchfooter', function (done) { .pipe($.rename(function (path) { path.basename += '.min'; })) - .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) + .pipe($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe($.size({showFiles: true, total: false})) .pipe(gulp.dest(DEPLOY_LOCATION)) .on('end', done); @@ -426,7 +426,7 @@ gulp.task('patchindex', function (done) { .pipe($.if(!mode.force(), $.newer( { dest: pkg.app.dist}))) .pipe($.replace(/indextpl[.]html/g, "indextpl.min.html")) .pipe($.replace(/footer[.]php/g, "footer.min.php")) - .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) + .pipe($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe($.size({showFiles: true, total: false})) .pipe(gulp.dest(DEPLOY_LOCATION)) .on('end', done); @@ -436,7 +436,7 @@ gulp.task('patchconfigs', function (done) { return gulp.src(pkg.app.src+'/*-config.php') .pipe($.if(!mode.force(), $.newer( { dest: pkg.app.dist}))) .pipe($.replace(/footer[.]php/g, "footer.min.php")) - .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) + .pipe($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe($.size({showFiles: true, total: false})) .pipe(gulp.dest(DEPLOY_LOCATION)) .on('end', done); @@ -451,7 +451,7 @@ gulp.task('minifyhtml', function (done) { .pipe($.rename(function (path) { path.basename += '.min'; })) - .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) + .pipe($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe($.size({showFiles: true, total: false})) .pipe(gulp.dest(DEPLOY_LOCATION+'/templates')) .on('end', done); @@ -515,7 +515,7 @@ gulp.task('deployback', gulp.series(['patchheader','patchfooter', 'patchindex', // optional headers fields can be update and or added: //.pipe( $.replaceTask({ patterns: REPLACEMENT_PATTERNS })) //.pipe($.if('*.html', $.header(banner_html, {pkg: pkg}) )) - .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) + .pipe($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe($.if(!mode.force(), $.newer( { dest: DEPLOY_LOCATION}))) //.pipe($.size({showFiles: true, total: true})) .pipe($.if('*.html', $.replaceTask({ patterns: REPLACEMENT_PATTERNS}))) @@ -527,7 +527,7 @@ gulp.task('deployback', gulp.series(['patchheader','patchfooter', 'patchindex', gulp.task('deployfront', function (done) { return gulp.src( [pkg.app.dest+'/**/*', '!'+pkg.app.dest+'/index.html'], { encoding: false } ) .pipe($.if(!mode.force(), $.newer( { dest: DEPLOY_LOCATION}))) - .pipe($.if(!(mode.test()||mode.remote()), $.chown('root','root'))) + .pipe($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe(gulp.dest(DEPLOY_LOCATION)) .on('end', done); }); From 5c8244e2716680dc75e9f4b5ddd3e2f94dad5d38 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:34:27 +0100 Subject: [PATCH 10/11] fix bitmask --- www/inc/constants.php | 8 ++++---- www/js/playerlib.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/www/inc/constants.php b/www/inc/constants.php index 3e4cce5fd..e2f3ebb87 100755 --- a/www/inc/constants.php +++ b/www/inc/constants.php @@ -194,24 +194,24 @@ const FEAT_HTTPS = 1; // y HTTPS mode const FEAT_AIRPLAY = 2; // y AirPlay renderer const FEAT_MINIDLNA = 4; // y DLNA server -const FEAT_RECORDER = 8; // Stream recorder +const FEAT_RECORDER = 8; // n Stream recorder const FEAT_SQUEEZELITE = 16; // y Squeezelite renderer const FEAT_UPMPDCLI = 32; // y UPnP client for MPD const FEAT_DEEZER = 64; // n Deezer Connect renderer const FEAT_ROONBRIDGE = 128; // y RoonBridge renderer const FEAT_LOCALDISPLAY = 256; // y Local display const FEAT_INPSOURCE = 512; // y Input source select -const FEAT_UPNPSYNC = 1024; // UPnP volume sync +const FEAT_UPNPSYNC = 1024; // n UPnP volume sync const FEAT_SPOTIFY = 2048; // y Spotify Connect renderer const FEAT_GPIO = 4096; // y GPIO button handler const FEAT_PLEXAMP = 8192; // y Plexamp renderer const FEAT_BLUETOOTH = 16384; // y Bluetooth renderer -const FEAT_DEVTWEAKS = 32768; // Developer tweaks +const FEAT_DEVTWEAKS = 32768; // n Developer tweaks const FEAT_MULTIROOM = 65536; // y Multiroom audio const FEAT_PEPPYDISPLAY = 131072; // y Peppy display const FEAT_SENDSPIN = 262144; // y Sendspin renderer // ------ -// 524287 +// 490423 // Selective resampling bitmask const SOX_UPSAMPLE_ALL = 3; // Upsample if source < target rate diff --git a/www/js/playerlib.js b/www/js/playerlib.js index 8d4fa279e..a05f750d4 100755 --- a/www/js/playerlib.js +++ b/www/js/playerlib.js @@ -25,7 +25,7 @@ const FEAT_MULTIROOM = 65536; // y Multiroom audio const FEAT_PEPPYDISPLAY = 131072; // y Peppy display const FEAT_SENDSPIN = 262144; // y Sendspin renderer // ------- -// 524287 +// 490423 // Notifications const NOTIFY_TITLE_INFO = ' Info'; From b8eefaf76336a1f2a6a4467d307b63dcb9a779f1 Mon Sep 17 00:00:00 2001 From: Mathieu Markus <72262120+m-mrks@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:35:12 +0100 Subject: [PATCH 11/11] remove Sendspin config helper functions --- www/inc/renderer.php | 16 ---------------- www/inc/sql.php | 24 +----------------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/www/inc/renderer.php b/www/inc/renderer.php index 53163bf4f..d5ce4533d 100644 --- a/www/inc/renderer.php +++ b/www/inc/renderer.php @@ -377,22 +377,6 @@ function startSendspin() { function stopSendspin() { sysCmd('systemctl stop sendspin'); } -function cfgSendspin() { - // example, minimal 'required config' for /root/.config/sendspin/settings-daemon.json - // - // { - // "static_delay_ms": 0.0, - // "name": "moOde Sendspin Audio Client", - // "client_id": "moode-sendspin", - // "audio_device": "snd_rpi_hifiberry_dacplus", - // "use_mpris": false - // } - - $filename = '/root/.config/sendspin/settings-daemon.json'; - $data = sqlRead(table:'cfg_sendspin', dbh: sqlConnect(), format: 'json'); - - file_put_contents($filename, $data); -} // Stop all renderers function stopAllRenderers() { diff --git a/www/inc/sql.php b/www/inc/sql.php index 956496ee9..e216b1958 100644 --- a/www/inc/sql.php +++ b/www/inc/sql.php @@ -16,7 +16,7 @@ function sqlConnect() { } } -function sqlRead($table, $dbh, $param = '', $id = '', $format = 'array') { +function sqlRead($table, $dbh, $param = '', $id = '') { if (empty($param) && empty($id)) { $queryStr = 'SELECT * FROM ' . $table; } else if (!empty($id)) { @@ -38,28 +38,6 @@ function sqlRead($table, $dbh, $param = '', $id = '', $format = 'array') { } $rows = sqlQuery($queryStr, $dbh); - if ($format === 'json') { - $clean = []; - foreach ($rows as $row) { - $param = $row['param']; - $value = $row['value']; - - // Universal type detection and conversion - if ($value === '' || $value === null) { - $clean[$param] = null; - } elseif (is_numeric($value) && strpos($value, '.') !== false) { - $clean[$param] = (float)$value; // "0.0" → 0.0 - } elseif (is_numeric($value)) { - $clean[$param] = (int)$value; // "123" → 123 - } elseif (in_array(strtolower($value), ['true', 'false', 'yes', 'no', '1', '0'])) { - $clean[$param] = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== false; - } else { - $clean[$param] = $value; // String - } - } - return json_encode($clean, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - return $rows; }