secubox-openwrt/package/secubox/luci-app-webradio/htdocs/luci-static/resources/view/webradio/overview.js
CyberMind-FR 418e99e481 feat(webradio): Add luci-app-webradio LuCI interface
Complete WebRadio management interface for OpenWrt:
- Dashboard with server status, listeners, now playing
- Icecast/Ezstream server configuration
- Playlist management with shuffle/upload
- Programming grid scheduler with jingle support
- Live audio input via DarkIce (ALSA)
- Security: SSL/TLS, rate limiting, CrowdSec integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-17 14:25:31 +01:00

250 lines
6.8 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require poll';
'require ui';
'require form';
var callStatus = rpc.declare({
object: 'luci.webradio',
method: 'status',
expect: {}
});
var callStart = rpc.declare({
object: 'luci.webradio',
method: 'start',
params: ['service'],
expect: {}
});
var callStop = rpc.declare({
object: 'luci.webradio',
method: 'stop',
params: ['service'],
expect: {}
});
var callSkip = rpc.declare({
object: 'luci.webradio',
method: 'skip',
expect: {}
});
var callGeneratePlaylist = rpc.declare({
object: 'luci.webradio',
method: 'generate_playlist',
params: ['shuffle'],
expect: {}
});
var callCurrentShow = rpc.declare({
object: 'luci.webradio',
method: 'current_show',
expect: {}
});
return view.extend({
load: function() {
return Promise.all([
callStatus(),
callCurrentShow()
]);
},
render: function(data) {
var self = this;
var status = data[0] || {};
var currentShow = data[1] || {};
poll.add(function() {
return Promise.all([callStatus(), callCurrentShow()]).then(function(res) {
self.updateStatus(res[0], res[1]);
});
}, 5);
var icecast = status.icecast || {};
var ezstream = status.ezstream || {};
var stream = status.stream || {};
var playlist = status.playlist || {};
var showName = currentShow.name || 'Default';
var content = [
E('h2', {}, 'WebRadio'),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Status'),
E('div', { 'class': 'table', 'id': 'status-table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Icecast Server'),
E('div', { 'class': 'td', 'id': 'icecast-status' },
this.statusBadge(icecast.running))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Ezstream Source'),
E('div', { 'class': 'td', 'id': 'ezstream-status' },
this.statusBadge(ezstream.running))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Listeners'),
E('div', { 'class': 'td', 'id': 'listeners' },
String(stream.listeners || 0))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Current Show'),
E('div', { 'class': 'td', 'id': 'current-show' }, [
E('span', { 'style': 'font-weight: bold;' }, showName),
currentShow.playlist ? E('span', { 'style': 'color: #666; margin-left: 10px;' },
'(' + currentShow.playlist + ')') : ''
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Now Playing'),
E('div', { 'class': 'td', 'id': 'current-song' },
stream.current_song || 'Nothing')
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Playlist'),
E('div', { 'class': 'td', 'id': 'playlist-info' },
playlist.tracks + ' tracks' + (playlist.shuffle ? ' (shuffle)' : ''))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Stream URL'),
E('div', { 'class': 'td' },
E('a', { 'href': status.url, 'target': '_blank' }, status.url || 'N/A'))
])
])
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Controls'),
E('div', { 'style': 'display: flex; gap: 10px; flex-wrap: wrap;' }, [
E('button', {
'class': 'btn cbi-button-positive',
'click': ui.createHandlerFn(this, 'handleStart')
}, 'Start'),
E('button', {
'class': 'btn cbi-button-negative',
'click': ui.createHandlerFn(this, 'handleStop')
}, 'Stop'),
E('button', {
'class': 'btn cbi-button-action',
'click': ui.createHandlerFn(this, 'handleSkip')
}, 'Skip Track'),
E('button', {
'class': 'btn cbi-button-neutral',
'click': ui.createHandlerFn(this, 'handleRegenerate')
}, 'Regenerate Playlist')
])
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Listen'),
E('audio', {
'id': 'radio-player',
'controls': true,
'style': 'width: 100%; max-width: 500px;'
}, [
E('source', { 'src': status.url, 'type': 'audio/mpeg' })
]),
E('p', { 'style': 'color: #666; font-size: 0.9em;' },
'Click play to listen to the stream')
])
];
return E('div', { 'class': 'cbi-map' }, content);
},
statusBadge: function(running) {
if (running) {
return E('span', {
'style': 'color: #fff; background: #5cb85c; padding: 2px 8px; border-radius: 3px;'
}, 'Running');
} else {
return E('span', {
'style': 'color: #fff; background: #d9534f; padding: 2px 8px; border-radius: 3px;'
}, 'Stopped');
}
},
updateStatus: function(status, currentShow) {
var icecast = status.icecast || {};
var ezstream = status.ezstream || {};
var stream = status.stream || {};
var playlist = status.playlist || {};
currentShow = currentShow || {};
var icecastEl = document.getElementById('icecast-status');
var ezstreamEl = document.getElementById('ezstream-status');
var listenersEl = document.getElementById('listeners');
var songEl = document.getElementById('current-song');
var playlistEl = document.getElementById('playlist-info');
var showEl = document.getElementById('current-show');
if (icecastEl) {
icecastEl.innerHTML = '';
icecastEl.appendChild(this.statusBadge(icecast.running));
}
if (ezstreamEl) {
ezstreamEl.innerHTML = '';
ezstreamEl.appendChild(this.statusBadge(ezstream.running));
}
if (listenersEl) {
listenersEl.textContent = String(stream.listeners || 0);
}
if (songEl) {
songEl.textContent = stream.current_song || 'Nothing';
}
if (playlistEl) {
playlistEl.textContent = playlist.tracks + ' tracks' + (playlist.shuffle ? ' (shuffle)' : '');
}
if (showEl) {
var showText = currentShow.name || 'Default';
if (currentShow.playlist) {
showText += ' (' + currentShow.playlist + ')';
}
showEl.textContent = showText;
}
},
handleStart: function() {
return callStart('all').then(function(res) {
ui.addNotification(null, E('p', 'WebRadio started'));
}).catch(function(e) {
ui.addNotification(null, E('p', 'Failed to start: ' + e.message), 'error');
});
},
handleStop: function() {
return callStop('all').then(function(res) {
ui.addNotification(null, E('p', 'WebRadio stopped'));
}).catch(function(e) {
ui.addNotification(null, E('p', 'Failed to stop: ' + e.message), 'error');
});
},
handleSkip: function() {
return callSkip().then(function(res) {
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'Skipping to next track...'));
} else {
ui.addNotification(null, E('p', 'Skip failed: ' + (res.error || 'unknown')), 'warning');
}
});
},
handleRegenerate: function() {
return callGeneratePlaylist(true).then(function(res) {
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'Playlist regenerated: ' + res.tracks + ' tracks'));
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
}
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});