Compare commits
10
Commits
0fa98c7283
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d6f8d95c3 | ||
|
|
ad82a96440 | ||
|
|
4fa53e7a64 | ||
|
|
16e979fe77 | ||
|
|
03ec3f0c68 | ||
|
|
e5df068778 | ||
|
|
4e596114c1 | ||
|
|
685bbb1683 | ||
|
|
2eaa0fee64 | ||
|
|
3eff9519db |
@@ -15,3 +15,6 @@ static/device_snapshots/
|
||||
|
||||
# Config locale spécifique à cette machine
|
||||
.claude/
|
||||
|
||||
# Mot de passe MongoDB (variables de docker-compose)
|
||||
.env
|
||||
|
||||
+5
-3
@@ -5,10 +5,12 @@ services:
|
||||
image: mongo:latest
|
||||
container_name: mongodb
|
||||
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:
|
||||
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:
|
||||
- mongodb_data:/data/db
|
||||
networks:
|
||||
@@ -20,7 +22,7 @@ services:
|
||||
depends_on:
|
||||
- mongodb
|
||||
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:
|
||||
- app-network
|
||||
volumes:
|
||||
|
||||
@@ -27,16 +27,12 @@ import io, re
|
||||
import xml.etree.ElementTree as ET
|
||||
from bs4 import BeautifulSoup
|
||||
import pathlib
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from bson import ObjectId
|
||||
import urllib.request
|
||||
import ipaddress
|
||||
|
||||
|
||||
|
||||
from pymongo import MongoClient
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
@@ -71,10 +67,16 @@ playlists = db_iptv['playlists']
|
||||
logos = db_iptv['logos']
|
||||
recents = db_iptv['recents']
|
||||
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']
|
||||
MAX_STREAMING_ERRORS_PER_DEVICE = 5
|
||||
devices = []
|
||||
active_xtream_imports = {}
|
||||
sio_session_ips = {}
|
||||
ip_country_cache = {}
|
||||
@@ -109,6 +111,18 @@ def safe_join(base_dir, user_filename):
|
||||
return candidate
|
||||
|
||||
|
||||
UPLOADED_PLAYLIST_RE = re.compile(r'^upload_[0-9a-f]{32}\.m3u$')
|
||||
|
||||
|
||||
def validate_id_playlist(value):
|
||||
"""Playlist ids are UUIDs; anything else is rejected (they end up in file paths
|
||||
and Mongo collection names)."""
|
||||
try:
|
||||
return str(uuid.UUID(str(value)))
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
raise ValueError('invalid id_playlist')
|
||||
|
||||
|
||||
def findInfo(tag, s):
|
||||
end = '"'
|
||||
if (s.find(tag)>-1):
|
||||
@@ -185,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):
|
||||
db_iptv.drop_collection("channels_" + id_playlist)
|
||||
|
||||
@@ -445,8 +515,6 @@ async def init_db(request):
|
||||
|
||||
|
||||
async def analyse_m3u_file(request):
|
||||
|
||||
NUM_MAX_CHANNEL = 300000
|
||||
data = await request.post()
|
||||
id_playlist = data['id_playlist']
|
||||
title_playlist = data['title']
|
||||
@@ -458,8 +526,17 @@ async def analyse_m3u_file(request):
|
||||
playlist_comment = data.get('comment', '').strip()
|
||||
|
||||
|
||||
new_path_filename = "./" + UPLOAD_PLAYLIST_DIR + '/' + id_playlist + '.m3u'
|
||||
old_path_filename = "./" + UPLOAD_PLAYLIST_DIR + '/' +filename
|
||||
try:
|
||||
max_connections = parse_max_connections(data.get('max_connections'))
|
||||
id_playlist = validate_id_playlist(id_playlist)
|
||||
if not UPLOADED_PLAYLIST_RE.match(filename or ''):
|
||||
raise ValueError('invalid filename')
|
||||
new_path_filename = safe_join(UPLOAD_PLAYLIST_DIR, id_playlist + '.m3u')
|
||||
old_path_filename = safe_join(UPLOAD_PLAYLIST_DIR, filename)
|
||||
except ValueError as exc:
|
||||
return web.json_response({'error': str(exc)}, status=400)
|
||||
if not os.path.isfile(old_path_filename):
|
||||
return web.json_response({'error': 'uploaded file not found'}, status=400)
|
||||
os.rename(old_path_filename,new_path_filename)
|
||||
|
||||
collection_channels = get_playlist_collection(id_playlist)
|
||||
@@ -476,6 +553,7 @@ async def analyse_m3u_file(request):
|
||||
'xtream_server': '',
|
||||
'xtream_username': '',
|
||||
'xtream_password': '',
|
||||
**({'max_connections': max_connections} if max_connections else {}),
|
||||
}
|
||||
)
|
||||
save_playlist_document(new_playlist)
|
||||
@@ -483,8 +561,12 @@ async def analyse_m3u_file(request):
|
||||
# Process the m3u file
|
||||
|
||||
new_channels =[]
|
||||
f = open(new_path_filename, "r", encoding="utf8")
|
||||
Lines = f.readlines()
|
||||
# Lecture ligne à ligne : une playlist de plusieurs centaines de Mo chargée d'un
|
||||
# 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)
|
||||
channel = {}
|
||||
separator = '--------'
|
||||
@@ -492,7 +574,6 @@ async def analyse_m3u_file(request):
|
||||
num_channel = 0
|
||||
TypeLangChannel = ['EN','FR','US','CA', 'UK' ]
|
||||
|
||||
total_lines = len(Lines)
|
||||
number_line = 0
|
||||
|
||||
for line in Lines:
|
||||
@@ -545,6 +626,7 @@ async def analyse_m3u_file(request):
|
||||
num_channel += 1
|
||||
|
||||
|
||||
f.close()
|
||||
bulk_insert_channels(collection_channels, new_channels, num_channel)
|
||||
print("Save final channels ", num_channel, flush=True)
|
||||
number_channels = collection_channels.count_documents({"id_playlist":id_playlist})
|
||||
@@ -554,120 +636,6 @@ async def analyse_m3u_file(request):
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
|
||||
|
||||
async def analyse_m3u_file2(request):
|
||||
|
||||
NUM_MAX_CHANNEL = 300000
|
||||
data = await request.post()
|
||||
id_playlist = data['id_playlist']
|
||||
title_playlist = data['title']
|
||||
url_playlist = data['url']
|
||||
date_end_playlist = data['date_end']
|
||||
filename = data['filename']
|
||||
sidClient = data['sidClient']
|
||||
playlist_comment = data.get('comment', '').strip()
|
||||
|
||||
|
||||
new_path_filename = "./" + UPLOAD_PLAYLIST_DIR + '/' + id_playlist + '.m3u'
|
||||
old_path_filename = "./" + UPLOAD_PLAYLIST_DIR + '/' +filename
|
||||
os.rename(old_path_filename,new_path_filename)
|
||||
|
||||
collection_channels = get_playlist_collection(id_playlist)
|
||||
collection_channels.drop()
|
||||
new_playlist = build_playlist_document(
|
||||
id_playlist,
|
||||
title_playlist,
|
||||
date_end_playlist,
|
||||
'',
|
||||
{
|
||||
'playlist_type': 'm3u',
|
||||
'url_playlist': url_playlist,
|
||||
'comment': playlist_comment,
|
||||
'xtream_server': '',
|
||||
'xtream_username': '',
|
||||
'xtream_password': '',
|
||||
}
|
||||
)
|
||||
save_playlist_document(new_playlist)
|
||||
|
||||
# Process the m3u file
|
||||
TypeLangChannel = ['EN','FR','US','CA', 'UK' ]
|
||||
new_channels =[]
|
||||
f = open(new_path_filename, "r", encoding="utf8")
|
||||
Lines = f.readlines()
|
||||
print('ouverture fichier', flush=True)
|
||||
channel = {}
|
||||
separator = '--------'
|
||||
topic = ''
|
||||
num_channel = 0
|
||||
|
||||
total_lines = len(Lines)
|
||||
number_line = 0
|
||||
|
||||
for line in Lines:
|
||||
|
||||
number_line += 1
|
||||
|
||||
if separator in line:
|
||||
topic = line[10:]
|
||||
topic = topic.replace(separator,'')
|
||||
|
||||
if len(channel)>0 and line[0:4]=="http":
|
||||
channel["url"] = line.strip('\n')
|
||||
if len(LanguageChannel) > 0:
|
||||
|
||||
new_channels.append(channel)
|
||||
print(num_channel, flush=True)
|
||||
if num_channel % 1000 == 0:
|
||||
bulk_insert_channels(collection_channels, new_channels, num_channel)
|
||||
new_channels = []
|
||||
print('%d / %d' % (number_line, total_lines), flush=True)
|
||||
|
||||
|
||||
if line[0:4] == "#EXT":
|
||||
channel = {}
|
||||
|
||||
# DEFINE CHANNEL NAME
|
||||
name_channel = line[line.rfind(',')+1:].strip('\n')
|
||||
id_channel = str(uuid.uuid4())
|
||||
LanguageChannel =''
|
||||
for langChannel in TypeLangChannel:
|
||||
if langChannel in line:
|
||||
LanguageChannel = langChannel
|
||||
|
||||
# deternine url logo
|
||||
url_logo = findInfo('tvg-logo', line)
|
||||
#if len(url_logo) == 0:
|
||||
# cleaned_channel_name = clean_name_channel(name_channel)
|
||||
# logo_data = logos.find({ 'logo_name': { '$regex': cleaned_channel_name, '$options': "i" } })
|
||||
# for l in logo_data:
|
||||
# url_logo = l['logo_url']
|
||||
# break
|
||||
|
||||
channel_exist = False
|
||||
if len(name_channel)>0:
|
||||
channel = {"id_playlist":id_playlist, "id_channel":id_channel, "Name":name_channel, "Language":LanguageChannel, "Topic":topic, "urlLogo" : url_logo}
|
||||
num_channel += 1
|
||||
channel_exist = True
|
||||
|
||||
if channel_exist == False:
|
||||
name_channel = line[line.rfind('|')+2:].strip('\n')
|
||||
if len(name_channel) > 0:
|
||||
channel = {"id_playlist":id_playlist, "id_channel":id_channel, "Name":name_channel, "Language":LanguageChannel, "Topic":topic, "urlLogo" : url_logo}
|
||||
|
||||
|
||||
bulk_insert_channels(collection_channels, new_channels, num_channel)
|
||||
print("Save final channels ", num_channel, flush=True)
|
||||
number_channels = collection_channels.count_documents({"id_playlist":id_playlist})
|
||||
playlists.update_many({'id_playlist':id_playlist}, { "$set": { "num_channels": number_channels } } )
|
||||
result = {'num_channel': number_channels}
|
||||
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
|
||||
|
||||
async def import_xtream_codes(request):
|
||||
data = await request.post()
|
||||
id_playlist = data.get('id_playlist', '').strip()
|
||||
@@ -679,6 +647,10 @@ async def import_xtream_codes(request):
|
||||
xtream_server = normalize_xtream_server(data.get('xtream_server', ''))
|
||||
xtream_username = data.get('xtream_username', '').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:
|
||||
return web.json_response({'error': 'Xtream server, username and password are required.'}, status=400)
|
||||
@@ -721,6 +693,7 @@ async def import_xtream_codes(request):
|
||||
'xtream_server': xtream_server,
|
||||
'xtream_username': xtream_username,
|
||||
'xtream_password': xtream_password,
|
||||
**({'max_connections': max_connections} if max_connections else {}),
|
||||
}
|
||||
)
|
||||
save_playlist_document(playlist_doc)
|
||||
@@ -741,19 +714,20 @@ async def run_xtream_import(id_playlist, import_key, collection_channels,
|
||||
try:
|
||||
await emit_analysis_progress({'percentage': 0.02}, sidClient)
|
||||
|
||||
live_categories = parse_xtream_category_map(
|
||||
fetch_xtream_json(xtream_server, xtream_username, xtream_password, 'get_live_categories')
|
||||
)
|
||||
vod_categories = parse_xtream_category_map(
|
||||
fetch_xtream_json(xtream_server, xtream_username, xtream_password, 'get_vod_categories')
|
||||
)
|
||||
series_categories = parse_xtream_category_map(
|
||||
fetch_xtream_json(xtream_server, xtream_username, xtream_password, 'get_series_categories')
|
||||
)
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
live_streams = fetch_xtream_json(xtream_server, xtream_username, xtream_password, 'get_live_streams')
|
||||
vod_streams = fetch_xtream_json(xtream_server, xtream_username, xtream_password, 'get_vod_streams')
|
||||
series_list = fetch_xtream_json(xtream_server, xtream_username, xtream_password, 'get_series')
|
||||
async def fetch(action, **extra_params):
|
||||
return await loop.run_in_executor(
|
||||
None, lambda: fetch_xtream_json(xtream_server, xtream_username, xtream_password, action, **extra_params)
|
||||
)
|
||||
|
||||
live_categories = parse_xtream_category_map(await fetch('get_live_categories'))
|
||||
vod_categories = parse_xtream_category_map(await fetch('get_vod_categories'))
|
||||
series_categories = parse_xtream_category_map(await fetch('get_series_categories'))
|
||||
|
||||
live_streams = await fetch('get_live_streams')
|
||||
vod_streams = await fetch('get_vod_streams')
|
||||
series_list = await fetch('get_series')
|
||||
|
||||
total_steps = max(1, len(live_streams) + len(vod_streams) + len(series_list))
|
||||
processed_steps = 0
|
||||
@@ -841,27 +815,36 @@ async def handle_upload(request):
|
||||
# Handle both form field and file in one pass
|
||||
async for part in reader:
|
||||
if part.name == "id_playlist_to_action":
|
||||
id_playlist_data = await part.text()
|
||||
id_playlist_data = (await part.text()).strip()
|
||||
if len(id_playlist_data) == 0:
|
||||
print('No playlist to update', flush=True)
|
||||
id_playlist = str(uuid.uuid4())
|
||||
else:
|
||||
id_playlist = id_playlist_data
|
||||
try:
|
||||
id_playlist = validate_id_playlist(id_playlist_data)
|
||||
except ValueError:
|
||||
if filepath and os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
return web.Response(text="Invalid id_playlist.", status=400)
|
||||
print('playlist to update :', id_playlist, flush=True)
|
||||
|
||||
elif part.name == "file":
|
||||
filename = part.filename
|
||||
if not filename:
|
||||
if not part.filename:
|
||||
return web.Response(text="No file provided.", status=400)
|
||||
|
||||
filepath = os.path.join(UPLOAD_PLAYLIST_DIR, filename)
|
||||
# Never trust the client-supplied filename: always store under a
|
||||
# server-generated .m3u name inside the playlists directory.
|
||||
filename = f"upload_{uuid.uuid4().hex}.m3u"
|
||||
filepath = safe_join(UPLOAD_PLAYLIST_DIR, filename)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
while chunk := await part.read_chunk():
|
||||
f.write(chunk)
|
||||
|
||||
if filepath:
|
||||
return web.json_response({'status': 'ok', 'id_playlist': id_playlist})
|
||||
if id_playlist is None:
|
||||
id_playlist = str(uuid.uuid4())
|
||||
return web.json_response({'status': 'ok', 'id_playlist': id_playlist, 'filename': filename})
|
||||
|
||||
return web.Response(text="No file field found in the request.", status=400)
|
||||
|
||||
@@ -988,6 +971,21 @@ async def update_playlist_comment(request):
|
||||
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
|
||||
async def get_list_playlists(request):
|
||||
active_count = playlists.count_documents({})
|
||||
@@ -1002,7 +1000,11 @@ async def get_list_playlists(request):
|
||||
if active_count == 0:
|
||||
return web.json_response({'status':'no data'})
|
||||
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):
|
||||
@@ -1012,31 +1014,44 @@ async def get_source(request):
|
||||
|
||||
|
||||
# List Channels
|
||||
async def list_channel(request):
|
||||
def _search_channels_sync(channel_to_search, current_page):
|
||||
MAX_RECORDS = 30
|
||||
data = await request.post()
|
||||
|
||||
channel_to_search = unquote(data['keyword'])
|
||||
current_page = int(data['num_page'])
|
||||
escaped_keyword = re.escape(channel_to_search)
|
||||
# Channel names are typically prefixed with a country/format tag (e.g.
|
||||
# "AR - BEIN SPORTS 1 FHD"), so match at word boundaries rather than the
|
||||
# start of the string to rank those above unrelated names that merely
|
||||
# contain the keyword mid-word.
|
||||
prefix_pattern = re.compile(r'\b' + escaped_keyword, re.IGNORECASE)
|
||||
contains_pattern = re.compile(escaped_keyword, re.IGNORECASE)
|
||||
|
||||
logger.info("Search keyword='%s' page=%s", channel_to_search, current_page)
|
||||
regex_pattern = ".*" + re.escape(channel_to_search) + ".*"
|
||||
|
||||
# First pass: compute totals without pulling all docs into memory
|
||||
total_records = 0
|
||||
active_playlists = list(playlists.find({"activation": 1}))
|
||||
per_playlist_counts = []
|
||||
|
||||
# Two relevance tiers, most relevant first: names starting with the keyword,
|
||||
# then everything else that merely contains it. Counted per tier/playlist so
|
||||
# pagination stays a cheap count+skip/limit instead of pulling every match
|
||||
# into memory to sort it.
|
||||
tiers = []
|
||||
for playlist in active_playlists:
|
||||
collection_channels = db_iptv['channels_' + playlist['id_playlist']]
|
||||
count = collection_channels.count_documents({
|
||||
prefix_filter = {'id_playlist': playlist['id_playlist'], 'Name': prefix_pattern}
|
||||
count = collection_channels.count_documents(prefix_filter)
|
||||
if count:
|
||||
tiers.append((playlist, prefix_filter, count))
|
||||
for playlist in active_playlists:
|
||||
collection_channels = db_iptv['channels_' + playlist['id_playlist']]
|
||||
contains_only_filter = {
|
||||
'id_playlist': playlist['id_playlist'],
|
||||
'Name': {'$regex': regex_pattern, '$options': "i"}
|
||||
})
|
||||
per_playlist_counts.append((playlist, count))
|
||||
total_records += count
|
||||
'$and': [{'Name': contains_pattern}, {'Name': {'$not': prefix_pattern}}],
|
||||
}
|
||||
count = collection_channels.count_documents(contains_only_filter)
|
||||
if count:
|
||||
tiers.append((playlist, contains_only_filter, count))
|
||||
|
||||
total_records = sum(count for _, _, count in tiers)
|
||||
|
||||
if total_records == 0:
|
||||
return web.json_response({'num_total_page': 0, 'current_page': 1, 'data': []})
|
||||
return {'num_total_page': 0, 'current_page': 1, 'data': []}
|
||||
|
||||
num_total_page = max(1, (total_records + MAX_RECORDS - 1) // MAX_RECORDS)
|
||||
current_page = max(1, min(current_page, num_total_page))
|
||||
@@ -1046,7 +1061,7 @@ async def list_channel(request):
|
||||
# Second pass: fetch only the page slice
|
||||
serialized_data = []
|
||||
offset = 0
|
||||
for playlist, count in per_playlist_counts:
|
||||
for playlist, mongo_filter, count in tiers:
|
||||
if remaining == 0:
|
||||
break
|
||||
if offset + count <= begin:
|
||||
@@ -1056,16 +1071,30 @@ async def list_channel(request):
|
||||
local_skip = max(0, begin - offset)
|
||||
fetch_limit = min(remaining, count - local_skip)
|
||||
collection_channels = db_iptv['channels_' + playlist['id_playlist']]
|
||||
cursor = collection_channels.find({
|
||||
'id_playlist': playlist['id_playlist'],
|
||||
'Name': {'$regex': regex_pattern, '$options': "i"}
|
||||
}).skip(local_skip).limit(fetch_limit)
|
||||
cursor = collection_channels.find(mongo_filter).skip(local_skip).limit(fetch_limit)
|
||||
|
||||
serialized_data.extend(serialize_doc(doc) for doc in cursor)
|
||||
remaining = MAX_RECORDS - len(serialized_data)
|
||||
offset += count
|
||||
|
||||
return web.json_response({'num_total_page': num_total_page, 'current_page': current_page, 'data': serialized_data})
|
||||
return {'num_total_page': num_total_page, 'current_page': current_page, 'data': serialized_data}
|
||||
|
||||
|
||||
async def list_channel(request):
|
||||
data = await request.post()
|
||||
|
||||
channel_to_search = unquote(data['keyword'])
|
||||
current_page = int(data['num_page'])
|
||||
|
||||
logger.info("Search keyword='%s' page=%s", channel_to_search, current_page)
|
||||
|
||||
# Searching can mean scanning a multi-hundred-thousand-channel collection
|
||||
# with an unindexed regex (several seconds); run it off the event loop so
|
||||
# it doesn't stall every other request/socket while it works.
|
||||
result = await asyncio.get_running_loop().run_in_executor(
|
||||
None, _search_channels_sync, channel_to_search, current_page
|
||||
)
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
|
||||
@@ -1151,8 +1180,6 @@ async def list_logo(request):
|
||||
|
||||
|
||||
async def list_devices(request):
|
||||
json_devices = json.dumps(devices)
|
||||
#return web.json_response(json_devices)
|
||||
return web.json_response(dumps(devices_db.find({}).limit(50)))
|
||||
|
||||
|
||||
@@ -1403,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('/import_xtream_codes', import_xtream_codes)
|
||||
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('/cleanup_xtream_duplicates', cleanup_xtream_duplicates)
|
||||
app.router.add_post('/list_channel', list_channel)
|
||||
@@ -1477,9 +1505,31 @@ async def communication(fromSid, data):
|
||||
print("search request from ", fromSid, flush=True)
|
||||
toSidDevice = data['toSidDevice']
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
async def streaming_error(sid, data):
|
||||
IdDevice = data.get('IdDevice')
|
||||
@@ -1498,12 +1548,14 @@ async def streaming_error(sid, data):
|
||||
@sio.event
|
||||
def disconnect(sid):
|
||||
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)
|
||||
|
||||
|
||||
@sio.event
|
||||
async def polling(sid, data):
|
||||
global devices
|
||||
device_name = data['name']
|
||||
IdDevice = data['IdDevice']
|
||||
mediaRunning = data['mediaRunning']
|
||||
@@ -1515,7 +1567,7 @@ async def polling(sid, data):
|
||||
print('data_running :', data_running, flush=True)
|
||||
try:
|
||||
vpn_status = data['vpn_status']
|
||||
except:
|
||||
except KeyError:
|
||||
vpn_status = 'unknown'
|
||||
|
||||
ip_address = sio_session_ips.get(sid, '')
|
||||
@@ -1533,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}
|
||||
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:
|
||||
new_record = {
|
||||
"id_device": IdDevice,
|
||||
@@ -1564,24 +1623,6 @@ async def polling(sid, data):
|
||||
else:
|
||||
recents.insert_one(new_record)
|
||||
|
||||
test_exist_device = False
|
||||
for device in devices:
|
||||
if device['IdDevice'] == IdDevice:
|
||||
test_exist_device = True
|
||||
device['last_datetime'] = device_last_datetime
|
||||
device['name'] = device_name
|
||||
device['sid'] = sid
|
||||
device['mediaRunning'] = mediaRunning
|
||||
try:
|
||||
device['vpn_status'] = vpn_status
|
||||
except:
|
||||
device['vpn_status'] = 'unknown'
|
||||
|
||||
if not test_exist_device:
|
||||
new_device = { 'IdDevice': IdDevice, 'name' : device_name, 'last_datetime' : device_last_datetime, 'sid':sid ,'mediaRunning' : mediaRunning, 'vpn_status':vpn_status}
|
||||
devices.append(new_device)
|
||||
|
||||
|
||||
|
||||
# Main
|
||||
if __name__ == '__main__':
|
||||
|
||||
+4
-4
@@ -1114,8 +1114,8 @@ def communication(data):
|
||||
except (TypeError, ValueError):
|
||||
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Invalid snapshot interval', 0)
|
||||
else:
|
||||
if interval_seconds < 2:
|
||||
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Snapshot interval must be at least 2 seconds', 0)
|
||||
if interval_seconds < 1:
|
||||
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Snapshot interval must be at least 1 second', 0)
|
||||
else:
|
||||
SNAPSHOT_INTERVAL_SECONDS = interval_seconds
|
||||
sendMessage(
|
||||
@@ -1302,7 +1302,7 @@ def initIPTV():
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# ----------------------------------------------------------------
|
||||
num_version='5.0.6'
|
||||
num_version='5.0.7'
|
||||
# ----------------------------------------------------------------
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
@@ -1328,7 +1328,7 @@ mediaRunning = ''
|
||||
status_vpn = 'disconnected'
|
||||
data_running = {}
|
||||
auto_snapshot_enabled = True
|
||||
SNAPSHOT_INTERVAL_SECONDS = 10
|
||||
SNAPSHOT_INTERVAL_SECONDS = 2
|
||||
POLLING_INTERVAL_SECONDS = 10
|
||||
SOCKET_CONNECT_TIMEOUT = 10
|
||||
SOCKET_RETRY_MIN_INTERVAL = 3
|
||||
|
||||
+112
-11
@@ -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">
|
||||
@@ -1220,7 +1221,7 @@
|
||||
<div class="modal-body">
|
||||
<div id="snapshotIntervalSettingsAlert"></div>
|
||||
<label for="snapshot_interval_input" class="form-label">Seconds between 2 snapshots</label>
|
||||
<input type="number" min="2" step="1" class="form-control" id="snapshot_interval_input" placeholder="10">
|
||||
<input type="number" min="1" step="1" class="form-control" id="snapshot_interval_input" placeholder="2">
|
||||
<div class="form-text">This value is applied to the currently selected device.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -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>
|
||||
@@ -1528,7 +1539,7 @@
|
||||
var PROGRESS_STALE_MS = 15000
|
||||
var playlistImportProgress = 0
|
||||
var playlistImportInFlight = false
|
||||
var selectedDeviceSnapshotIntervalSeconds = 10
|
||||
var selectedDeviceSnapshotIntervalSeconds = 2
|
||||
var currentSelectedDeviceData = null
|
||||
|
||||
var socket = io('https://iptv.mrk.ovh', {
|
||||
@@ -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'])
|
||||
@@ -1710,6 +1732,22 @@
|
||||
$placeholder.hide();
|
||||
}
|
||||
|
||||
// Reloads the currently selected device's snapshot image on its own fast
|
||||
// timer, independent of the 5s full device-list poll, so a low snapshot
|
||||
// interval (see Settings > Snapshot interval) actually feels near-live.
|
||||
function refreshSelectedDeviceSnapshotFast() {
|
||||
var device = currentSelectedDeviceData;
|
||||
if (!device || !device.snapshot_url) {
|
||||
return;
|
||||
}
|
||||
var dataRunning = device.data_running || {};
|
||||
var hasChannel = Object.keys(dataRunning).length > 0 && dataRunning.name_channel;
|
||||
if (!hasChannel) {
|
||||
return;
|
||||
}
|
||||
$('#selectedDeviceSnapshot').attr('src', device.snapshot_url + '?t=' + Date.now());
|
||||
}
|
||||
|
||||
function updateAutoSnapshotToggle(device) {
|
||||
var $toggle = $('#autoSnapshotToggle');
|
||||
if (!device || !device.id_device) {
|
||||
@@ -1723,12 +1761,12 @@
|
||||
|
||||
function updateSnapshotIntervalState(device) {
|
||||
if (!device || !device.id_device) {
|
||||
selectedDeviceSnapshotIntervalSeconds = 10;
|
||||
selectedDeviceSnapshotIntervalSeconds = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
var interval = Number(device.snapshot_interval_seconds);
|
||||
if (!Number.isFinite(interval) || interval < 2) {
|
||||
if (!Number.isFinite(interval) || interval < 1) {
|
||||
interval = selectedDeviceSnapshotIntervalSeconds;
|
||||
}
|
||||
selectedDeviceSnapshotIntervalSeconds = interval;
|
||||
@@ -1746,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');
|
||||
@@ -1774,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);
|
||||
}
|
||||
@@ -1789,6 +1831,7 @@
|
||||
$(window).on('scroll resize', lazyLoad);
|
||||
updateListDevices()
|
||||
setInterval(updateListDevices, 5000)
|
||||
setInterval(refreshSelectedDeviceSnapshotFast, 1000)
|
||||
update_list_playlist()
|
||||
display_most_frequent()
|
||||
setInterval(display_most_frequent, 15000)
|
||||
@@ -1881,6 +1924,13 @@
|
||||
get_search_channel(1)
|
||||
})
|
||||
|
||||
$('#input_search').on('keypress', function(e){
|
||||
if (e.which === 13) {
|
||||
e.preventDefault();
|
||||
get_search_channel(1);
|
||||
}
|
||||
})
|
||||
|
||||
$('#btn_initdb').click(function(){
|
||||
$.get('init_db').done(function(data){
|
||||
window.location.href = "/"
|
||||
@@ -1978,6 +2028,8 @@
|
||||
var name_channel = element['Name']
|
||||
var url_channel = element['url']
|
||||
var id_playlist = element['id_playlist']
|
||||
var topic_channel = element['Topic'] || ''
|
||||
var topicBadge = topic_channel ? `<span class="badge bg-secondary channel-topic-badge">${topic_channel}</span>` : ''
|
||||
var url_channel_macos = url_channel.replace("http://", 'rtmp://')
|
||||
|
||||
var oid = ''
|
||||
@@ -2005,6 +2057,7 @@
|
||||
<img src="logos/${id_channel}.jpg" class="channel-row-logo"
|
||||
onerror="this.onerror=null; this.src='get_logo?id_channel=${id_channel}&id_playlist=${id_playlist}';">
|
||||
<span class="channel-row-name text-truncate">${name_channel}</span>
|
||||
${topicBadge}
|
||||
${colorCircle}
|
||||
</div>
|
||||
|
||||
@@ -2024,7 +2077,7 @@
|
||||
data-name_channel="${name_channel}"
|
||||
data-id_channel="${id_channel}"
|
||||
data-id_playlist="${id_playlist}">${name_channel}</p>
|
||||
<div class="d-flex justify-content-center align-items-center gap-2">${colorCircle}</div>
|
||||
<div class="d-flex justify-content-center align-items-center gap-2">${colorCircle}${topicBadge}</div>
|
||||
<div class="d-flex justify-content-center gap-2 mt-3">
|
||||
<button data-display_url="${url_channel}" class="btn_display_url btn btn-sm btn-outline-info">url</button>
|
||||
<a class="btn btn-sm btn-primary" href="vlc://${url_channel_macos}">vlc ios</a>
|
||||
@@ -2065,17 +2118,26 @@
|
||||
function get_search_channel(num_page) {
|
||||
var keyword = $('#input_search').val()
|
||||
if (keyword.length>0) {
|
||||
$('#pagination-container').empty();
|
||||
$('#listChannels').show().html('<div class="text-center text-muted py-4"><i class="fas fa-spinner fa-spin"></i> Searching...</div>');
|
||||
$.post('list_channel', {keyword:encodeURIComponent(keyword), num_page:num_page}).done(function(payload){
|
||||
$('#list_channels').html('')
|
||||
var html =''
|
||||
num_total_page = payload.num_total_page
|
||||
current_page = payload.current_page
|
||||
result_search = payload.data
|
||||
console.log(num_total_page, current_page)
|
||||
renderPagination(num_total_page, current_page);
|
||||
|
||||
if (!result_search || result_search.length === 0) {
|
||||
$('#pagination-container').empty();
|
||||
$('#listChannels').html('<div class="text-center text-muted py-4">No channels found.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
renderPagination(num_total_page, current_page);
|
||||
displayResult()
|
||||
lazyLoad();
|
||||
}).fail(function(){
|
||||
$('#pagination-container').empty();
|
||||
$('#listChannels').html('<div class="text-center text-danger py-4">Search failed. Please try again.</div>');
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2403,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>-->\
|
||||
@@ -2432,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>\
|
||||
@@ -2812,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();
|
||||
@@ -2871,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);
|
||||
@@ -2903,6 +3001,7 @@
|
||||
title: title,
|
||||
date_end: endDate,
|
||||
comment: playlistComment,
|
||||
max_connections: maxConnections,
|
||||
sidClient: $('#sidClient').val(),
|
||||
selectedColor: selectedColor,
|
||||
xtream_server: xtreamServer,
|
||||
@@ -2967,6 +3066,7 @@
|
||||
$('#add_playlist_alert').html('<div class="alert alert-success" role="alert">Upload successful!</div>');
|
||||
var response = JSON.parse(xhr.response);
|
||||
var id_playlist = response['id_playlist'];
|
||||
$("#fileName").val(response['filename']);
|
||||
if (response.status == 'ok') {
|
||||
$.post('analyse_m3u_file', {
|
||||
'id_playlist': id_playlist,
|
||||
@@ -2974,6 +3074,7 @@
|
||||
'url': playlistUrl,
|
||||
'date_end': endDate,
|
||||
'comment': playlistComment,
|
||||
'max_connections': maxConnections,
|
||||
'sidClient': $('#sidClient').val(),
|
||||
'filename': $("#fileName").val(),
|
||||
'selectedColor': selectedColor
|
||||
@@ -3139,9 +3240,9 @@
|
||||
}
|
||||
|
||||
var intervalValue = Number($('#snapshot_interval_input').val());
|
||||
if (!Number.isInteger(intervalValue) || intervalValue < 2) {
|
||||
if (!Number.isInteger(intervalValue) || intervalValue < 1) {
|
||||
$('#snapshot_interval_input').addClass('is-invalid');
|
||||
$('#snapshotIntervalSettingsAlert').html('<div class="alert alert-danger" role="alert">Enter an integer value greater than or equal to 2.</div>');
|
||||
$('#snapshotIntervalSettingsAlert').html('<div class="alert alert-danger" role="alert">Enter an integer value greater than or equal to 1.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user