diff --git a/etc/systemd/system/sendspin.service b/etc/systemd/system/sendspin.service new file mode 100644 index 000000000..0f909f63c --- /dev/null +++ b/etc/systemd/system/sendspin.service @@ -0,0 +1,13 @@ +[Unit] +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 daemon +Restart=on-failure +User=root + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index b48ce210a..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,18 +451,18 @@ 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); }); 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,11 +511,11 @@ 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}) )) - .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}))) @@ -525,9 +525,9 @@ 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($.if(!(mode.test()||mode.remote()), $.chown(0o644))) .pipe(gulp.dest(DEPLOY_LOCATION)) .on('end', done); }); diff --git a/usr/local/bin/moodeutl b/usr/local/bin/moodeutl index 00bed765a..81473d278 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 4b701868f..b079e91f6 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 29e5239a6..8f8e1c428 100755 --- a/www/daemon/worker.php +++ b/www/daemon/worker.php @@ -615,6 +615,18 @@ } workerLog('worker: RoonBridge: ' . $msg); +// Sendspin +// 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'; + +} 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])) { @@ -1196,6 +1208,23 @@ } workerLog('worker: RoonBridge: ' . $status); +// Start Sendspin renderer +if ($_SESSION['feat_bitmask'] & FEAT_SENDSPIN) { + if ($_SESSION['sendspin_installed'] == 'yes') { + 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 @@ -3262,7 +3291,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 07105aaf1..e2f3ebb87 100755 --- a/www/inc/constants.php +++ b/www/inc/constants.php @@ -133,6 +133,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'; @@ -193,23 +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 -// ------- -// 228279 +const FEAT_SENDSPIN = 262144; // y Sendspin renderer +// ------ +// 490423 // 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..d5ce4533d 100644 --- a/www/inc/renderer.php +++ b/www/inc/renderer.php @@ -369,6 +369,15 @@ 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 +388,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/inc/sql.php b/www/inc/sql.php index 81a6fffa0..e216b1958 100644 --- a/www/inc/sql.php +++ b/www/inc/sql.php @@ -37,7 +37,8 @@ function sqlRead($table, $dbh, $param = '', $id = '') { $queryStr = 'SELECT value FROM ' . $table . " WHERE param='" . $param . "'"; } - return sqlQuery($queryStr, $dbh); + $rows = sqlQuery($queryStr, $dbh); + return $rows; } function sqlUpdate($table, $dbh, $key = '', $value) { diff --git a/www/js/playerlib.js b/www/js/playerlib.js index 7e6d95577..a05f750d4 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 +// 490423 // Notifications const NOTIFY_TITLE_INFO = ' Info'; @@ -3706,7 +3707,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/setup_3rdparty_sendspin.txt b/www/setup_3rdparty_sendspin.txt new file mode 100644 index 000000000..47cdd3672 --- /dev/null +++ b/www/setup_3rdparty_sendspin.txt @@ -0,0 +1,42 @@ +################################################################################ +# +# Setup Guide for sendspin-cli 3rd party component +# +# Version: 1.0 2026-02-28 +# +################################################################################ + + +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 start the installer script to configure Sendspin as daemon. + +$ curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cli/refs/heads/main/scripts/systemd/install-systemd.sh | sudo bash + +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 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/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 aeda9c08a..7906278bf 100644 --- a/www/templates/ren-config.html +++ b/www/templates/ren-config.html @@ -414,6 +414,27 @@

Renderers

RoonBridge + +
+ Sendspin +

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

+ + +
+
+ $_select[sendspinsvc_on] + $_select[sendspinsvc_off] +
+ + + + Sendspin is a project from the Open Home Foundation.
+
+
+ +
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 4be7a21a8..ff4a48db2 100755 --- a/www/util/sysinfo.sh +++ b/www/util/sysinfo.sh @@ -141,6 +141,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 @@ -385,6 +388,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" @@ -447,6 +455,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