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", "title": "DPI Dual-Stream",
"order": 45, "order": 45,
"action": { "action": {
"type": "firstchildview", "type": "firstchild"
"recurse": true
}, },
"depends": { "depends": {
"acl": ["luci-app-dpi-dual"], "acl": ["luci-app-dpi-dual"],

View File

@ -3,271 +3,176 @@
'require ui'; 'require ui';
'require rpc'; 'require rpc';
'require poll'; 'require poll';
'require secubox/kiss-theme';
var callStatus = rpc.declare({ object: 'luci.lyrion', method: 'status', expect: {} }); var callStatus = rpc.declare({ object: 'luci.lyrion', method: 'status' });
var callLibraryStats = rpc.declare({ object: 'luci.lyrion', method: 'get_library_stats', expect: {} }); var callInstall = rpc.declare({ object: 'luci.lyrion', method: 'install' });
var callInstall = rpc.declare({ object: 'luci.lyrion', method: 'install', expect: {} }); var callStart = rpc.declare({ object: 'luci.lyrion', method: 'start' });
var callStart = rpc.declare({ object: 'luci.lyrion', method: 'start', expect: {} }); var callStop = rpc.declare({ object: 'luci.lyrion', method: 'stop' });
var callStop = rpc.declare({ object: 'luci.lyrion', method: 'stop', expect: {} }); var callRescan = rpc.declare({ object: 'luci.lyrion', method: 'rescan' });
var callRestart = rpc.declare({ object: 'luci.lyrion', method: 'restart', expect: {} });
var callRescan = rpc.declare({ object: 'luci.lyrion', method: 'rescan', expect: {} });
return view.extend({ return view.extend({
pollActive: true,
libraryStats: null,
load: function() { load: function() {
return Promise.all([callStatus(), callLibraryStats()]); return callStatus().catch(function() { return {}; });
}, },
startPolling: function() { startPolling: function() {
var self = this; var self = this;
this.pollActive = true; poll.add(function() {
poll.add(L.bind(function() { return callStatus().then(function(s) {
if (!this.pollActive) return Promise.resolve(); self.updateUI(s);
return Promise.all([callStatus(), callLibraryStats()]).then(L.bind(function(results) { });
this.updateStatus(results[0]); }, 5);
this.updateLibraryStats(results[1]);
}, this));
}, this), 3);
}, },
updateStatus: function(status) { updateUI: function(s) {
var badge = document.getElementById('lyrion-status-badge'); var badge = document.getElementById('status-badge');
if (badge) { if (badge) {
badge.innerHTML = ''; badge.className = 'cbi-value-field';
badge.appendChild(KissTheme.badge(status.running ? 'RUNNING' : 'STOPPED', status.running ? 'green' : 'red')); badge.innerHTML = s.running
} ? '<span style="color:#4caf50;font-weight:600">● Running</span>'
}, : '<span style="color:#f44336;font-weight:600">● Stopped</span>';
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); });
} }
var scanEl = document.getElementById('lyrion-scan'); var stats = document.getElementById('library-stats');
if (scanEl) { if (stats && s.songs !== undefined) {
scanEl.innerHTML = ''; stats.textContent = s.songs + ' songs, ' + s.albums + ' albums, ' + s.artists + ' artists';
scanEl.appendChild(this.renderScanStatus(stats));
} }
},
formatNumber: function(n) { ['btn-start', 'btn-rescan'].forEach(function(id) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; var el = document.getElementById(id);
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; if (el) el.disabled = s.running;
return n.toString(); });
}, var stopBtn = document.getElementById('btn-stop');
if (stopBtn) stopBtn.disabled = !s.running;
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)
]);
})
);
}, },
handleInstall: function() { handleInstall: function() {
var self = this; ui.showModal(_('Installing'), [
ui.showModal('Installing Lyrion', [ E('p', { 'class': 'spinning' }, _('Installing Lyrion Music Server...'))
E('p', { 'class': 'spinning' }, 'Installing Lyrion Music Server. This may take several minutes...')
]); ]);
callInstall().then(function(r) { callInstall().then(function(r) {
ui.hideModal(); ui.hideModal();
if (r.success) { if (r.success) {
ui.addNotification(null, E('p', r.message || 'Installation started')); ui.addNotification(null, E('p', _('Installation started')));
self.startPolling(); setTimeout(function() { location.reload(); }, 3000);
window.location.reload();
} else { } else {
ui.addNotification(null, E('p', 'Failed: ' + (r.error || 'Unknown error')), 'error'); ui.addNotification(null, E('p', r.error || _('Installation failed')), 'error');
} }
}); });
}, },
handleStart: function() { handleStart: function() {
ui.showModal('Starting...', [E('p', { 'class': 'spinning' }, 'Starting Lyrion...')]); callStart().then(function() {
callStart().then(function(r) { ui.addNotification(null, E('p', _('Lyrion started')));
ui.hideModal();
if (r.success) ui.addNotification(null, E('p', 'Lyrion started'));
}); });
}, },
handleStop: function() { handleStop: function() {
ui.showModal('Stopping...', [E('p', { 'class': 'spinning' }, 'Stopping Lyrion...')]); callStop().then(function() {
callStop().then(function(r) { ui.addNotification(null, E('p', _('Lyrion stopped')));
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'));
}); });
}, },
handleRescan: function() { handleRescan: function() {
callRescan().then(function(r) { callRescan().then(function() {
if (r.success) ui.addNotification(null, E('p', 'Library rescan started')); ui.addNotification(null, E('p', _('Library rescan started')));
}); });
}, },
render: function(data) { render: function(status) {
var status = data[0] || {}; var s = status || {};
var stats = data[1] || {};
this.libraryStats = stats;
// Not installed view // Not installed
if (!status.installed) { if (!s.installed) {
var notInstalledContent = [ return E('div', { 'class': 'cbi-map' }, [
E('div', { 'style': 'margin-bottom: 24px;' }, [ E('h2', {}, _('Lyrion Music Server')),
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [ E('div', { 'class': 'cbi-section' }, [
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Lyrion Music Server'), E('div', { 'style': 'text-align:center;padding:40px' }, [
KissTheme.badge('NOT INSTALLED', 'red') E('p', { 'style': 'font-size:48px;margin:0' }, '🎵'),
]), E('h3', {}, _('Lyrion Music Server')),
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'Self-hosted music streaming with Squeezebox compatibility') E('p', {}, _('Self-hosted music streaming with Squeezebox compatibility.')),
]), E('button', {
'class': 'cbi-button cbi-button-positive',
KissTheme.card('Install', E('div', { 'style': 'text-align: center; padding: 40px;' }, [ 'click': ui.createHandlerFn(this, 'handleInstall')
E('div', { 'style': 'font-size: 4rem; margin-bottom: 16px;' }, '🎵'), }, _('Install Lyrion'))
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');
} }
// Installed view // Installed
this.startPolling(); this.startPolling();
var content = [ return E('div', { 'class': 'cbi-map' }, [
// Header E('h2', {}, [
E('div', { 'style': 'margin-bottom: 24px;' }, [ '🎵 ',
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [ _('Lyrion Music Server'),
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, 'Lyrion Music Server'), ' ',
E('span', { 'id': 'lyrion-status-badge' }, [ E('span', { 'id': 'status-badge', 'style': 'font-size:14px' },
KissTheme.badge(status.running ? 'RUNNING' : 'STOPPED', status.running ? 'green' : 'red') s.running
]) ? E('span', { 'style': 'color:#4caf50;font-weight:600' }, '● Running')
]), : E('span', { 'style': 'color:#f44336;font-weight:600' }, '● Stopped')
E('p', { 'style': 'color: var(--kiss-muted); margin: 8px 0 0 0;' }, 'Self-hosted music streaming') )
]), ]),
// Stats E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'kiss-grid kiss-grid-4', 'id': 'lyrion-stats', 'style': 'margin: 20px 0;' }, this.renderStats(stats)), E('h3', {}, _('Controls')),
E('div', { 'class': 'cbi-value' }, [
// Scan progress E('button', {
KissTheme.card('Library Status', E('div', { 'id': 'lyrion-scan' }, [this.renderScanStatus(stats)])), 'id': 'btn-start',
'class': 'cbi-button cbi-button-positive',
// Two-column layout 'click': ui.createHandlerFn(this, 'handleStart'),
E('div', { 'class': 'kiss-grid kiss-grid-2' }, [ 'disabled': s.running,
KissTheme.card('Service Info', this.renderServiceInfo(status)), 'style': 'margin-right:8px'
KissTheme.card('Controls', this.renderControls(status)) }, _('Start')),
]), E('button', {
'id': 'btn-stop',
// Web UI link 'class': 'cbi-button cbi-button-negative',
status.running && status.web_accessible ? KissTheme.card('Web Interface', 'click': ui.createHandlerFn(this, 'handleStop'),
E('div', { 'style': 'display: flex; align-items: center; gap: 16px;' }, [ 'disabled': !s.running,
E('div', { 'style': 'font-size: 2rem;' }, '🌐'), 'style': 'margin-right:8px'
E('div', { 'style': 'flex: 1;' }, [ }, _('Stop')),
E('div', { 'style': 'font-weight: 600;' }, 'Lyrion Web Interface'), E('button', {
E('div', { 'style': 'font-family: monospace; font-size: 12px; color: var(--kiss-purple);' }, status.web_url) 'id': 'btn-rescan',
]), 'class': 'cbi-button',
E('a', { 'click': ui.createHandlerFn(this, 'handleRescan'),
'href': status.web_url, 'disabled': !s.running
'target': '_blank', }, _('Rescan Library'))
'class': 'kiss-btn kiss-btn-blue',
'style': 'text-decoration: none;'
}, 'Open')
]) ])
) : '' ]),
];
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, handleSaveApply: null,

View File

@ -2,7 +2,6 @@
'require view'; 'require view';
'require form'; 'require form';
'require uci'; 'require uci';
'require secubox/kiss-theme';
return view.extend({ return view.extend({
load: function() { load: function() {
@ -12,67 +11,28 @@ return view.extend({
render: function() { render: function() {
var m, s, o; var m, s, o;
m = new form.Map('lyrion', _('Lyrion Settings'), m = new form.Map('lyrion', _('Lyrion Settings'));
_('Configure Lyrion Music Server settings. Changes require service restart to take effect.'));
s = m.section(form.TypedSection, 'lyrion', _('General Settings')); s = m.section(form.TypedSection, 'lyrion');
s.anonymous = true; s.anonymous = true;
s.addremove = false; s.addremove = false;
o = s.option(form.Flag, 'enabled', _('Enabled'), o = s.option(form.Flag, 'enabled', _('Enabled'));
_('Enable Lyrion Music Server'));
o.default = '0'; o.default = '0';
o.rmempty = false;
o = s.option(form.ListValue, 'runtime', _('Container Runtime'), o = s.option(form.Value, 'port', _('Web UI Port'));
_('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.datatype = 'port'; o.datatype = 'port';
o.default = '9000'; o.default = '9000';
o.placeholder = '9000';
o = s.option(form.Value, 'data_path', _('Data Path'), o = s.option(form.Value, 'media_path', _('Media 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.default = '/srv/media'; o.default = '/srv/media';
o.placeholder = '/srv/media';
o = s.option(form.Value, 'memory_limit', _('Memory Limit'), o = s.option(form.Value, 'data_path', _('Data Path'));
_('Maximum memory for the container (e.g., 256M, 512M, 1G)')); o.default = '/srv/lyrion';
o = s.option(form.Value, 'memory_limit', _('Memory Limit'));
o.default = '256M'; o.default = '256M';
o.placeholder = '256M';
o = s.option(form.Value, 'timezone', _('Timezone'), return m.render();
_('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
}); });

View File

@ -1,300 +1,109 @@
#!/bin/sh #!/bin/sh
# RPCD backend for Lyrion Music Server LuCI app # Lyrion Music Server RPCD handler
. /lib/functions.sh . /lib/functions.sh
CONFIG="lyrion" 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_get() { uci -q get ${CONFIG}.main.$1; }
uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; }
# Get service status
get_status() { get_status() {
local enabled=$(uci_get enabled)
local runtime=$(uci_get runtime)
local port=$(uci_get port) local port=$(uci_get port)
local data_path=$(uci_get data_path) local data_path=$(uci_get data_path)
local media_path=$(uci_get media_path) local media_path=$(uci_get media_path)
local memory_limit=$(uci_get memory_limit) port=${port:-9000}
local image=$(uci_get image)
# Check if service is running # Check running state
local running=0 local running=0
local container_status="stopped" if command -v lxc-info >/dev/null 2>&1 && lxc-info -n lyrion -s 2>/dev/null | grep -q "RUNNING"; then
running=1
if command -v lxc-info >/dev/null 2>&1; then elif command -v docker >/dev/null 2>&1 && docker ps --filter "name=secbx-lyrion" -q 2>/dev/null | grep -q .; then
if lxc-info -n lyrion -s 2>/dev/null | grep -q "RUNNING"; then running=1
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
fi fi
# Check if installed (LXC rootfs or Docker image exists) # Check installed
local installed=0 local installed=0
if [ -d "/srv/lxc/lyrion/rootfs" ] && [ -f "/srv/lxc/lyrion/rootfs/opt/lyrion/slimserver.pl" ]; then [ -d "/srv/lxc/lyrion/rootfs" ] && installed=1
installed=1 [ "$installed" = "0" ] && command -v docker >/dev/null 2>&1 && docker images 2>/dev/null | grep -q lyrionmusicserver && installed=1
elif command -v docker >/dev/null 2>&1 && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "lyrionmusicserver"; then
installed=1
fi
# Detect runtime # Detect runtime
local detected_runtime="none" local runtime="none"
if command -v lxc-start >/dev/null 2>&1; then command -v lxc-start >/dev/null 2>&1 && runtime="lxc"
detected_runtime="lxc" [ "$runtime" = "none" ] && command -v docker >/dev/null 2>&1 && runtime="docker"
elif command -v docker >/dev/null 2>&1; then
detected_runtime="docker"
fi
# Check web UI accessibility # Check web access
local web_accessible=0 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 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 fi
cat <<EOF cat <<EOF
{ {
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"), "installed": $([ "$installed" = "1" ] && echo true || echo false),
"running": $([ "$running" = "1" ] && echo "true" || echo "false"), "running": $([ "$running" = "1" ] && echo true || echo false),
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"), "detected_runtime": "$runtime",
"container_status": "$container_status", "port": $port,
"runtime": "${runtime:-auto}",
"detected_runtime": "$detected_runtime",
"port": ${port:-9000},
"data_path": "${data_path:-/srv/lyrion}",
"media_path": "${media_path:-/srv/media}", "media_path": "${media_path:-/srv/media}",
"memory_limit": "${memory_limit:-256M}", "data_path": "${data_path:-/srv/lyrion}",
"image": "${image:-ghcr.io/lms-community/lyrionmusicserver:stable}", "web_accessible": $([ "$web_accessible" = "1" ] && echo true || echo false),
"web_accessible": $([ "$web_accessible" = "1" ] && echo "true" || echo "false"), "web_url": "http://192.168.255.1:${port}",
"web_url": "http://192.168.255.1:${port:-9000}" "songs": ${songs:-0},
"albums": ${albums:-0},
"artists": ${artists:-0}
} }
EOF 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() { do_install() {
if command -v lyrionctl >/dev/null 2>&1; then if command -v lyrionctl >/dev/null 2>&1; then
lyrionctl install >/tmp/lyrion-install.log 2>&1 & lyrionctl install >/tmp/lyrion-install.log 2>&1 &
echo '{"success": true, "message": "Installation started in background"}' echo '{"success":true}'
else else
echo '{"success": false, "error": "lyrionctl not found"}' echo '{"success":false,"error":"lyrionctl not found"}'
fi fi
} }
# Start service
do_start() { do_start() {
if [ -x /etc/init.d/lyrion ]; then [ -x /etc/init.d/lyrion ] && /etc/init.d/lyrion start >/dev/null 2>&1
/etc/init.d/lyrion start >/dev/null 2>&1 echo '{"success":true}'
uci_set enabled '1'
echo '{"success": true}'
else
echo '{"success": false, "error": "Service not installed"}'
fi
} }
# Stop service
do_stop() { do_stop() {
if [ -x /etc/init.d/lyrion ]; then [ -x /etc/init.d/lyrion ] && /etc/init.d/lyrion stop >/dev/null 2>&1
/etc/init.d/lyrion stop >/dev/null 2>&1 echo '{"success":true}'
echo '{"success": true}'
else
echo '{"success": false, "error": "Service not installed"}'
fi
} }
# 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() { do_rescan() {
local port=$(uci_get port) local port=$(uci_get port)
port=${port:-9000} curl -s --max-time 3 "http://127.0.0.1:${port:-9000}/jsonrpc.js" \
curl -s --max-time 5 "http://127.0.0.1:${port}/jsonrpc.js" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"id":1,"method":"slim.request","params":["", ["rescan", "full"]]}' >/dev/null 2>&1 -d '{"id":1,"method":"slim.request","params":["",["rescan","full"]]}' >/dev/null 2>&1
echo '{"success":true}'
echo '{"success": true, "message": "Rescan started"}'
} }
# 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 case "$1" in
list) list)
list_methods echo '{"status":{},"install":{},"start":{},"stop":{},"rescan":{}}'
;; ;;
call) call)
case "$2" in case "$2" in
status) get_status ;; status) get_status ;;
get_config) get_config ;; install) do_install ;;
save_config) save_config ;; start) do_start ;;
install) do_install ;; stop) do_stop ;;
start) do_start ;; rescan) do_rescan ;;
stop) do_stop ;; *) echo '{"error":"Unknown method"}' ;;
restart) do_restart ;;
update) do_update ;;
logs) get_logs ;;
get_library_stats) get_library_stats ;;
rescan) do_rescan ;;
*) echo '{"error": "Unknown method"}' ;;
esac esac
;; ;;
*)
echo '{"error": "Unknown command"}'
;;
esac esac

View File

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