Compare commits
3
Commits
16e979fe77
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d6f8d95c3 | ||
|
|
ad82a96440 | ||
|
|
4fa53e7a64 |
@@ -15,3 +15,6 @@ static/device_snapshots/
|
|||||||
|
|
||||||
# Config locale spécifique à cette machine
|
# Config locale spécifique à cette machine
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# Mot de passe MongoDB (variables de docker-compose)
|
||||||
|
.env
|
||||||
|
|||||||
+5
-3
@@ -5,10 +5,12 @@ services:
|
|||||||
image: mongo:latest
|
image: mongo:latest
|
||||||
container_name: mongodb
|
container_name: mongodb
|
||||||
ports:
|
ports:
|
||||||
- "27017:27017"
|
# Seulement depuis la machine elle-même : les applications passent par le réseau Docker
|
||||||
|
- "127.0.0.1:27017:27017"
|
||||||
environment:
|
environment:
|
||||||
MONGO_INITDB_ROOT_USERNAME: root
|
MONGO_INITDB_ROOT_USERNAME: root
|
||||||
MONGO_INITDB_ROOT_PASSWORD: example
|
# Mot de passe dans .env (MONGO_PASSWORD), hors du dépôt git
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:?MONGO_PASSWORD manquant dans .env}
|
||||||
volumes:
|
volumes:
|
||||||
- mongodb_data:/data/db
|
- mongodb_data:/data/db
|
||||||
networks:
|
networks:
|
||||||
@@ -20,7 +22,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- mongodb
|
- mongodb
|
||||||
environment:
|
environment:
|
||||||
- MONGO_URI=mongodb://root:example@mongodb:27017/?authSource=admin
|
- MONGO_URI=mongodb://root:${MONGO_PASSWORD:?MONGO_PASSWORD manquant dans .env}@mongodb:27017/?authSource=admin
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import io, re
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import pathlib
|
import pathlib
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import ipaddress
|
import ipaddress
|
||||||
@@ -67,6 +67,13 @@ playlists = db_iptv['playlists']
|
|||||||
logos = db_iptv['logos']
|
logos = db_iptv['logos']
|
||||||
recents = db_iptv['recents']
|
recents = db_iptv['recents']
|
||||||
settings_collection = db_iptv['settings']
|
settings_collection = db_iptv['settings']
|
||||||
|
# Visionnages en cours, partagés avec serverIPTV_2 (même collection, même format) :
|
||||||
|
# _id = "<source>:<clé>", un document par connexion ouverte chez le fournisseur.
|
||||||
|
viewings = db_iptv['viewings']
|
||||||
|
viewings.create_index("expires_at", expireAfterSeconds=0)
|
||||||
|
viewings.create_index("id_playlist")
|
||||||
|
VIEWING_ACTIVE_SECONDS = 35 # un appareil signale sa lecture toutes les 10 s
|
||||||
|
DEFAULT_MAX_CONNECTIONS = 1
|
||||||
|
|
||||||
devices_db = db_iptv['devices']
|
devices_db = db_iptv['devices']
|
||||||
MAX_STREAMING_ERRORS_PER_DEVICE = 5
|
MAX_STREAMING_ERRORS_PER_DEVICE = 5
|
||||||
@@ -192,6 +199,62 @@ def save_playlist_document(playlist_doc):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_max_connections(playlist_doc):
|
||||||
|
try:
|
||||||
|
return max(1, int((playlist_doc or {}).get('max_connections') or DEFAULT_MAX_CONNECTIONS))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return DEFAULT_MAX_CONNECTIONS
|
||||||
|
|
||||||
|
|
||||||
|
def parse_max_connections(value):
|
||||||
|
"""Valeur saisie dans la configuration de playlist (None si absente)."""
|
||||||
|
if value in (None, ''):
|
||||||
|
return None
|
||||||
|
number = int(value)
|
||||||
|
if not 1 <= number <= 100:
|
||||||
|
raise ValueError('max_connections must be between 1 and 100')
|
||||||
|
return number
|
||||||
|
|
||||||
|
|
||||||
|
def active_viewings_count(id_playlist, exclude_id=None):
|
||||||
|
since = datetime.utcnow() - timedelta(seconds=VIEWING_ACTIVE_SECONDS)
|
||||||
|
query = {'id_playlist': id_playlist, 'last_seen': {'$gte': since}}
|
||||||
|
if exclude_id:
|
||||||
|
query['_id'] = {'$ne': exclude_id}
|
||||||
|
return viewings.count_documents(query)
|
||||||
|
|
||||||
|
|
||||||
|
def touch_viewing(viewing_id, id_playlist, id_channel=None, name_channel=None, extra=None):
|
||||||
|
now = datetime.utcnow()
|
||||||
|
fields = {'id_playlist': id_playlist, 'last_seen': now,
|
||||||
|
'expires_at': now + timedelta(seconds=VIEWING_ACTIVE_SECONDS * 4)}
|
||||||
|
if id_channel is not None:
|
||||||
|
fields['id_channel'] = id_channel
|
||||||
|
if name_channel is not None:
|
||||||
|
fields['name_channel'] = name_channel
|
||||||
|
if extra:
|
||||||
|
fields.update(extra)
|
||||||
|
source, _, key = viewing_id.partition(':')
|
||||||
|
viewings.update_one({'_id': viewing_id},
|
||||||
|
{'$set': fields, '$setOnInsert': {'source': source, 'key': key, 'started_at': now}},
|
||||||
|
upsert=True)
|
||||||
|
|
||||||
|
|
||||||
|
def start_viewing(viewing_id, id_playlist, id_channel=None, name_channel=None, extra=None):
|
||||||
|
"""Enregistre un nouveau visionnage et renvoie l'état du quota de la playlist."""
|
||||||
|
others = active_viewings_count(id_playlist, exclude_id=viewing_id)
|
||||||
|
touch_viewing(viewing_id, id_playlist, id_channel, name_channel, extra)
|
||||||
|
playlist_doc = playlists.find_one({'id_playlist': id_playlist}) or {}
|
||||||
|
maximum = playlist_max_connections(playlist_doc)
|
||||||
|
active = others + 1
|
||||||
|
return {'id_playlist': id_playlist, 'title_playlist': playlist_doc.get('title_playlist', ''),
|
||||||
|
'active': active, 'max': maximum, 'exceeded': active > maximum}
|
||||||
|
|
||||||
|
|
||||||
|
def device_viewing_id(id_device):
|
||||||
|
return 'serverIPTV:' + str(id_device)
|
||||||
|
|
||||||
|
|
||||||
def delete_playlist_resources(id_playlist):
|
def delete_playlist_resources(id_playlist):
|
||||||
db_iptv.drop_collection("channels_" + id_playlist)
|
db_iptv.drop_collection("channels_" + id_playlist)
|
||||||
|
|
||||||
@@ -464,6 +527,7 @@ async def analyse_m3u_file(request):
|
|||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
max_connections = parse_max_connections(data.get('max_connections'))
|
||||||
id_playlist = validate_id_playlist(id_playlist)
|
id_playlist = validate_id_playlist(id_playlist)
|
||||||
if not UPLOADED_PLAYLIST_RE.match(filename or ''):
|
if not UPLOADED_PLAYLIST_RE.match(filename or ''):
|
||||||
raise ValueError('invalid filename')
|
raise ValueError('invalid filename')
|
||||||
@@ -489,6 +553,7 @@ async def analyse_m3u_file(request):
|
|||||||
'xtream_server': '',
|
'xtream_server': '',
|
||||||
'xtream_username': '',
|
'xtream_username': '',
|
||||||
'xtream_password': '',
|
'xtream_password': '',
|
||||||
|
**({'max_connections': max_connections} if max_connections else {}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
save_playlist_document(new_playlist)
|
save_playlist_document(new_playlist)
|
||||||
@@ -496,8 +561,12 @@ async def analyse_m3u_file(request):
|
|||||||
# Process the m3u file
|
# Process the m3u file
|
||||||
|
|
||||||
new_channels =[]
|
new_channels =[]
|
||||||
f = open(new_path_filename, "r", encoding="utf8")
|
# Lecture ligne à ligne : une playlist de plusieurs centaines de Mo chargée d'un
|
||||||
Lines = f.readlines()
|
# bloc (readlines) ne tient pas dans la mémoire du VPS.
|
||||||
|
with open(new_path_filename, "r", encoding="utf8", errors="replace") as count_file:
|
||||||
|
total_lines = max(1, sum(1 for _ in count_file))
|
||||||
|
f = open(new_path_filename, "r", encoding="utf8", errors="replace")
|
||||||
|
Lines = f
|
||||||
print('ouverture fichier', flush=True)
|
print('ouverture fichier', flush=True)
|
||||||
channel = {}
|
channel = {}
|
||||||
separator = '--------'
|
separator = '--------'
|
||||||
@@ -505,7 +574,6 @@ async def analyse_m3u_file(request):
|
|||||||
num_channel = 0
|
num_channel = 0
|
||||||
TypeLangChannel = ['EN','FR','US','CA', 'UK' ]
|
TypeLangChannel = ['EN','FR','US','CA', 'UK' ]
|
||||||
|
|
||||||
total_lines = len(Lines)
|
|
||||||
number_line = 0
|
number_line = 0
|
||||||
|
|
||||||
for line in Lines:
|
for line in Lines:
|
||||||
@@ -558,6 +626,7 @@ async def analyse_m3u_file(request):
|
|||||||
num_channel += 1
|
num_channel += 1
|
||||||
|
|
||||||
|
|
||||||
|
f.close()
|
||||||
bulk_insert_channels(collection_channels, new_channels, num_channel)
|
bulk_insert_channels(collection_channels, new_channels, num_channel)
|
||||||
print("Save final channels ", num_channel, flush=True)
|
print("Save final channels ", num_channel, flush=True)
|
||||||
number_channels = collection_channels.count_documents({"id_playlist":id_playlist})
|
number_channels = collection_channels.count_documents({"id_playlist":id_playlist})
|
||||||
@@ -578,6 +647,10 @@ async def import_xtream_codes(request):
|
|||||||
xtream_server = normalize_xtream_server(data.get('xtream_server', ''))
|
xtream_server = normalize_xtream_server(data.get('xtream_server', ''))
|
||||||
xtream_username = data.get('xtream_username', '').strip()
|
xtream_username = data.get('xtream_username', '').strip()
|
||||||
xtream_password = data.get('xtream_password', '').strip()
|
xtream_password = data.get('xtream_password', '').strip()
|
||||||
|
try:
|
||||||
|
max_connections = parse_max_connections(data.get('max_connections'))
|
||||||
|
except ValueError as exc:
|
||||||
|
return web.json_response({'error': str(exc)}, status=400)
|
||||||
|
|
||||||
if not xtream_server or not xtream_username or not xtream_password:
|
if not xtream_server or not xtream_username or not xtream_password:
|
||||||
return web.json_response({'error': 'Xtream server, username and password are required.'}, status=400)
|
return web.json_response({'error': 'Xtream server, username and password are required.'}, status=400)
|
||||||
@@ -620,6 +693,7 @@ async def import_xtream_codes(request):
|
|||||||
'xtream_server': xtream_server,
|
'xtream_server': xtream_server,
|
||||||
'xtream_username': xtream_username,
|
'xtream_username': xtream_username,
|
||||||
'xtream_password': xtream_password,
|
'xtream_password': xtream_password,
|
||||||
|
**({'max_connections': max_connections} if max_connections else {}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
save_playlist_document(playlist_doc)
|
save_playlist_document(playlist_doc)
|
||||||
@@ -897,6 +971,21 @@ async def update_playlist_comment(request):
|
|||||||
return web.json_response({'status': 'ok', 'id_playlist': id_playlist, 'comment': comment})
|
return web.json_response({'status': 'ok', 'id_playlist': id_playlist, 'comment': comment})
|
||||||
|
|
||||||
|
|
||||||
|
async def update_playlist_max_connections(request):
|
||||||
|
data = await request.post()
|
||||||
|
id_playlist = (data.get('id_playlist') or '').strip()
|
||||||
|
try:
|
||||||
|
max_connections = parse_max_connections(data.get('max_connections'))
|
||||||
|
except ValueError as exc:
|
||||||
|
return web.json_response({'error': str(exc)}, status=400)
|
||||||
|
if not id_playlist or not max_connections:
|
||||||
|
return web.json_response({'error': 'id_playlist and max_connections are required'}, status=400)
|
||||||
|
result = playlists.update_one({'id_playlist': id_playlist}, {'$set': {'max_connections': max_connections}})
|
||||||
|
if result.matched_count == 0:
|
||||||
|
return web.json_response({'error': 'playlist not found'}, status=404)
|
||||||
|
return web.json_response({'status': 'ok', 'id_playlist': id_playlist, 'max_connections': max_connections})
|
||||||
|
|
||||||
|
|
||||||
# Get List Playlists
|
# Get List Playlists
|
||||||
async def get_list_playlists(request):
|
async def get_list_playlists(request):
|
||||||
active_count = playlists.count_documents({})
|
active_count = playlists.count_documents({})
|
||||||
@@ -911,7 +1000,11 @@ async def get_list_playlists(request):
|
|||||||
if active_count == 0:
|
if active_count == 0:
|
||||||
return web.json_response({'status':'no data'})
|
return web.json_response({'status':'no data'})
|
||||||
else:
|
else:
|
||||||
return web.json_response(dumps(playlists.find()))
|
items = list(playlists.find())
|
||||||
|
for p in items:
|
||||||
|
p['max_connections'] = playlist_max_connections(p)
|
||||||
|
p['active_connections'] = active_viewings_count(p.get('id_playlist'))
|
||||||
|
return web.json_response(dumps(items))
|
||||||
|
|
||||||
|
|
||||||
async def get_source(request):
|
async def get_source(request):
|
||||||
@@ -1337,6 +1430,7 @@ app.router.add_post('/upload_snapshot', upload_snapshot)
|
|||||||
app.router.add_post('/analyse_m3u_file', analyse_m3u_file)
|
app.router.add_post('/analyse_m3u_file', analyse_m3u_file)
|
||||||
app.router.add_post('/import_xtream_codes', import_xtream_codes)
|
app.router.add_post('/import_xtream_codes', import_xtream_codes)
|
||||||
app.router.add_post('/update_playlist_comment', update_playlist_comment)
|
app.router.add_post('/update_playlist_comment', update_playlist_comment)
|
||||||
|
app.router.add_post('/update_playlist_max_connections', update_playlist_max_connections)
|
||||||
app.router.add_get('/delete_playlist', delete_playlist)
|
app.router.add_get('/delete_playlist', delete_playlist)
|
||||||
app.router.add_get('/cleanup_xtream_duplicates', cleanup_xtream_duplicates)
|
app.router.add_get('/cleanup_xtream_duplicates', cleanup_xtream_duplicates)
|
||||||
app.router.add_post('/list_channel', list_channel)
|
app.router.add_post('/list_channel', list_channel)
|
||||||
@@ -1411,9 +1505,31 @@ async def communication(fromSid, data):
|
|||||||
print("search request from ", fromSid, flush=True)
|
print("search request from ", fromSid, flush=True)
|
||||||
toSidDevice = data['toSidDevice']
|
toSidDevice = data['toSidDevice']
|
||||||
print("send signal to ", toSidDevice, flush=True)
|
print("send signal to ", toSidDevice, flush=True)
|
||||||
|
type_command = data.get('typeCommand')
|
||||||
|
if type_command in ('on', 'off'):
|
||||||
|
try:
|
||||||
|
await track_device_command(fromSid, toSidDevice, type_command, data.get('payload'))
|
||||||
|
except Exception as exc: # le suivi des quotas ne doit jamais bloquer la commande
|
||||||
|
logger.warning("Viewing tracking failed: %s", exc)
|
||||||
await sio.emit('communication', data, room=toSidDevice ,callback=messageReceived)
|
await sio.emit('communication', data, room=toSidDevice ,callback=messageReceived)
|
||||||
|
|
||||||
|
|
||||||
|
async def track_device_command(fromSid, toSidDevice, type_command, payload):
|
||||||
|
device = devices_db.find_one({'sid': toSidDevice}) or {}
|
||||||
|
viewing_id = device_viewing_id(device.get('id_device') or toSidDevice)
|
||||||
|
if type_command == 'off':
|
||||||
|
viewings.delete_one({'_id': viewing_id})
|
||||||
|
return
|
||||||
|
if not isinstance(payload, dict) or not payload.get('id_playlist'):
|
||||||
|
return
|
||||||
|
quota = start_viewing(viewing_id, payload['id_playlist'], payload.get('id_channel'),
|
||||||
|
payload.get('name'), {'device_name': device.get('name')})
|
||||||
|
logger.info("Viewing %s on playlist %s: %s/%s", viewing_id, quota['title_playlist'],
|
||||||
|
quota['active'], quota['max'])
|
||||||
|
if quota['exceeded']:
|
||||||
|
await sio.emit('communication', {'typeCommand': 'quota_alert', 'payload': quota}, room=fromSid)
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
async def streaming_error(sid, data):
|
async def streaming_error(sid, data):
|
||||||
IdDevice = data.get('IdDevice')
|
IdDevice = data.get('IdDevice')
|
||||||
@@ -1432,6 +1548,9 @@ async def streaming_error(sid, data):
|
|||||||
@sio.event
|
@sio.event
|
||||||
def disconnect(sid):
|
def disconnect(sid):
|
||||||
sio_session_ips.pop(sid, None)
|
sio_session_ips.pop(sid, None)
|
||||||
|
device = devices_db.find_one({'sid': sid}, {'id_device': 1})
|
||||||
|
if device:
|
||||||
|
viewings.delete_one({'_id': device_viewing_id(device['id_device'])})
|
||||||
print('disconnect ', sid, flush=True)
|
print('disconnect ', sid, flush=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -1466,6 +1585,13 @@ async def polling(sid, data):
|
|||||||
new_device = { 'id_device': IdDevice, 'name' : device_name, 'last_datetime' : device_last_datetime, 'sid':sid ,'mediaRunning' : mediaRunning, 'vpn_status':vpn_status, 'data_running':data_running, 'auto_snapshot_enabled': auto_snapshot_enabled, 'snapshot_interval_seconds': snapshot_interval_seconds, 'ip_address': ip_address, 'ip_country': ip_country}
|
new_device = { 'id_device': IdDevice, 'name' : device_name, 'last_datetime' : device_last_datetime, 'sid':sid ,'mediaRunning' : mediaRunning, 'vpn_status':vpn_status, 'data_running':data_running, 'auto_snapshot_enabled': auto_snapshot_enabled, 'snapshot_interval_seconds': snapshot_interval_seconds, 'ip_address': ip_address, 'ip_country': ip_country}
|
||||||
devices_db.insert_one(new_device)
|
devices_db.insert_one(new_device)
|
||||||
|
|
||||||
|
if data_running and data_running.get('id_playlist'):
|
||||||
|
touch_viewing(device_viewing_id(IdDevice), data_running['id_playlist'],
|
||||||
|
data_running.get('id_channel'), data_running.get('name_channel'),
|
||||||
|
{'device_name': device_name})
|
||||||
|
else:
|
||||||
|
viewings.delete_one({'_id': device_viewing_id(IdDevice)})
|
||||||
|
|
||||||
if data_running:
|
if data_running:
|
||||||
new_record = {
|
new_record = {
|
||||||
"id_device": IdDevice,
|
"id_device": IdDevice,
|
||||||
|
|||||||
+65
-1
@@ -852,6 +852,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<body>
|
<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 -->
|
<!-- ID CLIENT SOCKET -->
|
||||||
<input id="sidClient" val="" type="hidden">
|
<input id="sidClient" val="" type="hidden">
|
||||||
@@ -1387,6 +1388,16 @@
|
|||||||
<div class="m-2 valid-feedback">Date ok</div>
|
<div class="m-2 valid-feedback">Date ok</div>
|
||||||
</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="p-2">
|
||||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-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>
|
<label class="form-label mb-0" for="playlist_comment">Comment</label>
|
||||||
@@ -1545,6 +1556,17 @@
|
|||||||
socket.on('communication', function(data) {
|
socket.on('communication', function(data) {
|
||||||
console.log('communication', 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
|
// RECEPTION RETURN WARNING
|
||||||
if (data['typeCommand'] == 'warning') {
|
if (data['typeCommand'] == 'warning') {
|
||||||
$('#warning').html(data['payload'])
|
$('#warning').html(data['payload'])
|
||||||
@@ -1762,7 +1784,9 @@
|
|||||||
playlistImportInFlight = false;
|
playlistImportInFlight = false;
|
||||||
$('#upload-button').prop('disabled', false).text('Save');
|
$('#upload-button').prop('disabled', false).text('Save');
|
||||||
$('#btn_update_playlist_comment').prop('disabled', true);
|
$('#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');
|
$('#color-selector').removeClass('border border-danger rounded p-1');
|
||||||
$('input[name="playlist_color"]').prop('checked', false);
|
$('input[name="playlist_color"]').prop('checked', false);
|
||||||
$('#playlist_type').val('m3u');
|
$('#playlist_type').val('m3u');
|
||||||
@@ -1790,6 +1814,8 @@
|
|||||||
$('#xtream_username').val(playlist.xtream_username || '');
|
$('#xtream_username').val(playlist.xtream_username || '');
|
||||||
$('#xtream_password').val(playlist.xtream_password || '');
|
$('#xtream_password').val(playlist.xtream_password || '');
|
||||||
$('#btn_update_playlist_comment').prop('disabled', !(playlist.id_playlist || '').length);
|
$('#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) {
|
if (playlist.selectedColor) {
|
||||||
$(`input[name="playlist_color"][value="${playlist.selectedColor}"]`).prop('checked', true);
|
$(`input[name="playlist_color"][value="${playlist.selectedColor}"]`).prop('checked', true);
|
||||||
}
|
}
|
||||||
@@ -2439,6 +2465,7 @@
|
|||||||
<th scope="col">Title</th>\
|
<th scope="col">Title</th>\
|
||||||
<th scope="col">End Date</th>\
|
<th scope="col">End Date</th>\
|
||||||
<th scope="col">Num channels</th>\
|
<th scope="col">Num channels</th>\
|
||||||
|
<th scope="col">Connections</th>\
|
||||||
<th scope="col"></th>\
|
<th scope="col"></th>\
|
||||||
<th scope="col"></th>\
|
<th scope="col"></th>\
|
||||||
<!--<th scope="col">Sid</th>-->\
|
<!--<th scope="col">Sid</th>-->\
|
||||||
@@ -2468,6 +2495,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>${dataJSON[i].date_end_playlist}</td>\
|
<td>${dataJSON[i].date_end_playlist}</td>\
|
||||||
<td>${dataJSON[i].num_channels}</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: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: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>\
|
<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(){
|
$('#btn_update_playlist_comment').click(function(){
|
||||||
var idPlaylist = $('#id_playlist_to_action').val().trim();
|
var idPlaylist = $('#id_playlist_to_action').val().trim();
|
||||||
var playlistComment = $('#playlist_comment').val().trim();
|
var playlistComment = $('#playlist_comment').val().trim();
|
||||||
@@ -2907,6 +2965,10 @@
|
|||||||
var validationTitle = title.length > 0;
|
var validationTitle = title.length > 0;
|
||||||
var validationDate = !!endDate;
|
var validationDate = !!endDate;
|
||||||
var validationColor = typeof selectedColor !== 'undefined';
|
var validationColor = typeof selectedColor !== 'undefined';
|
||||||
|
var maxConnections = readMaxConnections();
|
||||||
|
if (!maxConnections) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setFieldState('#playlist_title', validationTitle);
|
setFieldState('#playlist_title', validationTitle);
|
||||||
setFieldState('#playlist_end_date', validationDate);
|
setFieldState('#playlist_end_date', validationDate);
|
||||||
@@ -2939,6 +3001,7 @@
|
|||||||
title: title,
|
title: title,
|
||||||
date_end: endDate,
|
date_end: endDate,
|
||||||
comment: playlistComment,
|
comment: playlistComment,
|
||||||
|
max_connections: maxConnections,
|
||||||
sidClient: $('#sidClient').val(),
|
sidClient: $('#sidClient').val(),
|
||||||
selectedColor: selectedColor,
|
selectedColor: selectedColor,
|
||||||
xtream_server: xtreamServer,
|
xtream_server: xtreamServer,
|
||||||
@@ -3011,6 +3074,7 @@
|
|||||||
'url': playlistUrl,
|
'url': playlistUrl,
|
||||||
'date_end': endDate,
|
'date_end': endDate,
|
||||||
'comment': playlistComment,
|
'comment': playlistComment,
|
||||||
|
'max_connections': maxConnections,
|
||||||
'sidClient': $('#sidClient').val(),
|
'sidClient': $('#sidClient').val(),
|
||||||
'filename': $("#fileName").val(),
|
'filename': $("#fileName").val(),
|
||||||
'selectedColor': selectedColor
|
'selectedColor': selectedColor
|
||||||
|
|||||||
Reference in New Issue
Block a user