secubox-openwrt/package/secubox/luci-app-webradio/htdocs/luci-static/resources/view/webradio/schedule.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

377 lines
11 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require ui';
'require form';
'require uci';
var callSchedules = rpc.declare({
object: 'luci.webradio',
method: 'schedules',
expect: {}
});
var callCurrentShow = rpc.declare({
object: 'luci.webradio',
method: 'current_show',
expect: {}
});
var callAddSchedule = rpc.declare({
object: 'luci.webradio',
method: 'add_schedule',
params: ['name', 'start_time', 'end_time', 'days', 'playlist', 'jingle_before'],
expect: {}
});
var callUpdateSchedule = rpc.declare({
object: 'luci.webradio',
method: 'update_schedule',
params: ['slot', 'enabled', 'name', 'start_time', 'end_time', 'days', 'playlist', 'jingle_before'],
expect: {}
});
var callDeleteSchedule = rpc.declare({
object: 'luci.webradio',
method: 'delete_schedule',
params: ['slot'],
expect: {}
});
var callGenerateCron = rpc.declare({
object: 'luci.webradio',
method: 'generate_cron',
expect: {}
});
var DAYS = {
'0': 'Sun',
'1': 'Mon',
'2': 'Tue',
'3': 'Wed',
'4': 'Thu',
'5': 'Fri',
'6': 'Sat'
};
function formatDays(days) {
if (!days) return 'Every day';
if (days === '0123456') return 'Every day';
if (days === '12345') return 'Weekdays';
if (days === '06') return 'Weekends';
return days.split('').map(function(d) {
return DAYS[d] || d;
}).join(', ');
}
return view.extend({
load: function() {
return Promise.all([
callSchedules(),
callCurrentShow(),
uci.load('webradio')
]);
},
render: function(data) {
var self = this;
var scheduleData = data[0] || {};
var currentShow = data[1] || {};
var schedules = scheduleData.schedules || [];
var content = [
E('h2', {}, 'Programming Schedule'),
// Current show info
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Now Playing'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Show'),
E('div', { 'class': 'cbi-value-field' }, [
E('span', { 'style': 'font-weight: bold; font-size: 1.1em;' },
currentShow.name || 'Default')
])
]),
currentShow.playlist ? E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Playlist'),
E('div', { 'class': 'cbi-value-field' }, currentShow.playlist)
]) : '',
currentShow.start ? E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Started'),
E('div', { 'class': 'cbi-value-field' }, currentShow.start)
]) : ''
]),
// Scheduling settings
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Scheduling Settings'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Enable Scheduling'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'checkbox',
'id': 'scheduling-enabled',
'checked': scheduleData.scheduling_enabled
}),
E('span', { 'style': 'margin-left: 10px;' },
'Automatically switch shows based on schedule')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Timezone'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'id': 'timezone', 'class': 'cbi-input-select' }, [
E('option', { 'value': 'UTC', 'selected': scheduleData.timezone === 'UTC' }, 'UTC'),
E('option', { 'value': 'Europe/Paris', 'selected': scheduleData.timezone === 'Europe/Paris' }, 'Europe/Paris'),
E('option', { 'value': 'Europe/London', 'selected': scheduleData.timezone === 'Europe/London' }, 'Europe/London'),
E('option', { 'value': 'America/New_York', 'selected': scheduleData.timezone === 'America/New_York' }, 'America/New_York'),
E('option', { 'value': 'America/Los_Angeles', 'selected': scheduleData.timezone === 'America/Los_Angeles' }, 'America/Los_Angeles'),
E('option', { 'value': 'Asia/Tokyo', 'selected': scheduleData.timezone === 'Asia/Tokyo' }, 'Asia/Tokyo')
])
])
]),
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 10px;' }, [
E('button', {
'class': 'btn cbi-button-action',
'click': ui.createHandlerFn(this, 'handleSaveSettings')
}, 'Save Settings'),
E('button', {
'class': 'btn cbi-button-neutral',
'click': ui.createHandlerFn(this, 'handleGenerateCron')
}, 'Regenerate Cron')
])
]),
// Add new schedule
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Add New Schedule'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Show Name'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-name',
'class': 'cbi-input-text',
'placeholder': 'Morning Show',
'style': 'width: 250px;'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Start Time'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'time',
'id': 'new-start',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'End Time'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'time',
'id': 'new-end',
'class': 'cbi-input-text'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Days'),
E('div', { 'class': 'cbi-value-field' }, [
E('div', { 'style': 'display: flex; gap: 10px; flex-wrap: wrap;' },
Object.keys(DAYS).map(function(d) {
return E('label', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [
E('input', {
'type': 'checkbox',
'class': 'day-checkbox',
'data-day': d,
'checked': true
}),
DAYS[d]
]);
})
)
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Playlist'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'new-playlist',
'class': 'cbi-input-text',
'placeholder': 'morning_mix',
'style': 'width: 200px;'
}),
E('p', { 'style': 'color: #666; font-size: 0.9em;' },
'Playlist name (without .m3u extension)')
])
]),
E('button', {
'class': 'btn cbi-button-positive',
'style': 'margin-top: 10px;',
'click': ui.createHandlerFn(this, 'handleAddSchedule')
}, 'Add Schedule')
]),
// Schedule list
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Scheduled Shows (' + schedules.length + ')'),
this.renderScheduleTable(schedules)
])
];
return E('div', { 'class': 'cbi-map' }, content);
},
renderScheduleTable: function(schedules) {
if (!schedules || schedules.length === 0) {
return E('p', { 'style': 'color: #666;' },
'No schedules configured. Add a schedule above to create a programming grid.');
}
var self = this;
var rows = schedules.map(function(sched) {
var statusStyle = sched.enabled
? 'background: #4CAF50; color: white; padding: 2px 8px; border-radius: 3px;'
: 'background: #9e9e9e; color: white; padding: 2px 8px; border-radius: 3px;';
return E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'width: 30px;' }, [
E('input', {
'type': 'checkbox',
'checked': sched.enabled,
'data-slot': sched.slot,
'change': function(ev) {
self.handleToggleEnabled(sched.slot, ev.target.checked);
}
})
]),
E('div', { 'class': 'td', 'style': 'font-weight: bold;' }, sched.name),
E('div', { 'class': 'td' }, sched.start_time + ' - ' + (sched.end_time || '...')),
E('div', { 'class': 'td' }, formatDays(sched.days)),
E('div', { 'class': 'td' }, sched.playlist || '-'),
E('div', { 'class': 'td' }, sched.jingle_before || '-'),
E('div', { 'class': 'td', 'style': 'width: 80px;' }, [
E('button', {
'class': 'btn cbi-button-remove',
'style': 'padding: 2px 8px;',
'click': ui.createHandlerFn(self, 'handleDelete', sched.slot)
}, 'Delete')
])
]);
});
return E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr cbi-section-table-titles' }, [
E('div', { 'class': 'th' }, 'On'),
E('div', { 'class': 'th' }, 'Name'),
E('div', { 'class': 'th' }, 'Time'),
E('div', { 'class': 'th' }, 'Days'),
E('div', { 'class': 'th' }, 'Playlist'),
E('div', { 'class': 'th' }, 'Jingle'),
E('div', { 'class': 'th' }, 'Action')
])
].concat(rows));
},
handleSaveSettings: function() {
var enabled = document.getElementById('scheduling-enabled').checked;
var timezone = document.getElementById('timezone').value;
uci.set('webradio', 'scheduling', 'scheduling');
uci.set('webradio', 'scheduling', 'enabled', enabled ? '1' : '0');
uci.set('webradio', 'scheduling', 'timezone', timezone);
return uci.save().then(function() {
return uci.apply();
}).then(function() {
if (enabled) {
return callGenerateCron();
}
}).then(function() {
ui.addNotification(null, E('p', 'Settings saved'));
});
},
handleGenerateCron: function() {
ui.showModal('Generating Cron', [
E('p', { 'class': 'spinning' }, 'Generating cron schedule...')
]);
return callGenerateCron().then(function(res) {
ui.hideModal();
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'Cron schedule regenerated'));
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
}
});
},
handleAddSchedule: function() {
var name = document.getElementById('new-name').value;
var start_time = document.getElementById('new-start').value;
var end_time = document.getElementById('new-end').value;
var playlist = document.getElementById('new-playlist').value;
if (!name || !start_time) {
ui.addNotification(null, E('p', 'Name and start time are required'), 'warning');
return;
}
// Collect selected days
var days = '';
document.querySelectorAll('.day-checkbox:checked').forEach(function(cb) {
days += cb.dataset.day;
});
ui.showModal('Adding Schedule', [
E('p', { 'class': 'spinning' }, 'Creating schedule...')
]);
return callAddSchedule(name, start_time, end_time, days, playlist, '').then(function(res) {
ui.hideModal();
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'Schedule added: ' + name));
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
}
});
},
handleToggleEnabled: function(slot, enabled) {
return callUpdateSchedule(slot, enabled, null, null, null, null, null, null).then(function(res) {
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'Schedule ' + (enabled ? 'enabled' : 'disabled')));
}
});
},
handleDelete: function(slot) {
if (!confirm('Delete this schedule?')) return;
ui.showModal('Deleting', [
E('p', { 'class': 'spinning' }, 'Removing schedule...')
]);
return callDeleteSchedule(slot).then(function(res) {
ui.hideModal();
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'Schedule deleted'));
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
}
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});