secubox-openwrt/package/secubox/luci-app-webradio/htdocs/luci-static/resources/view/webradio/lyrion.js
CyberMind-FR 6db547f7f8 feat: Add WebRadio, TURN server, and Lyrion streaming integration
New packages:
- luci-app-webradio: Web radio management with Lyrion bridge tab
- luci-app-turn: TURN/STUN server UI for WebRTC (Jitsi integration)
- secubox-app-lyrion-bridge: Lyrion → Squeezelite → FFmpeg → Icecast pipeline
- secubox-app-squeezelite: Squeezelite audio player with FIFO output
- secubox-app-turn: TURN server with ACME SSL and Jitsi setup
- secubox-app-webradio: Icecast/ezstream web radio server

Features:
- HTTPS streaming via HAProxy (stream.gk2.secubox.in)
- Lyrion Music Server bridge for streaming playlists to Icecast
- TURN server with time-limited credential generation
- CrowdSec integration for WebRadio security
- Schedule-based radio programming with jingles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 17:46:54 +01:00

196 lines
8.5 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require poll';
'require ui';
var callBridgeStatus = rpc.declare({ object: 'luci.webradio', method: 'bridge_status', expect: {} });
var callBridgeStart = rpc.declare({ object: 'luci.webradio', method: 'bridge_start', expect: {} });
var callBridgeStop = rpc.declare({ object: 'luci.webradio', method: 'bridge_stop', expect: {} });
var callBridgeSetup = rpc.declare({ object: 'luci.webradio', method: 'bridge_setup', params: ['lyrion_server'], expect: {} });
return view.extend({
data: {},
load: function() {
return callBridgeStatus().then(function(r) { this.data = r; return r; }.bind(this)).catch(function() { return {}; });
},
render: function(data) {
var self = this;
this.data = data || {};
poll.add(function() {
return callBridgeStatus().then(function(r) {
self.data = r;
self.updateUI(r);
}).catch(function() {});
}, 5);
return E('div', { 'class': 'cbi-map' }, [
E('style', {}, this.getStyles()),
E('div', { 'class': 'sb-header' }, [
E('h2', {}, 'Lyrion Stream Bridge'),
E('div', { 'class': 'sb-chips' }, [
E('span', { 'class': 'chip', 'id': 'chip-bridge' }, this.data.bridge_running ? 'Bridge Running' : 'Bridge Stopped'),
E('span', { 'class': 'chip', 'id': 'chip-lyrion' }, this.data.lyrion_online ? 'Lyrion Online' : 'Lyrion Offline')
])
]),
E('div', { 'class': 'sb-section' }, [
E('h3', {}, 'Architecture'),
E('div', { 'class': 'pipeline' }, [
E('span', { 'class': 'pipe-node' }, 'Lyrion Server'),
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
E('span', { 'class': 'pipe-node' }, 'Squeezelite'),
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
E('span', { 'class': 'pipe-node' }, 'FIFO'),
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
E('span', { 'class': 'pipe-node' }, 'FFmpeg'),
E('span', { 'class': 'pipe-arrow' }, '\u2192'),
E('span', { 'class': 'pipe-node' }, 'Icecast')
])
]),
E('div', { 'class': 'sb-section' }, [
E('h3', {}, 'Status'),
E('div', { 'class': 'sb-grid' }, [
this.renderCard('Lyrion', this.data.lyrion_online ? 'Online' : 'Offline', this.data.lyrion_online ? 'success' : 'danger', 'lyrion-card'),
this.renderCard('Squeezelite', this.data.squeezelite_running ? 'Running' : 'Stopped', this.data.squeezelite_running ? 'success' : 'warning', 'squeeze-card'),
this.renderCard('FFmpeg', this.data.ffmpeg_running ? 'Encoding' : 'Idle', this.data.ffmpeg_running ? 'success' : 'warning', 'ffmpeg-card'),
this.renderCard('Icecast Mount', this.data.mount_active ? 'Active' : 'Inactive', this.data.mount_active ? 'success' : 'warning', 'mount-card')
])
]),
E('div', { 'class': 'sb-section' }, [
E('h3', {}, 'Now Playing'),
E('div', { 'class': 'now-playing', 'id': 'now-playing' }, [
E('span', { 'class': 'np-title' }, this.data.title || 'Nothing playing'),
this.data.artist ? E('span', { 'class': 'np-artist' }, this.data.artist) : ''
]),
E('div', { 'style': 'margin-top:10px;' }, [
E('span', {}, 'Listeners: '),
E('strong', { 'id': 'listeners' }, String(this.data.listeners || 0))
])
]),
E('div', { 'class': 'sb-section' }, [
E('h3', {}, 'Quick Setup'),
E('div', { 'class': 'form-row' }, [
E('input', { 'type': 'text', 'id': 'lyrion-server', 'placeholder': 'Lyrion IP (e.g. 127.0.0.1)', 'value': this.data.lyrion_server || '', 'class': 'sb-input' }),
E('button', { 'class': 'sb-btn sb-btn-primary', 'click': ui.createHandlerFn(this, 'handleSetup') }, 'Setup Pipeline')
]),
E('p', { 'style': 'color:#888; font-size:0.9em; margin-top:10px;' },
'This will configure Squeezelite FIFO output, FFmpeg encoder, and Icecast mount.')
]),
E('div', { 'class': 'sb-section' }, [
E('h3', {}, 'Bridge Control'),
E('div', { 'class': 'btn-row' }, [
E('button', { 'class': 'sb-btn sb-btn-success', 'click': ui.createHandlerFn(this, 'handleStart') }, 'Start Bridge'),
E('button', { 'class': 'sb-btn sb-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop') }, 'Stop Bridge')
])
]),
E('div', { 'class': 'sb-section' }, [
E('h3', {}, 'Stream URL'),
E('div', { 'class': 'stream-url' }, [
E('a', { 'href': this.data.stream_url || '#', 'target': '_blank', 'id': 'stream-url-link' },
this.data.stream_url || 'http://127.0.0.1:8000/lyrion')
]),
E('audio', { 'controls': true, 'style': 'width:100%; max-width:400px; margin-top:10px;' }, [
E('source', { 'src': this.data.stream_url || 'http://127.0.0.1:8000/lyrion', 'type': 'audio/mpeg' })
])
])
]);
},
renderCard: function(title, value, status, id) {
var cls = 'sb-card card-' + status;
return E('div', { 'class': cls, 'id': id }, [
E('div', { 'class': 'card-title' }, title),
E('div', { 'class': 'card-value' }, value)
]);
},
updateUI: function(data) {
var chipBridge = document.getElementById('chip-bridge');
var chipLyrion = document.getElementById('chip-lyrion');
if (chipBridge) chipBridge.textContent = data.bridge_running ? 'Bridge Running' : 'Bridge Stopped';
if (chipLyrion) chipLyrion.textContent = data.lyrion_online ? 'Lyrion Online' : 'Lyrion Offline';
var np = document.getElementById('now-playing');
if (np) {
np.innerHTML = '';
np.appendChild(E('span', { 'class': 'np-title' }, data.title || 'Nothing playing'));
if (data.artist) np.appendChild(E('span', { 'class': 'np-artist' }, data.artist));
}
var listeners = document.getElementById('listeners');
if (listeners) listeners.textContent = String(data.listeners || 0);
},
handleSetup: function() {
var server = document.getElementById('lyrion-server').value || '127.0.0.1';
return callBridgeSetup(server).then(function(res) {
ui.addNotification(null, E('p', 'Pipeline setup complete. Stream URL: ' + (res.stream_url || 'http://127.0.0.1:8000/lyrion')));
}).catch(function(e) {
ui.addNotification(null, E('p', 'Setup failed: ' + e.message), 'error');
});
},
handleStart: function() {
return callBridgeStart().then(function() {
ui.addNotification(null, E('p', 'Lyrion bridge started'));
}).catch(function(e) {
ui.addNotification(null, E('p', 'Failed: ' + e.message), 'error');
});
},
handleStop: function() {
return callBridgeStop().then(function() {
ui.addNotification(null, E('p', 'Lyrion bridge stopped'));
}).catch(function(e) {
ui.addNotification(null, E('p', 'Failed: ' + e.message), 'error');
});
},
getStyles: function() {
return [
'.sb-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }',
'.sb-chips { display:flex; gap:10px; }',
'.chip { padding:5px 12px; border-radius:15px; font-size:0.85em; background:#444; color:#fff; }',
'.sb-section { background:#1a1a2e; padding:20px; margin-bottom:15px; border-radius:8px; }',
'.sb-section h3 { margin:0 0 15px 0; color:#4fc3f7; }',
'.pipeline { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:15px; background:#0a0a15; border-radius:5px; }',
'.pipe-node { padding:8px 15px; background:#2196f3; color:#fff; border-radius:20px; font-weight:bold; }',
'.pipe-arrow { color:#4fc3f7; font-size:1.5em; }',
'.sb-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:15px; }',
'.sb-card { padding:15px; border-radius:8px; text-align:center; }',
'.card-success { background:#155724; border:1px solid #28a745; }',
'.card-danger { background:#721c24; border:1px solid #dc3545; }',
'.card-warning { background:#856404; border:1px solid #ffc107; }',
'.card-title { font-size:0.85em; color:#aaa; margin-bottom:5px; }',
'.card-value { font-size:1.1em; font-weight:bold; }',
'.now-playing { padding:15px; background:#0a0a15; border-radius:5px; }',
'.np-title { display:block; font-size:1.2em; font-weight:bold; }',
'.np-artist { display:block; color:#888; margin-top:5px; }',
'.btn-row { display:flex; gap:10px; flex-wrap:wrap; }',
'.sb-btn { padding:8px 16px; border:none; border-radius:5px; cursor:pointer; background:#444; color:#fff; }',
'.sb-btn:hover { background:#555; }',
'.sb-btn-primary { background:#007bff; }',
'.sb-btn-success { background:#28a745; }',
'.sb-btn-danger { background:#dc3545; }',
'.form-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }',
'.sb-input { padding:8px 12px; border:1px solid #444; border-radius:5px; background:#2a2a3e; color:#fff; }',
'.stream-url { padding:15px; background:#0a0a15; border-radius:5px; }',
'.stream-url a { color:#4fc3f7; text-decoration:none; font-family:monospace; }'
].join('\n');
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});