(timed out or device gave up)
+
+import base64
+import os
+import pwd
+import socket
+import subprocess
+import sys
+import uuid
+
+import dbus
+import dbus.mainloop.glib
+import dbus.service
+from gi.repository import GLib
+
+AGENT_PATH = '/org/bluez/moode_agent'
+# DisplayYesNo drives Numeric Comparison (authenticated). NoInputNoOutput falls
+# back to Just Works, i.e. today's behaviour with no modal. argv wins for testing,
+# then the unit's environment, then the default.
+CAPABILITY = (len(sys.argv) > 1 and sys.argv[1]) \
+ or os.environ.get('BT_AGENT_CAPABILITY') or 'DisplayYesNo'
+SEND_FECMD = '/var/www/util/send-fecmd.php'
+RESPONSE_SOCK = '/tmp/moode-btagent.sock'
+RESPONSE_USER = 'www-data' # front-end (php-fpm) writes the reply here
+# Safety net only. Normal closure is driven by bluez calling Cancel() when the device
+# gives up, which keeps the modal in sync with what the phone shows. This long timeout
+# just prevents a stuck modal if Cancel() never arrives.
+CONFIRM_TIMEOUT = 60
+
+
+def log(msg):
+ print(msg, flush=True)
+
+
+def push_fe(cmd):
+ # Fire-and-forget notify to every connected UI; never let it block the agent.
+ try:
+ subprocess.Popen(['php', SEND_FECMD, cmd],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ except Exception as e:
+ log('push_fe failed: %s' % e)
+
+
+def device_props(path):
+ bus = dbus.SystemBus()
+ props = dbus.Interface(bus.get_object('org.bluez', path),
+ 'org.freedesktop.DBus.Properties')
+ def get(name, default=''):
+ try:
+ return str(props.Get('org.bluez.Device1', name))
+ except dbus.DBusException:
+ return default
+ return get('Name', 'Bluetooth device'), get('Icon', 'bluetooth')
+
+
+class Rejected(dbus.DBusException):
+ _dbus_error_name = 'org.bluez.Error.Rejected'
+
+
+class PairingAgent(dbus.service.Object):
+ def __init__(self, bus, path):
+ super().__init__(bus, path)
+ self.pending = {} # id -> {'reply', 'error', 'timeout', 'code'}
+
+ # --- request lifecycle ------------------------------------------------
+ def _open(self, method, device, code, reply, error):
+ req_id = uuid.uuid4().hex[:8]
+ name, icon = device_props(device)
+ name_b64 = base64.b64encode(name.encode()).decode()
+ timeout = GLib.timeout_add_seconds(CONFIRM_TIMEOUT, self._expire, req_id)
+ self.pending[req_id] = {'reply': reply, 'error': error,
+ 'timeout': timeout, 'code': code}
+ push_fe('pairreq,%s,%s,%s,%s,%s' % (req_id, method, code, name_b64, icon))
+ log('%s(%s) code=%s -> req %s' % (method, device, code, req_id))
+ return req_id
+
+ def _resolve(self, req_id, accepted, code=None):
+ req = self.pending.pop(req_id, None)
+ if req is None:
+ return # already resolved (duplicate/late reply): ignore silently
+ log('resolve req %s accepted=%s' % (req_id, accepted))
+ GLib.source_remove(req['timeout'])
+ if accepted:
+ if req['code'] == '__input__':
+ req['reply'](dbus.UInt32(code))
+ else:
+ req['reply']()
+ else:
+ req['error'](Rejected('Rejected by user'))
+ # Close the dialog on any other UI that also popped it (browser + local
+ # display): the client that answered has already closed its own.
+ push_fe('paircancel,%s' % req_id)
+
+ def _expire(self, req_id):
+ req = self.pending.pop(req_id, None)
+ if req is not None:
+ req['error'](Rejected('Timed out'))
+ push_fe('paircancel,%s' % req_id)
+ log('req %s timed out' % req_id)
+ return False
+
+ def _cancel_all(self):
+ for req_id in list(self.pending):
+ req = self.pending.pop(req_id)
+ GLib.source_remove(req['timeout'])
+ req['error'](Rejected('Cancelled'))
+ push_fe('paircancel,%s' % req_id)
+
+ # --- org.bluez.Agent1 -------------------------------------------------
+ @dbus.service.method('org.bluez.Agent1', in_signature='', out_signature='')
+ def Release(self):
+ log('Release')
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='os', out_signature='',
+ async_callbacks=('reply', 'error'))
+ def AuthorizeService(self, device, uuid_, reply, error):
+ # A2DP/AVRCP on an already-paired device: accept silently, like today.
+ log('AuthorizeService(%s, %s) -> accept' % (device, uuid_))
+ reply()
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='ou', out_signature='',
+ async_callbacks=('reply', 'error'))
+ def RequestConfirmation(self, device, passkey, reply, error):
+ self._open('confirm', device, '%06u' % passkey, reply, error)
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='o', out_signature='',
+ async_callbacks=('reply', 'error'))
+ def RequestAuthorization(self, device, reply, error):
+ self._open('authorize', device, '', reply, error)
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='o', out_signature='u',
+ async_callbacks=('reply', 'error'))
+ def RequestPasskey(self, device, reply, error):
+ self._open('input', device, '__input__', reply, error)
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='ouq', out_signature='')
+ def DisplayPasskey(self, device, passkey, entered):
+ # Informational: show the code the user must type on their device.
+ name, icon = device_props(device)
+ name_b64 = base64.b64encode(name.encode()).decode()
+ push_fe('pairreq,%s,display,%06u,%s,%s' % (uuid.uuid4().hex[:8], passkey, name_b64, icon))
+ log('DisplayPasskey(%s, %06u)' % (device, passkey))
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='os', out_signature='')
+ def DisplayPinCode(self, device, pincode):
+ log('DisplayPinCode(%s, %s) -> ignored (legacy)' % (device, pincode))
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='o', out_signature='s')
+ def RequestPinCode(self, device):
+ # Legacy PIN pairing is not offered; reject so bluez does not fall back to it.
+ log('RequestPinCode(%s) -> reject (legacy not supported)' % device)
+ raise Rejected('Legacy PIN not supported')
+
+ @dbus.service.method('org.bluez.Agent1', in_signature='', out_signature='')
+ def Cancel(self):
+ log('Cancel')
+ self._cancel_all()
+
+
+def on_response(sock, _cond, agent):
+ try:
+ data = sock.recv(256).decode().strip()
+ except OSError:
+ return True
+ for line in data.splitlines():
+ parts = line.split(',')
+ if parts[0] == 'pairresp' and len(parts) >= 3:
+ req_id, accepted = parts[1], parts[2] == '1'
+ code = int(parts[3]) if accepted and len(parts) >= 4 and parts[3].isdigit() else None
+ agent._resolve(req_id, accepted, code)
+ return True
+
+
+def make_response_socket():
+ if os.path.exists(RESPONSE_SOCK):
+ os.unlink(RESPONSE_SOCK)
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+ sock.bind(RESPONSE_SOCK)
+ # php-fpm (www-data) must be able to write the user's answer here.
+ ent = pwd.getpwnam(RESPONSE_USER)
+ os.chown(RESPONSE_SOCK, ent.pw_uid, ent.pw_gid)
+ os.chmod(RESPONSE_SOCK, 0o660)
+ return sock
+
+
+def main():
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ bus = dbus.SystemBus()
+
+ agent = PairingAgent(bus, AGENT_PATH)
+ manager = dbus.Interface(bus.get_object('org.bluez', '/org/bluez'),
+ 'org.bluez.AgentManager1')
+ manager.RegisterAgent(AGENT_PATH, CAPABILITY)
+ manager.RequestDefaultAgent(AGENT_PATH)
+ log('registered as default agent, capability=%s' % CAPABILITY)
+
+ sock = make_response_socket()
+ GLib.io_add_watch(sock, GLib.IO_IN, lambda s, c: on_response(s, c, agent))
+
+ loop = GLib.MainLoop()
+ try:
+ loop.run()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ try:
+ manager.UnregisterAgent(AGENT_PATH)
+ except dbus.DBusException:
+ pass
+ if os.path.exists(RESPONSE_SOCK):
+ os.unlink(RESPONSE_SOCK)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/www/daemon/peppy-gain.php b/www/daemon/peppy-gain.php
new file mode 100755
index 000000000..b5bc12572
--- /dev/null
+++ b/www/daemon/peppy-gain.php
@@ -0,0 +1,70 @@
+#!/usr/bin/php
+/dev/null', 'r');
+ if ($monitor !== false) {
+ while (fgets($monitor) !== false) {
+ publishGainDb($dbh);
+ }
+ pclose($monitor);
+ }
+ // Only reached if the card went away (USB DAC unplugged). The worker restarts us
+ // on a card change; keep retrying so a replug alone is enough to recover.
+ sleep(PEPPY_GAIN_MON_RETRY);
+}
diff --git a/www/daemon/touchmon.php b/www/daemon/touchmon.php
index fcb5cd8f0..d1ff219bb 100644
--- a/www/daemon/touchmon.php
+++ b/www/daemon/touchmon.php
@@ -18,6 +18,7 @@
//debugLog('touchmon: Started');
$timeoutArg = !isset($argv[1]) ? TOUCHMON_TIMEOUT_DEFAULT : $argv[1];
$timeout = $timeoutArg;
+$closedCount = 0;
$dbh = sqlConnect();
sysCmd('rm ' . TOUCHMON_LOG . ' > /dev/null');
sysCmd('killall -s9 xinput > /dev/null');
@@ -80,9 +81,16 @@
}
}
// Switch to WebUI
+ // MPD closes the ALSA device between tracks, so a single closed reading is
+ // not proof that playback stopped. Require a few in a row.
if (isPeppyOn($dbh) === true && isAudioPlaying() === false) {
- //debugLog('touchmon: - switch to webui');
- exec('sudo moodeutl --setdisplay webui');
+ if (++$closedCount >= TOUCHMON_CLOSED_COUNT) {
+ //debugLog('touchmon: - switch to webui');
+ exec('sudo moodeutl --setdisplay webui');
+ $closedCount = 0;
+ }
+ } else {
+ $closedCount = 0;
}
} else {
//debugLog('touchmon: - WARNING: peppyalsa is not enabled');
diff --git a/www/daemon/worker.php b/www/daemon/worker.php
index a88582785..38e5a8fc7 100755
--- a/www/daemon/worker.php
+++ b/www/daemon/worker.php
@@ -18,6 +18,7 @@
require_once __DIR__ . '/../inc/music-source.php';
require_once __DIR__ . '/../inc/network.php';
require_once __DIR__ . '/../inc/peripheral.php';
+require_once __DIR__ . '/../inc/radio-browser.php';
require_once __DIR__ . '/../inc/renderer.php';
require_once __DIR__ . '/../inc/session.php';
require_once __DIR__ . '/../inc/sql.php';
@@ -144,6 +145,9 @@
}
// - Delete session vars that have been removed or renamed
$sessionVars = array(
+ 'mpd_dbupdate_status',
+ 'trackcover_url_cache',
+ 'radio_track_covers'
);
foreach ($sessionVars as $var) {
sysCmd('moodeutl -D ' . $var);
@@ -188,6 +192,8 @@
sysCmd('touch ' . SLPOWER_LOG);
sysCmd('truncate ' . MOUNTMON_LOG . ' --size 0');
sysCmd('mkdir ' . THMCACHE_DIR . ' > /dev/null 2>&1');
+// Radio Browser caches are written synchronously by www-data (php-fpm), so unlike moOde's
+sysCmd('/var/www/util/radio-browser.sh --fix-permissions > /dev/null 2>&1');
// Delete any tmp files left over from New/Edit station or playlist
sysCmd('rm /var/local/www/imagesw/radio-logos/' . TMP_IMAGE_PREFIX . '* > /dev/null 2>&1');
sysCmd('rm /var/local/www/imagesw/radio-logos/thumbs/' . TMP_IMAGE_PREFIX . '* > /dev/null 2>&1');
@@ -211,7 +217,6 @@
sysCmd('chmod 0666 ' . SLPOWER_LOG);
sysCmd('chmod 0666 ' . MOODE_LOG);
sysCmd('chmod 0666 ' . MOUNTMON_LOG);
-sysCmd('chmod 0600 ' . BT_PINCODE_CONF);
if (!file_exists(ETC_MACHINE_INFO)) {
sysCmd('cp /usr/share/moode-player' . ETC_MACHINE_INFO . ' /etc/');
workerLog('worker: File check: created default /etc/machine-info');
@@ -739,6 +744,13 @@
// ALSA mixer
phpSession('write', 'amixname', getAlsaMixerName($_SESSION['adevname']));
workerLog('worker: ALSA mixer: ' . ($_SESSION['amixname'] == 'none' ? 'none exists' : $_SESSION['amixname']));
+// Drop a stray softvol control left under the simple mixer name by an earlier release.
+// ALSA restores it at boot, where it shadows the hardware control. Only application
+// created controls are removed, so a hardware element is never touched.
+if ($_SESSION['amixname'] != 'none') {
+ sysCmd('alsactl clean ' . $_SESSION['cardnum'] . ' "name=\'' . $_SESSION['amixname'] . '\'"');
+ sysCmd('alsactl store ' . $_SESSION['cardnum']);
+}
// HDMI mixer initialize (after first boot a test signal needs to be sent to "register" the mixer with ALSA)
if ($_SESSION['alsa_output_mode'] == 'iec958') {
$result = getAlsaVolume($_SESSION['amixname']);
@@ -912,9 +924,13 @@
}
}
-// Database update item count
-if (!isset($_SESSION['mpd_dbupdate_status'])) {
- $_SESSION['mpd_dbupdate_status'] = 0;
+// Database update file count
+if (!isset($_SESSION['mpd_dbupdate_count'])) {
+ $_SESSION['mpd_dbupdate_count'] = 0;
+}
+// Database stats (artists/albums/tracks)
+if (!isset($_SESSION['mpd_db_stats'])) {
+ $_SESSION['mpd_db_stats'] = 'none';
}
// Start MPD
@@ -979,9 +995,9 @@
workerLog('worker: MPD CDSP volsync: ' . lcfirst($_SESSION['camilladsp_volume_sync']));
$serviceCmd = CamillaDSP::isMPD2CamillaDSPVolSyncEnabled() ? 'start' : 'stop';
sysCmd('systemctl ' . $serviceCmd .' mpd2cdspvolume');
-// Library stats
-$stats = getLibraryStats($sock);
-workerLog('worker: Library stats: ' . $stats);
+workerLog('worker: Database stats: ' .
+ ($_SESSION['mpd_db_stats'] == 'none' ? 'Analyze has not been run' : $_SESSION['mpd_db_stats'])
+);
//----------------------------------------------------------------------------//
workerLog('worker: --');
@@ -1058,10 +1074,12 @@
// Bluetooth session vars
$status = 'session vars ok';
-if (!isset($_SESSION['bt_pin_code'])) {
+if (!isset($_SESSION['bt_pairing_confirm'])) {
$status = 'session vars created';
- $_SESSION['bt_pin_code'] = '';
+ $_SESSION['bt_pairing_confirm'] = '1';
}
+// Keep the agent's capability file in step with the setting before it is started.
+applyBtPairingConfirm($_SESSION['bt_pairing_confirm']);
// ALSA/CDSP max volumes
if (!isset($_SESSION['alsavolume_max_bt'])) {
$_SESSION['alsavolume_max_bt'] = $_SESSION['alsavolume_max'];
@@ -1073,10 +1091,6 @@
if (!isset($_SESSION['bluez_sbc_quality'])) {
$_SESSION['bluez_sbc_quality'] = 'xq+';
}
-// ALSA output mode
-if (!isset($_SESSION['alsa_output_mode_bt'])) {
- $_SESSION['alsa_output_mode_bt'] = '_audioout';
-}
// Controller mode
if (!isset($_SESSION['bluez_controller_mode'])) {
$_SESSION['bluez_controller_mode'] = 'dual';
@@ -1092,20 +1106,23 @@
} else {
$status = 'n/a';
}
-$status .= ', PIN: ' . (empty($_SESSION['bt_pin_code']) ? 'None' : 'Set');
+$status .= ', Pair confirm: ' . ($_SESSION['bt_pairing_confirm'] == '1' ? 'On' : 'Off');
$status .= ', ALSA/CDSP max: ' . $_SESSION['alsavolume_max_bt'] . '%/' . $_SESSION['cdspvolume_max_bt'] . 'dB';
-$status .= ', ALSA out: ' . ALSA_OUTPUT_MODE_BT_NAME[$_SESSION['alsa_output_mode_bt']];
$status .= ', Transport: ' . $_SESSION['bluez_controller_mode'];
workerLog('worker: Bluetooth: ' . $status);
// Start airplay renderer
if ($_SESSION['feat_bitmask'] & FEAT_AIRPLAY) {
+ if (!isset($_SESSION['airplaysvc_type'])) {
+ $_SESSION['airplaysvc_type'] = '2';
+ }
if (isset($_SESSION['airplaysvc']) && $_SESSION['airplaysvc'] == 1) {
$status = 'started';
startAirPlay();
} else {
$status = 'available';
}
+ $status = $status . ', protocol: ' . $_SESSION['airplaysvc_type'];
} else {
$status = 'n/a';
}
@@ -1422,6 +1439,12 @@
}
startLocalDisplay();
}
+// Not gated on peppy_display: that only says which screen touchmon is showing right now
+// (it swaps to the WebUI whenever playback stops), while the meter can come back at the
+// next track. Follow the ALSA chain instead, like updAudioOutAndBtOutConfs() does.
+if ($_SESSION['peppy_display'] == '1' || $_SESSION['enable_peppyalsa'] == '1') {
+ startPeppyGainMon();
+}
// WebUI display
workerLog('worker: WebUI display: ' . ($_SESSION['local_display'] == '1' ? 'on' : 'off'));
@@ -1521,9 +1544,9 @@
// NOTE: updaterAutoCheck() logs status
$_SESSION['updater_available_update'] = updaterAutoCheck($validIPAddress);
-// Radio track covers
-workerLog('worker: Radio track covers: ' . lcfirst($_SESSION['radio_track_covers']));
-workerLog('worker: iTunes query timeout: ' . $_SESSION['itunes_query_timeout'] . ' sec(s)');
+// Radio cover search provider
+workerLog('worker: Radio covers: ' . $_SESSION['radio_covers']);
+workerLog('worker: iTunes timeout: ' . $_SESSION['itunes_query_timeout'] . ' secs');
// Automatic CoverView (Preferences)
workerLog('worker: Auto-CoverView: ' . ($_SESSION['auto_coverview'] == '-on' ? 'on' : 'off'));
@@ -1647,13 +1670,6 @@
$_SESSION['lib_fv_only'] = 'off';
}
-// Radio track cover URL cache
-if (!isset($_SESSION['trackcover_url_cache'])) {
- $_SESSION['trackcover_url_cache'] = '';
-}
-// Empty cache
-$_SESSION['trackcover_url_cache'] = array('' => ''); // trackTitle => URL
-
// Metadata file
if (!isset($_SESSION['extmeta'])) {
$_SESSION['extmeta'] = '0';
@@ -1949,6 +1965,10 @@
//debugLog('** chkPeppyScnBlank');
chkPeppyScnBlank();
}
+ if ($_SESSION['peppy_display'] == '1' || $_SESSION['enable_peppyalsa'] == '1') {
+ //debugLog('** chkPeppyGainMon');
+ chkPeppyGainMon();
+ }
// CoverView (as screen saver)
if ($_SESSION['scnsaver_timeout'] != 'Never') {
//debugLog('** chkScnSaver');
@@ -2280,6 +2300,16 @@ function chkAttachedDisplayOnOff() {
sendFECmd('local_display_onoff,' . $currentOnOff);
}
}
+// Tracks Hardware volume and updates peppy
+function chkPeppyGainMon() {
+ // The meter gain is only as good as the daemon that publishes it: if it dies the
+ // needles keep displaying the last dB and silently stop following the volume.
+ // The [.] keeps the pattern from matching the shell that runs pgrep itself.
+ if (sysCmd('pgrep -c -f "peppy-gain[.]php"')[0] == '0') {
+ workerLog('worker: Peppy gain monitor: not running, restarted');
+ startPeppyGainMon();
+ }
+}
// Peppy screen blank
// - Timeout is set
// - Peppy is on
@@ -2359,18 +2389,17 @@ function chkLibraryUpdate() {
workerLog('worker: CRITICAL ERROR: chkLibraryUpdate(): Connection to MPD failed');
} else {
$status = getMpdStatus($sock);
- $stats = getLibraryStats($sock);
closeMpdSock($sock);
- $_SESSION['mpd_dbupdate_status'] = countMpdLogLines();
- if ($_SESSION['mpd_dbupdate_status'] != 0) {
- debugLog('mpdindex: File count ' . $_SESSION['mpd_dbupdate_status']);
+ $_SESSION['mpd_dbupdate_count'] = countMpdLogLines();
+ if ($_SESSION['mpd_dbupdate_count'] != 0) {
+ debugLog('mpdindex: File count ' . $_SESSION['mpd_dbupdate_count']);
}
if (!isset($status['updating_db'])) {
sendFECmd('libupd_done');
$GLOBALS['check_library_update'] = '0';
- workerLog('mpdindex: Done: indexed ' . $stats);
+ workerLog('mpdindex: Done: updated ' . $_SESSION['mpd_dbupdate_count'] . ' files');
workerLog('worker: Job update_library done');
}
}
@@ -2382,18 +2411,17 @@ function chkLibraryRegen() {
workerLog('worker: CRITICAL ERROR: chkLibraryRegen(): Connection to MPD failed');
} else {
$status = getMpdStatus($sock);
- $stats = getLibraryStats($sock);
closeMpdSock($sock);
- $_SESSION['mpd_dbupdate_status'] = countMpdLogLines();
- if ($_SESSION['mpd_dbupdate_status'] != 0) {
- debugLog('mpdindex: File count ' . $_SESSION['mpd_dbupdate_status']);
+ $_SESSION['mpd_dbupdate_count'] = countMpdLogLines();
+ if ($_SESSION['mpd_dbupdate_count'] != 0) {
+ debugLog('mpdindex: File count ' . $_SESSION['mpd_dbupdate_count']);
}
if (!isset($status['updating_db'])) {
sendFECmd('libregen_done');
$GLOBALS['check_library_regen'] = '0';
- workerLog('mpdindex: Done: indexed ' . $stats);
+ workerLog('mpdindex: Done: indexed ' . $_SESSION['mpd_dbupdate_count'] . ' files');
workerLog('worker: Job regen_library done');
}
}
@@ -2425,13 +2453,12 @@ function chkClockRadio() {
$mpdCmd = 'play ' . parseMpdRespAsArray($resp)['Pos'];
}
+ // Set volume
+ sysCmd('/var/www/util/vol.sh ' . $_SESSION['clkradio_volume']);
// Send play cmd
sendMpdCmd($sock, $mpdCmd);
$resp = readMpdResp($sock);
closeMpdSock($sock);
-
- // Set volume
- sysCmd('/var/www/util/vol.sh ' . $_SESSION['clkradio_volume']);
}
} else if ($currentTime == $GLOBALS['clkradio_stop_time'] && $GLOBALS['clkradio_stop_days'][$currentDay] == '1') {
//workerLog('chkClockRadio(): stoptime=(' . $GLOBALS['clkradio_stop_time'] . ')');
@@ -2809,7 +2836,7 @@ function runQueuedJob() {
workerLog('worker: Truncate MPD log');
truncateMpdLog();
// Update library
- $cmd = empty($_SESSION['w_queueargs']) ? 'update' : 'update "' . html_entity_decode($_SESSION['w_queueargs']) . '"';
+ $cmd = empty($_SESSION['w_queueargs']) ? 'update' : 'update "' . escapeDblQuotes(html_entity_decode($_SESSION['w_queueargs'])) . '"';
workerLog('mpdindex: Cmd (' . $cmd . ')');
workerLog('mpdindex: Scanning');
if (false === ($sock = openMpdSock('localhost', 6600))) {
@@ -3146,6 +3173,9 @@ function runQueuedJob() {
break;
}
+ // Regenerate the Bluetooth A2DP sink device (AUDIODEV) for the new DSP head
+ updDspAndBtInConfs($_SESSION['cardnum'], $_SESSION['alsa_output_mode']);
+
// Restart MPD
// NOTE: Don't restart if already done in the camillaDSP section
if ($_SESSION['w_queue'] != 'camilladsp' || ($_SESSION['w_queue'] == 'camilladsp' && empty($queueArgs[1]))) {
@@ -3199,17 +3229,10 @@ function runQueuedJob() {
}
}
break;
- case 'bt_pin_code':
- if (empty($_SESSION['w_queueargs'])) {
- sysCmd('echo "* ' . '" > ' . BT_PINCODE_CONF);
- sysCmd("sed -i s'|ExecStart=/usr/bin/bt-agent.*|ExecStart=/usr/bin/bt-agent -c NoInputNoOutput|' /etc/systemd/system/bt-agent.service");
- sysCmd("sed -i s'|ExecStartPost=/bin/hciconfig.*|ExecStartPost=/bin/hciconfig hci0 sspmode 1|' /etc/systemd/system/bt-agent.service");
- } else {
- sysCmd('echo "* ' . $_SESSION['w_queueargs'] . '" > ' . BT_PINCODE_CONF);
- sysCmd("sed -i s'|ExecStart=/usr/bin/bt-agent.*|ExecStart=/usr/bin/bt-agent -c NoInputNoOutput -p " . BT_PINCODE_CONF . "|' /etc/systemd/system/bt-agent.service");
- sysCmd("sed -i s'|ExecStartPost=/bin/hciconfig.*|ExecStartPost=/bin/hciconfig hci0 sspmode 0|' /etc/systemd/system/bt-agent.service");
- }
- sysCmd('systemctl daemon-reload');
+ case 'bt_pairing_confirm':
+ // On: the pairing agent asks the user to confirm the code (DisplayYesNo,
+ // Numeric Comparison). Off: Just Works, no confirmation.
+ applyBtPairingConfirm($_SESSION['bt_pairing_confirm']);
sysCmd('systemctl restart bt-agent');
break;
case 'reset_bt_auto_disconnect':
@@ -3824,7 +3847,7 @@ function runQueuedJob() {
setAudioOut($_SESSION['w_queueargs']);
break;
- // command jobs
+ // From Prefs > Appearance
case 'set_bg_image':
$imgdata = base64_decode($_SESSION['w_queueargs'], true);
if ($imgdata === false) {
@@ -3835,6 +3858,8 @@ function runQueuedJob() {
fclose($fh);
}
break;
+
+ // Radio and Playlist view cover images
case 'set_ralogo_image':
case 'set_plcover_image':
$job = $_SESSION['w_queue'];
@@ -3935,6 +3960,38 @@ function runQueuedJob() {
sysCmd('chmod 0777 "' . $imgDir . $thmDir . TMP_IMAGE_PREFIX . '"*');
break;
+ // Radio Browser favorite to Radio view
+ case 'set_rblogo_image':
+ $queueArgs = explode('~~~', $_SESSION['w_queueargs'], 2);
+ $name = $queueArgs[0];
+ $imageData = $queueArgs[1];
+
+ $image = @imagecreatefromstring($imageData);
+ if (!$image) {
+ workerLog('worker: '. $job .' ERROR: imagecreatefromstring() failed for ' . $name);
+ break;
+ }
+
+ $w = imagesx($image);
+ $h = imagesy($image);
+ $ok1 = rbResizeAndSave($image, $w, $h, 400, RADIO_LOGOS_ROOT . $name . '.jpg');
+ $ok2 = rbResizeAndSave($image, $w, $h, 200, RADIO_LOGOS_ROOT . 'thumbs/' . $name . '.jpg');
+ $ok3 = rbResizeAndSave($image, $w, $h, 80, RADIO_LOGOS_ROOT . 'thumbs/' . $name . '_sm.jpg');
+
+ if ($ok1 && $ok2 && $ok3) {
+ if (imagedestroy($image) === false) {
+ workerLog('worker: '. $job .' ERROR: imagedestroy() failed for ' . $name);
+ break;
+ }
+ } else {
+ workerLog('worker: '. $job .' ERROR: rbResizeAndSave() failed for ' . $name);
+ break;
+ }
+
+ sysCmd('chmod 0777 "' . RADIO_LOGOS_ROOT . $name . '"*');
+ sysCmd('chmod 0777 "' . RADIO_LOGOS_ROOT . 'thumbs/' . $name . '"*');
+ break;
+
// Other jobs
case 'reboot':
case 'poweroff':
@@ -3983,7 +4040,8 @@ function runQueuedJob() {
// Clear MPD log
function truncateMpdLog() {
sysCmd('truncate ' . MPD_LOG . ' --size 0');
- $_SESSION['mpd_dbupdate_status'] = 0;
+ $_SESSION['mpd_dbupdate_count'] = 0;
+ $_SESSION['mpd_db_stats'] = 'none';
}
// Count number of lines in MPD log for database update or regen
function countMpdLogLines() {
diff --git a/www/engine-mpd.php b/www/engine-mpd.php
index 20f713914..e9ee21dcd 100644
--- a/www/engine-mpd.php
+++ b/www/engine-mpd.php
@@ -53,6 +53,7 @@
$event = explode("\n", $resp)[0];
$status = getMpdStatus($sock);
$status['idle_timeout_event'] = $event;
+ $status['idle_mixer_changed'] = (strpos($resp, 'changed: mixer') !== false) ? '1' : '0';
$status['empd_socket_timeout'] = $sockTimeout;
scriptLog('Event (' . $event . ')');
}
diff --git a/www/footer.php b/www/footer.php
index 8bd286828..9b3edc800 100644
--- a/www/footer.php
+++ b/www/footer.php
@@ -19,11 +19,11 @@
Your Privacy
- We want you to know that our audio player does not serve Ads, nag for subscriptions, use cookies, analytics/tracking or any other such technology. Player preference, configuration and operational data is stored on the local boot media and is not provided to any 3rd parties.
+ We want you to know that our audio player does not serve Ads, nag for subscriptions, use cookies, analytics/tracking or any other such technology. Player preference, configuration and operational data is stored on the local boot media and is under your complete control.
Release Information
- - Release: 10.2.3 2026-06-15
+ - Release: 10.3.3 2026-MM-DD
- Maintainer: Tim Curtis © 2014
- Documentation: View release notes, View setup guide
- Contributors: View contributors
@@ -69,6 +69,7 @@
Input select
+
Radio Cover+
@@ -89,6 +90,22 @@
+
+
+
+