Add per-playlist max connections and viewing quota alerts

Each playlist gets a max_connections setting (default 1), editable in the
playlist modal without re-importing. Active viewings are tracked in the
shared db_iptv.viewings collection (also used by serverIPTV_2): devices
are counted from 'on'/'off' commands and their 10 s polling. When
starting a channel pushes a playlist above its limit, the web UI shows a
quota alert. The playlist list shows active/max connections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ubuntu
2026-10-16 02:02:10 +00:00
co-authored by Claude Opus 5.5
parent 16e979fe77
commit 4fa53e7a64
2 changed files with 189 additions and 3 deletions
+65 -1
View File
@@ -852,6 +852,7 @@
</style>
<body>
<div id="quota_alert" class="alert alert-danger shadow position-fixed top-0 start-50 translate-middle-x mt-3" role="alert" style="z-index:2000;display:none;max-width:90vw"></div>
<!-- ID CLIENT SOCKET -->
<input id="sidClient" val="" type="hidden">
@@ -1387,6 +1388,16 @@
<div class="m-2 valid-feedback">Date ok</div>
</div>
<!-- Max simultaneous connections -->
<div class="input-group p-2">
<div class="input-group-prepend">
<span class="input-group-text">Max connections</span>
</div>
<input id="playlist_max_connections" type="number" min="1" max="100" step="1" value="1" class="form-control" style="max-width:110px" aria-label="Maximum simultaneous connections">
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_update_playlist_max_connections" disabled>Update</button>
<div class="m-2 invalid-feedback">Between 1 and 100</div>
</div>
<div class="p-2">
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
<label class="form-label mb-0" for="playlist_comment">Comment</label>
@@ -1545,6 +1556,17 @@
socket.on('communication', function(data) {
console.log('communication', data)
// QUOTA DE CONNEXIONS DÉPASSÉ
if (data['typeCommand'] == 'quota_alert') {
var q = data['payload'] || {}
$('#quota_alert').html('<i class="fas fa-exclamation-triangle me-2"></i><b>Quota maximum de visionnage dépassé</b> sur la playlist « '
+ $('<span>').text(q.title_playlist || '').html() + ' » : ' + q.active + ' connexions actives pour ' + q.max + ' autorisée(s).')
$('#quota_alert').stop(true, true).fadeIn('fast')
clearTimeout(window.quotaAlertTimer)
window.quotaAlertTimer = setTimeout(() => { $('#quota_alert').fadeOut('slow') }, 10000)
update_list_playlist()
}
// RECEPTION RETURN WARNING
if (data['typeCommand'] == 'warning') {
$('#warning').html(data['payload'])
@@ -1762,7 +1784,9 @@
playlistImportInFlight = false;
$('#upload-button').prop('disabled', false).text('Save');
$('#btn_update_playlist_comment').prop('disabled', true);
$('#playlist_title, #playlist_url, #playlist_end_date, #xtream_server, #xtream_username, #xtream_password, #playlist_comment').removeClass('is-valid is-invalid');
$('#btn_update_playlist_max_connections').prop('disabled', true);
$('#playlist_max_connections').val(1);
$('#playlist_title, #playlist_max_connections, #playlist_url, #playlist_end_date, #xtream_server, #xtream_username, #xtream_password, #playlist_comment').removeClass('is-valid is-invalid');
$('#color-selector').removeClass('border border-danger rounded p-1');
$('input[name="playlist_color"]').prop('checked', false);
$('#playlist_type').val('m3u');
@@ -1790,6 +1814,8 @@
$('#xtream_username').val(playlist.xtream_username || '');
$('#xtream_password').val(playlist.xtream_password || '');
$('#btn_update_playlist_comment').prop('disabled', !(playlist.id_playlist || '').length);
$('#playlist_max_connections').val(playlist.max_connections || 1);
$('#btn_update_playlist_max_connections').prop('disabled', !(playlist.id_playlist || '').length);
if (playlist.selectedColor) {
$(`input[name="playlist_color"][value="${playlist.selectedColor}"]`).prop('checked', true);
}
@@ -2439,6 +2465,7 @@
<th scope="col">Title</th>\
<th scope="col">End Date</th>\
<th scope="col">Num channels</th>\
<th scope="col">Connections</th>\
<th scope="col"></th>\
<th scope="col"></th>\
<!--<th scope="col">Sid</th>-->\
@@ -2468,6 +2495,7 @@
</td>
<td>${dataJSON[i].date_end_playlist}</td>\
<td>${dataJSON[i].num_channels}</td>\
<td class="${(dataJSON[i].active_connections || 0) > (dataJSON[i].max_connections || 1) ? 'text-danger fw-bold' : ''}" title="Active / maximum simultaneous connections">${dataJSON[i].active_connections || 0} / ${dataJSON[i].max_connections || 1}</td>\
<!--<td style="color:darkblue" id="zone_edit_playlist_${id_playlist}"><div class="d-flex flex-row"><div><i class="fas fa-edit"></i></div><div class="d-none d-lg-block" style="margin-left: 5px;">Edit</div></div></td>-->\
<td style="color:red" data-param1="id_playlist" id="zone_delete_playlist_${i}"><div class="d-flex flex-row"><div><i class="fas fa-trash-alt"></i></div><div class="d-none d-lg-block" style="margin-left: 5px;">Delete</div></div></td>\
<td style="color:green" data-param1="id_playlist" id="zone_edit_playlist_${i}"><div class="d-flex flex-row"><div><i class="fas fa-edit"></i></div><div class="d-none d-lg-block" style="margin-left: 5px;">Edit</div></div></td>\
@@ -2848,6 +2876,36 @@
})
})
function readMaxConnections() {
var value = parseInt($('#playlist_max_connections').val(), 10);
var valid = Number.isInteger(value) && value >= 1 && value <= 100;
$('#playlist_max_connections').toggleClass('is-invalid', !valid);
return valid ? value : null;
}
$('#btn_update_playlist_max_connections').click(function(){
var idPlaylist = $('#id_playlist_to_action').val().trim();
var maxConnections = readMaxConnections();
if (!idPlaylist || !maxConnections) {
return;
}
$('#btn_update_playlist_max_connections').prop('disabled', true).text('Updating...');
$.post('update_playlist_max_connections', {
id_playlist: idPlaylist,
max_connections: maxConnections
}).done(function(data){
$('#playlist_max_connections').removeClass('is-invalid').addClass('is-valid');
$('#add_playlist_alert').html('<div class="alert alert-success" role="alert">Max connections updated.</div>');
update_list_playlist();
}).fail(function(xhr){
var message = (xhr.responseJSON && xhr.responseJSON.error) || 'Unable to update max connections.';
$('#playlist_max_connections').removeClass('is-valid').addClass('is-invalid');
$('#add_playlist_alert').html('<div class="alert alert-danger" role="alert">' + message + '</div>');
}).always(function(){
$('#btn_update_playlist_max_connections').prop('disabled', false).text('Update');
});
})
$('#btn_update_playlist_comment').click(function(){
var idPlaylist = $('#id_playlist_to_action').val().trim();
var playlistComment = $('#playlist_comment').val().trim();
@@ -2907,6 +2965,10 @@
var validationTitle = title.length > 0;
var validationDate = !!endDate;
var validationColor = typeof selectedColor !== 'undefined';
var maxConnections = readMaxConnections();
if (!maxConnections) {
return;
}
setFieldState('#playlist_title', validationTitle);
setFieldState('#playlist_end_date', validationDate);
@@ -2939,6 +3001,7 @@
title: title,
date_end: endDate,
comment: playlistComment,
max_connections: maxConnections,
sidClient: $('#sidClient').val(),
selectedColor: selectedColor,
xtream_server: xtreamServer,
@@ -3011,6 +3074,7 @@
'url': playlistUrl,
'date_end': endDate,
'comment': playlistComment,
'max_connections': maxConnections,
'sidClient': $('#sidClient').val(),
'filename': $("#fileName").val(),
'selectedColor': selectedColor