fix(luci): Fix dpi-dual menu and simplify lyrion UI

- Fix dpi-dual "firstchildview" error (changed to "firstchild")
- Simplify luci-app-lyrion: overview.js 276→150 lines
- Simplify luci-app-lyrion: settings.js 78→32 lines
- Simplify luci-app-lyrion: RPCD 300→90 lines
- Combined status + library stats into single RPC call
- Removed unused methods (update, logs, get_config, save_config)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-15 17:03:13 +01:00
parent fd54253f66
commit 58ba852564
5 changed files with 188 additions and 515 deletions

View File

@ -3,8 +3,7 @@
"title": "DPI Dual-Stream",
"order": 45,
"action": {
"type": "firstchildview",
"recurse": true
"type": "firstchild"
},
"depends": {
"acl": ["luci-app-dpi-dual"],

View File

@ -3,271 +3,176 @@
'require ui';
'require rpc';
'require poll';
'require secubox/kiss-theme';
var callStatus = rpc.declare({ object: 'luci.lyrion', method: 'status', expect: {} });
var callLibraryStats = rpc.declare({ object: 'luci.lyrion', method: 'get_library_stats', expect: {} });
var callInstall = rpc.declare({ object: 'luci.lyrion', method: 'install', expect: {} });
var callStart = rpc.declare({ object: 'luci.lyrion', method: 'start', expect: {} });
var callStop = rpc.declare({ object: 'luci.lyrion', method: 'stop', expect: {} });
var callRestart = rpc.declare({ object: 'luci.lyrion', method: 'restart', expect: {} });
var callRescan = rpc.declare({ object: 'luci.lyrion', method: 'rescan', expect: {} });
var callStatus = rpc.declare({ object: 'luci.lyrion', method: 'status' });
var callInstall = rpc.declare({ object: 'luci.lyrion', method: 'install' });
var callStart = rpc.declare({ object: 'luci.lyrion', method: 'start' });
var callStop = rpc.declare({ object: 'luci.lyrion', method: 'stop' });
var callRescan = rpc.declare({ object: 'luci.lyrion', method: 'rescan' });
return view.extend({
pollActive: true,
libraryStats: null,
load: function() {
return Promise.all([callStatus(), callLibraryStats()]);
return callStatus().catch(function() { return {}; });
},
startPolling: function() {
var self = this;
this.pollActive = true;
poll.add(L.bind(function() {
if (!this.pollActive) return Promise.resolve();
return Promise.all([callStatus(), callLibraryStats()]).then(L.bind(function(results) {
this.updateStatus(results[0]);
this.updateLibraryStats(results[1]);
}, this));
}, this), 3);
poll.add(function() {
return callStatus().then(function(s) {
self.updateUI(s);
});
}, 5);
},
updateStatus: function(status) {
var badge = document.getElementById('lyrion-status-badge');
updateUI: function(s) {
var badge = document.getElementById('status-badge');
if (badge) {
badge.innerHTML = '';
badge.appendChild(KissTheme.badge(status.running ? 'RUNNING' : 'STOPPED', status.running ? 'green' : 'red'));
}
},
updateLibraryStats: function(stats) {
if (!stats) return;
this.libraryStats = stats;
var statsEl = document.getElementById('lyrion-stats');
if (statsEl) {
statsEl.innerHTML = '';
this.renderStats(stats).forEach(function(el) { statsEl.appendChild(el); });
badge.className = 'cbi-value-field';
badge.innerHTML = s.running
? '<span style="color:#4caf50;font-weight:600">● Running</span>'
: '<span style="color:#f44336;font-weight:600">● Stopped</span>';
}
var scanEl = document.getElementById('lyrion-scan');
if (scanEl) {
scanEl.innerHTML = '';
scanEl.appendChild(this.renderScanStatus(stats));
var stats = document.getElementById('library-stats');
if (stats && s.songs !== undefined) {
stats.textContent = s.songs + ' songs, ' + s.albums + ' albums, ' + s.artists + ' artists';
}
},
formatNumber: function(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return n.toString();
},
renderStats: function(stats) {
var c = KissTheme.colors;
return [
KissTheme.stat(this.formatNumber(stats.songs || 0), 'Songs', c.purple),
KissTheme.stat(this.formatNumber(stats.albums || 0), 'Albums', c.blue),
KissTheme.stat(this.formatNumber(stats.artists || 0), 'Artists', c.green),
KissTheme.stat(this.formatNumber(stats.genres || 0), 'Genres', c.orange)
];
},
renderScanStatus: function(stats) {
if (stats.scanning) {
var pct = stats.scan_total > 0 ? Math.round((stats.scan_progress / stats.scan_total) * 100) : 0;
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('span', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [
E('span', { 'class': 'spinning', 'style': 'font-size: 14px;' }, '⏳'),
E('span', { 'style': 'font-weight: 600;' }, 'Scanning...')
]),
E('span', { 'style': 'font-size: 12px; color: var(--kiss-muted);' },
(stats.scan_phase || 'Processing') + ' (' + stats.scan_progress + '/' + stats.scan_total + ')')
]),
E('div', { 'style': 'height: 8px; background: var(--kiss-bg); border-radius: 4px; overflow: hidden;' }, [
E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: linear-gradient(90deg, var(--kiss-purple), var(--kiss-blue)); border-radius: 4px; transition: width 0.3s;' })
])
]);
} else {
return E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [
E('span', { 'style': 'display: flex; align-items: center; gap: 8px; color: var(--kiss-green);' }, [
E('span', {}, '✓'),
E('span', { 'style': 'font-weight: 600;' }, 'Library Ready')
]),
E('span', { 'style': 'font-size: 12px; color: var(--kiss-muted);' }, 'DB: ' + (stats.db_size || '0'))
]);
}
},
renderControls: function(status) {
return E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap;' }, [
E('button', {
'class': 'kiss-btn kiss-btn-green',
'click': ui.createHandlerFn(this, 'handleStart'),
'disabled': status.running
}, 'Start'),
E('button', {
'class': 'kiss-btn kiss-btn-red',
'click': ui.createHandlerFn(this, 'handleStop'),
'disabled': !status.running
}, 'Stop'),
E('button', {
'class': 'kiss-btn',
'click': ui.createHandlerFn(this, 'handleRestart'),
'disabled': !status.running
}, 'Restart'),
E('button', {
'class': 'kiss-btn kiss-btn-blue',
'click': ui.createHandlerFn(this, 'handleRescan'),
'disabled': !status.running
}, 'Rescan Library')
]);
},
renderServiceInfo: function(status) {
var checks = [
{ label: 'Runtime', value: status.detected_runtime || 'auto' },
{ label: 'Port', value: status.port || '9000' },
{ label: 'Memory', value: status.memory_limit || '256M' },
{ label: 'Media Path', value: status.media_path || '/srv/media' }
];
return E('div', { 'style': 'display: flex; flex-direction: column; gap: 8px;' },
checks.map(function(c) {
return E('div', { 'style': 'display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--kiss-line);' }, [
E('span', { 'style': 'color: var(--kiss-muted);' }, c.label),
E('span', { 'style': 'font-family: monospace;' }, c.value)
]);
})
);
['btn-start', 'btn-rescan'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.disabled = s.running;
});
var stopBtn = document.getElementById('btn-stop');
if (stopBtn) stopBtn.disabled = !s.running;
},
handleInstall: function() {
var self = this;
ui.showModal('Installing Lyrion', [
E('p', { 'class': 'spinning' }, 'Installing Lyrion Music Server. This may take several minutes...')
ui.showModal(_('Installing'), [
E('p', { 'class': 'spinning' }, _('Installing Lyrion Music Server...'))
]);
callInstall().then(function(r) {
ui.hideModal();
if (r.success) {
ui.addNotification(null, E('p', r.message || 'Installation started'));
self.startPolling();
window.location.reload();
ui.addNotification(null, E('p', _('Installation started')));
setTimeout(function() { location.reload(); }, 3000);
} else {
ui.addNotification(null, E('p', 'Failed: ' + (r.error || 'Unknown error')), 'error');
ui.addNotification(null, E('p', r.error || _('Installation failed')), 'error');
}
});
},
handleStart: function() {
ui.showModal('Starting...', [E('p', { 'class': 'spinning' }, 'Starting Lyrion...')]);
callStart().then(function(r) {
ui.hideModal();
if (r.success) ui.addNotification(null, E('p', 'Lyrion started'));
callStart().then(function() {
ui.addNotification(null, E('p', _('Lyrion started')));
});
},
handleStop: function() {
ui.showModal('Stopping...', [E('p', { 'class': 'spinning' }, 'Stopping Lyrion...')]);
callStop().then(function(r) {
ui.hideModal();
if (r.success) ui.addNotification(null, E('p', 'Lyrion stopped'));
});
},
handleRestart: function() {
ui.showModal('Restarting...', [E('p', { 'class': 'spinning' }, 'Restarting Lyrion...')]);
callRestart().then(function(r) {
ui.hideModal();
if (r.success) ui.addNotification(null, E('p', 'Lyrion restarted'));
callStop().then(function() {
ui.addNotification(null, E('p', _('Lyrion stopped')));
});
},
handleRescan: function() {
callRescan().then(function(r) {
if (r.success) ui.addNotification(null, E('p', 'Library rescan started'));
callRescan().then(function() {
ui.addNotification(null, E('p', _('Library rescan started')));
});
},
render: function(data) {
var status = data[0] || {};
var stats = data[1] || {};
this.libraryStats = stats;
render: function(status) {
var s = status || {};
// Not installed view
if (!status.installed) {
var notInstalledContent = [
E('div', { 'style': 'margin-bottom: 24px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Lyrion Music Server'),
KissTheme.badge('NOT INSTALLED', 'red')
]),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'Self-hosted music streaming with Squeezebox compatibility')
]),
KissTheme.card('Install', E('div', { 'style': 'text-align: center; padding: 40px;' }, [
E('div', { 'style': 'font-size: 4rem; margin-bottom: 16px;' }, '🎵'),
E('h3', { 'style': 'margin: 0 0 8px 0;' }, 'Lyrion Music Server'),
E('p', { 'style': 'color: var(--kiss-muted); margin: 0 0 20px 0;' }, 'Self-hosted music streaming with Squeezebox compatibility.'),
E('button', {
'class': 'kiss-btn kiss-btn-green',
'click': ui.createHandlerFn(this, 'handleInstall'),
'disabled': status.detected_runtime === 'none'
}, 'Install Lyrion')
]))
];
return KissTheme.wrap(notInstalledContent, 'admin/services/lyrion/overview');
// Not installed
if (!s.installed) {
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Lyrion Music Server')),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'text-align:center;padding:40px' }, [
E('p', { 'style': 'font-size:48px;margin:0' }, '🎵'),
E('h3', {}, _('Lyrion Music Server')),
E('p', {}, _('Self-hosted music streaming with Squeezebox compatibility.')),
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, 'handleInstall')
}, _('Install Lyrion'))
])
])
]);
}
// Installed view
// Installed
this.startPolling();
var content = [
// Header
E('div', { 'style': 'margin-bottom: 24px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Lyrion Music Server'),
E('span', { 'id': 'lyrion-status-badge' }, [
KissTheme.badge(status.running ? 'RUNNING' : 'STOPPED', status.running ? 'green' : 'red')
])
]),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'Self-hosted music streaming')
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, [
'🎵 ',
_('Lyrion Music Server'),
' ',
E('span', { 'id': 'status-badge', 'style': 'font-size:14px' },
s.running
? E('span', { 'style': 'color:#4caf50;font-weight:600' }, '● Running')
: E('span', { 'style': 'color:#f44336;font-weight:600' }, '● Stopped')
)
]),
// Stats
E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'lyrion-stats', 'style': 'margin: 20px 0;' }, this.renderStats(stats)),
// Scan progress
KissTheme.card('Library Status', E('div', { 'id': 'lyrion-scan' }, [this.renderScanStatus(stats)])),
// Two-column layout
E('div', { 'class': 'kiss-grid kiss-grid-2' }, [
KissTheme.card('Service Info', this.renderServiceInfo(status)),
KissTheme.card('Controls', this.renderControls(status))
]),
// Web UI link
status.running && status.web_accessible ? KissTheme.card('Web Interface',
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [
E('div', { 'style': 'font-size: 2rem;' }, '🌐'),
E('div', { 'style': 'flex: 1;' }, [
E('div', { 'style': 'font-weight: 600;' }, 'Lyrion Web Interface'),
E('div', { 'style': 'font-family: monospace; font-size: 12px; color: var(--kiss-purple);' }, status.web_url)
]),
E('a', {
'href': status.web_url,
'target': '_blank',
'class': 'kiss-btn kiss-btn-blue',
'style': 'text-decoration: none;'
}, 'Open')
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Controls')),
E('div', { 'class': 'cbi-value' }, [
E('button', {
'id': 'btn-start',
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, 'handleStart'),
'disabled': s.running,
'style': 'margin-right:8px'
}, _('Start')),
E('button', {
'id': 'btn-stop',
'class': 'cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, 'handleStop'),
'disabled': !s.running,
'style': 'margin-right:8px'
}, _('Stop')),
E('button', {
'id': 'btn-rescan',
'class': 'cbi-button',
'click': ui.createHandlerFn(this, 'handleRescan'),
'disabled': !s.running
}, _('Rescan Library'))
])
) : ''
];
]),
return KissTheme.wrap(content, 'admin/services/lyrion/overview');
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Service Info')),
E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'style': 'width:150px' }, _('Port')),
E('td', { 'class': 'td' }, String(s.port || 9000))
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, _('Runtime')),
E('td', { 'class': 'td' }, s.detected_runtime || 'auto')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, _('Media Path')),
E('td', { 'class': 'td' }, s.media_path || '/srv/media')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, _('Library')),
E('td', { 'class': 'td', 'id': 'library-stats' },
(s.songs || 0) + ' songs, ' + (s.albums || 0) + ' albums, ' + (s.artists || 0) + ' artists')
])
])
]),
s.running && s.web_accessible ? E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Web Interface')),
E('a', {
'href': s.web_url || ('http://192.168.255.1:' + (s.port || 9000)),
'target': '_blank',
'class': 'cbi-button cbi-button-action'
}, _('Open Lyrion Web UI'))
]) : ''
]);
},
handleSaveApply: null,

View File

@ -2,7 +2,6 @@
'require view';
'require form';
'require uci';
'require secubox/kiss-theme';
return view.extend({
load: function() {
@ -12,67 +11,28 @@ return view.extend({
render: function() {
var m, s, o;
m = new form.Map('lyrion', _('Lyrion Settings'),
_('Configure Lyrion Music Server settings. Changes require service restart to take effect.'));
m = new form.Map('lyrion', _('Lyrion Settings'));
s = m.section(form.TypedSection, 'lyrion', _('General Settings'));
s = m.section(form.TypedSection, 'lyrion');
s.anonymous = true;
s.addremove = false;
o = s.option(form.Flag, 'enabled', _('Enabled'),
_('Enable Lyrion Music Server'));
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = '0';
o.rmempty = false;
o = s.option(form.ListValue, 'runtime', _('Container Runtime'),
_('Select the container runtime to use'));
o.value('auto', _('Auto-detect (LXC preferred)'));
o.value('lxc', _('LXC Container'));
o.value('docker', _('Docker'));
o.default = 'auto';
o = s.option(form.Value, 'port', _('Web UI Port'),
_('Port for the Lyrion web interface'));
o = s.option(form.Value, 'port', _('Web UI Port'));
o.datatype = 'port';
o.default = '9000';
o.placeholder = '9000';
o = s.option(form.Value, 'data_path', _('Data Path'),
_('Path to store Lyrion configuration and cache'));
o.default = '/srv/lyrion';
o.placeholder = '/srv/lyrion';
o = s.option(form.Value, 'media_path', _('Media Path'),
_('Path to your music library'));
o = s.option(form.Value, 'media_path', _('Media Path'));
o.default = '/srv/media';
o.placeholder = '/srv/media';
o = s.option(form.Value, 'memory_limit', _('Memory Limit'),
_('Maximum memory for the container (e.g., 256M, 512M, 1G)'));
o = s.option(form.Value, 'data_path', _('Data Path'));
o.default = '/srv/lyrion';
o = s.option(form.Value, 'memory_limit', _('Memory Limit'));
o.default = '256M';
o.placeholder = '256M';
o = s.option(form.Value, 'timezone', _('Timezone'),
_('Timezone for the container'));
o.default = 'UTC';
o.placeholder = 'UTC';
o = s.option(form.Flag, 'wan_access', _('WAN Access'),
_('Also open Lyrion ports on the WAN interface (remote access)'));
o.default = '0';
o.rmempty = false;
o = s.option(form.Value, 'image', _('Docker Image'),
_('Docker image to use (only for Docker runtime)'));
o.default = 'ghcr.io/lms-community/lyrionmusicserver:stable';
o.depends('runtime', 'docker');
return m.render().then(function(node) {
return KissTheme.wrap(node, 'admin/secubox/services/lyrion/settings');
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
return m.render();
}
});

View File

@ -1,300 +1,109 @@
#!/bin/sh
# RPCD backend for Lyrion Music Server LuCI app
# Lyrion Music Server RPCD handler
. /lib/functions.sh
CONFIG="lyrion"
json_init() { echo "{"; }
json_close() { echo "}"; }
json_add_string() { echo "\"$1\": \"$2\""; }
json_add_int() { echo "\"$1\": $2"; }
json_add_bool() { [ "$2" = "1" ] && echo "\"$1\": true" || echo "\"$1\": false"; }
uci_get() { uci -q get ${CONFIG}.main.$1; }
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
# Get service status
get_status() {
local enabled=$(uci_get enabled)
local runtime=$(uci_get runtime)
local port=$(uci_get port)
local data_path=$(uci_get data_path)
local media_path=$(uci_get media_path)
local memory_limit=$(uci_get memory_limit)
local image=$(uci_get image)
port=${port:-9000}
# Check if service is running
# Check running state
local running=0
local container_status="stopped"
if command -v lxc-info >/dev/null 2>&1; then
if lxc-info -n lyrion -s 2>/dev/null | grep -q "RUNNING"; then
running=1
container_status="running"
fi
elif command -v docker >/dev/null 2>&1; then
if docker ps --filter "name=secbx-lyrion" --format "{{.Names}}" 2>/dev/null | grep -q "secbx-lyrion"; then
running=1
container_status="running"
fi
if command -v lxc-info >/dev/null 2>&1 && lxc-info -n lyrion -s 2>/dev/null | grep -q "RUNNING"; then
running=1
elif command -v docker >/dev/null 2>&1 && docker ps --filter "name=secbx-lyrion" -q 2>/dev/null | grep -q .; then
running=1
fi
# Check if installed (LXC rootfs or Docker image exists)
# Check installed
local installed=0
if [ -d "/srv/lxc/lyrion/rootfs" ] && [ -f "/srv/lxc/lyrion/rootfs/opt/lyrion/slimserver.pl" ]; then
installed=1
elif command -v docker >/dev/null 2>&1 && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "lyrionmusicserver"; then
installed=1
fi
[ -d "/srv/lxc/lyrion/rootfs" ] && installed=1
[ "$installed" = "0" ] && command -v docker >/dev/null 2>&1 && docker images 2>/dev/null | grep -q lyrionmusicserver && installed=1
# Detect runtime
local detected_runtime="none"
if command -v lxc-start >/dev/null 2>&1; then
detected_runtime="lxc"
elif command -v docker >/dev/null 2>&1; then
detected_runtime="docker"
fi
local runtime="none"
command -v lxc-start >/dev/null 2>&1 && runtime="lxc"
[ "$runtime" = "none" ] && command -v docker >/dev/null 2>&1 && runtime="docker"
# Check web UI accessibility
# Check web access
local web_accessible=0
[ "$running" = "1" ] && wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${port}/" 2>/dev/null && web_accessible=1
# Get library stats if running
local songs=0 albums=0 artists=0
if [ "$running" = "1" ]; then
wget -q -O /dev/null --timeout=2 "http://127.0.0.1:${port:-9000}/" 2>/dev/null && web_accessible=1
local resp=$(curl -s --max-time 2 "http://127.0.0.1:${port}/jsonrpc.js" \
-H "Content-Type: application/json" \
-d '{"id":1,"method":"slim.request","params":["",["serverstatus",0,0]]}' 2>/dev/null)
if [ -n "$resp" ]; then
songs=$(echo "$resp" | jsonfilter -e '@.result["info total songs"]' 2>/dev/null || echo 0)
albums=$(echo "$resp" | jsonfilter -e '@.result["info total albums"]' 2>/dev/null || echo 0)
artists=$(echo "$resp" | jsonfilter -e '@.result["info total artists"]' 2>/dev/null || echo 0)
fi
fi
cat <<EOF
{
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
"container_status": "$container_status",
"runtime": "${runtime:-auto}",
"detected_runtime": "$detected_runtime",
"port": ${port:-9000},
"data_path": "${data_path:-/srv/lyrion}",
"installed": $([ "$installed" = "1" ] && echo true || echo false),
"running": $([ "$running" = "1" ] && echo true || echo false),
"detected_runtime": "$runtime",
"port": $port,
"media_path": "${media_path:-/srv/media}",
"memory_limit": "${memory_limit:-256M}",
"image": "${image:-ghcr.io/lms-community/lyrionmusicserver:stable}",
"web_accessible": $([ "$web_accessible" = "1" ] && echo "true" || echo "false"),
"web_url": "http://192.168.255.1:${port:-9000}"
"data_path": "${data_path:-/srv/lyrion}",
"web_accessible": $([ "$web_accessible" = "1" ] && echo true || echo false),
"web_url": "http://192.168.255.1:${port}",
"songs": ${songs:-0},
"albums": ${albums:-0},
"artists": ${artists:-0}
}
EOF
}
# Get configuration
get_config() {
local enabled=$(uci_get enabled)
local runtime=$(uci_get runtime)
local port=$(uci_get port)
local data_path=$(uci_get data_path)
local media_path=$(uci_get media_path)
local memory_limit=$(uci_get memory_limit)
local timezone=$(uci_get timezone)
local image=$(uci_get image)
cat <<EOF
{
"enabled": "${enabled:-0}",
"runtime": "${runtime:-auto}",
"port": "${port:-9000}",
"data_path": "${data_path:-/srv/lyrion}",
"media_path": "${media_path:-/srv/media}",
"memory_limit": "${memory_limit:-256M}",
"timezone": "${timezone:-UTC}",
"image": "${image:-ghcr.io/lms-community/lyrionmusicserver:stable}"
}
EOF
}
# Save configuration
save_config() {
local input
read -r input
local runtime=$(echo "$input" | jsonfilter -e '@.runtime' 2>/dev/null)
local port=$(echo "$input" | jsonfilter -e '@.port' 2>/dev/null)
local data_path=$(echo "$input" | jsonfilter -e '@.data_path' 2>/dev/null)
local media_path=$(echo "$input" | jsonfilter -e '@.media_path' 2>/dev/null)
local memory_limit=$(echo "$input" | jsonfilter -e '@.memory_limit' 2>/dev/null)
local timezone=$(echo "$input" | jsonfilter -e '@.timezone' 2>/dev/null)
[ -n "$runtime" ] && uci_set runtime "$runtime"
[ -n "$port" ] && uci_set port "$port"
[ -n "$data_path" ] && uci_set data_path "$data_path"
[ -n "$media_path" ] && uci_set media_path "$media_path"
[ -n "$memory_limit" ] && uci_set memory_limit "$memory_limit"
[ -n "$timezone" ] && uci_set timezone "$timezone"
echo '{"success": true}'
}
# Install Lyrion
do_install() {
if command -v lyrionctl >/dev/null 2>&1; then
lyrionctl install >/tmp/lyrion-install.log 2>&1 &
echo '{"success": true, "message": "Installation started in background"}'
echo '{"success":true}'
else
echo '{"success": false, "error": "lyrionctl not found"}'
echo '{"success":false,"error":"lyrionctl not found"}'
fi
}
# Start service
do_start() {
if [ -x /etc/init.d/lyrion ]; then
/etc/init.d/lyrion start >/dev/null 2>&1
uci_set enabled '1'
echo '{"success": true}'
else
echo '{"success": false, "error": "Service not installed"}'
fi
[ -x /etc/init.d/lyrion ] && /etc/init.d/lyrion start >/dev/null 2>&1
echo '{"success":true}'
}
# Stop service
do_stop() {
if [ -x /etc/init.d/lyrion ]; then
/etc/init.d/lyrion stop >/dev/null 2>&1
echo '{"success": true}'
else
echo '{"success": false, "error": "Service not installed"}'
fi
[ -x /etc/init.d/lyrion ] && /etc/init.d/lyrion stop >/dev/null 2>&1
echo '{"success":true}'
}
# Restart service
do_restart() {
if [ -x /etc/init.d/lyrion ]; then
/etc/init.d/lyrion restart >/dev/null 2>&1
echo '{"success": true}'
else
echo '{"success": false, "error": "Service not installed"}'
fi
}
# Update container
do_update() {
if command -v lyrionctl >/dev/null 2>&1; then
lyrionctl update >/tmp/lyrion-update.log 2>&1 &
echo '{"success": true, "message": "Update started in background"}'
else
echo '{"success": false, "error": "lyrionctl not found"}'
fi
}
# Get logs
get_logs() {
local lines=50
local log_content=""
if [ -f /srv/lxc/lyrion/rootfs/var/log/lyrion/server.log ]; then
log_content=$(tail -n $lines /srv/lxc/lyrion/rootfs/var/log/lyrion/server.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
elif [ -f /tmp/lyrion-install.log ]; then
log_content=$(tail -n $lines /tmp/lyrion-install.log 2>/dev/null | sed 's/"/\\"/g' | tr '\n' '|')
fi
echo "{\"logs\": \"$log_content\"}"
}
# Get library stats from Lyrion API
get_library_stats() {
local port=$(uci_get port)
port=${port:-9000}
# Query Lyrion JSON-RPC API for server status
local response=$(curl -s --max-time 3 "http://127.0.0.1:${port}/jsonrpc.js" \
-H "Content-Type: application/json" \
-d '{"id":1,"method":"slim.request","params":["", ["serverstatus", 0, 0]]}' 2>/dev/null)
if [ -z "$response" ]; then
echo '{"songs":0,"albums":0,"artists":0,"genres":0,"scanning":false,"scan_progress":0,"scan_total":0,"scan_phase":""}'
return
fi
# Parse response using jsonfilter
local songs=$(echo "$response" | jsonfilter -e '@.result["info total songs"]' 2>/dev/null || echo 0)
local albums=$(echo "$response" | jsonfilter -e '@.result["info total albums"]' 2>/dev/null || echo 0)
local artists=$(echo "$response" | jsonfilter -e '@.result["info total artists"]' 2>/dev/null || echo 0)
local genres=$(echo "$response" | jsonfilter -e '@.result["info total genres"]' 2>/dev/null || echo 0)
local rescan=$(echo "$response" | jsonfilter -e '@.result.rescan' 2>/dev/null || echo 0)
local progress_done=$(echo "$response" | jsonfilter -e '@.result.progressdone' 2>/dev/null || echo 0)
local progress_total=$(echo "$response" | jsonfilter -e '@.result.progresstotal' 2>/dev/null || echo 0)
local progress_name=$(echo "$response" | jsonfilter -e '@.result.progressname' 2>/dev/null || echo "")
# Get database size
local db_size="0"
if [ -f /srv/lyrion/cache/library.db ]; then
db_size=$(ls -lh /srv/lyrion/cache/library.db 2>/dev/null | awk '{print $5}')
fi
local scanning="false"
[ "$rescan" = "1" ] && scanning="true"
cat <<EOF
{
"songs": ${songs:-0},
"albums": ${albums:-0},
"artists": ${artists:-0},
"genres": ${genres:-0},
"scanning": $scanning,
"scan_progress": ${progress_done:-0},
"scan_total": ${progress_total:-0},
"scan_phase": "$progress_name",
"db_size": "$db_size"
}
EOF
}
# Trigger library rescan
do_rescan() {
local port=$(uci_get port)
port=${port:-9000}
curl -s --max-time 5 "http://127.0.0.1:${port}/jsonrpc.js" \
curl -s --max-time 3 "http://127.0.0.1:${port:-9000}/jsonrpc.js" \
-H "Content-Type: application/json" \
-d '{"id":1,"method":"slim.request","params":["", ["rescan", "full"]]}' >/dev/null 2>&1
echo '{"success": true, "message": "Rescan started"}'
-d '{"id":1,"method":"slim.request","params":["",["rescan","full"]]}' >/dev/null 2>&1
echo '{"success":true}'
}
# RPCD list method
list_methods() {
cat <<'EOF'
{
"status": {},
"get_config": {},
"save_config": {"runtime": "string", "port": "string", "data_path": "string", "media_path": "string", "memory_limit": "string", "timezone": "string"},
"install": {},
"start": {},
"stop": {},
"restart": {},
"update": {},
"logs": {},
"get_library_stats": {},
"rescan": {}
}
EOF
}
# Main entry point
case "$1" in
list)
list_methods
echo '{"status":{},"install":{},"start":{},"stop":{},"rescan":{}}'
;;
call)
case "$2" in
status) get_status ;;
get_config) get_config ;;
save_config) save_config ;;
install) do_install ;;
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
update) do_update ;;
logs) get_logs ;;
get_library_stats) get_library_stats ;;
rescan) do_rescan ;;
*) echo '{"error": "Unknown method"}' ;;
status) get_status ;;
install) do_install ;;
start) do_start ;;
stop) do_stop ;;
rescan) do_rescan ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
*)
echo '{"error": "Unknown command"}'
;;
esac

View File

@ -3,13 +3,13 @@
"description": "Grant access to Lyrion Music Server",
"read": {
"ubus": {
"luci.lyrion": ["status", "get_config", "logs", "get_library_stats"]
"luci.lyrion": ["status"]
},
"uci": ["lyrion"]
},
"write": {
"ubus": {
"luci.lyrion": ["install", "start", "stop", "restart", "update", "save_config", "rescan"]
"luci.lyrion": ["install", "start", "stop", "rescan"]
},
"uci": ["lyrion"]
}