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:
parent
fd54253f66
commit
58ba852564
@ -3,8 +3,7 @@
|
||||
"title": "DPI Dual-Stream",
|
||||
"order": 45,
|
||||
"action": {
|
||||
"type": "firstchildview",
|
||||
"recurse": true
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-dpi-dual"],
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user