Add device-side log file, remote terminal, and streaming error reporting
IPTV.py now writes logs to a rotating file and forwards critical VLC playback errors (start timeout, MediaPlayerEncounteredError) to the server instead of losing everything to unredirected print() calls. The web UI's device menu gets a working Terminal (to run diagnostic commands like tail on the log) and a Recent errors view backed by a capped last_errors list on each device document, keeping DB volume bounded to the last 5 entries per device. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
10fd5106f8
commit
3eb0d5e8cd
@@ -74,6 +74,7 @@ recents = db_iptv['recents']
|
|||||||
settings_collection = db_iptv['settings']
|
settings_collection = db_iptv['settings']
|
||||||
|
|
||||||
devices_db = db_iptv['devices']
|
devices_db = db_iptv['devices']
|
||||||
|
MAX_STREAMING_ERRORS_PER_DEVICE = 5
|
||||||
devices = []
|
devices = []
|
||||||
active_xtream_imports = {}
|
active_xtream_imports = {}
|
||||||
sio_session_ips = {}
|
sio_session_ips = {}
|
||||||
@@ -1471,6 +1472,21 @@ async def communication(fromSid, data):
|
|||||||
await sio.emit('communication', data, room=toSidDevice ,callback=messageReceived)
|
await sio.emit('communication', data, room=toSidDevice ,callback=messageReceived)
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def streaming_error(sid, data):
|
||||||
|
IdDevice = data.get('IdDevice')
|
||||||
|
message = data.get('message', '')
|
||||||
|
if not IdDevice:
|
||||||
|
return
|
||||||
|
|
||||||
|
print('streaming_error from device :', IdDevice, message, flush=True)
|
||||||
|
error_entry = {'message': str(message)[:500], 'timestamp': time.time()}
|
||||||
|
devices_db.update_one(
|
||||||
|
{'id_device': IdDevice},
|
||||||
|
{'$push': {'last_errors': {'$each': [error_entry], '$slice': -MAX_STREAMING_ERRORS_PER_DEVICE}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
def disconnect(sid):
|
def disconnect(sid):
|
||||||
sio_session_ips.pop(sid, None)
|
sio_session_ips.pop(sid, None)
|
||||||
|
|||||||
+100
-54
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
import vlc
|
import vlc
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -12,7 +13,6 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import queue
|
import queue
|
||||||
import psutil
|
import psutil
|
||||||
import logging
|
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
@@ -44,6 +44,31 @@ sio = socketio.Client(
|
|||||||
)
|
)
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# LOGGING (fichier local avec rotation, consultable via le Terminal
|
||||||
|
# du menu device sur le site, ex: tail -n 200 iptv.log)
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
LOG_FILE_PATH = "/home/pi/Documents/iptv.log"
|
||||||
|
LOG_FILE_MAX_BYTES = 1_000_000
|
||||||
|
LOG_FILE_BACKUP_COUNT = 5
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
_log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
_console_handler = logging.StreamHandler()
|
||||||
|
_console_handler.setFormatter(_log_formatter)
|
||||||
|
logger.addHandler(_console_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_file_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
LOG_FILE_PATH, maxBytes=LOG_FILE_MAX_BYTES, backupCount=LOG_FILE_BACKUP_COUNT
|
||||||
|
)
|
||||||
|
_file_handler.setFormatter(_log_formatter)
|
||||||
|
logger.addHandler(_file_handler)
|
||||||
|
except OSError as exc:
|
||||||
|
logger.warning("Unable to open log file %s: %s", LOG_FILE_PATH, exc)
|
||||||
|
|
||||||
|
|
||||||
class IdleScreenManager:
|
class IdleScreenManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -167,7 +192,7 @@ class IdleScreenManager:
|
|||||||
self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)
|
self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)
|
||||||
self.root.mainloop()
|
self.root.mainloop()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("Unable to start idle screen:", exc, flush=True)
|
logger.warning("Unable to start idle screen: %s", exc)
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
|
|
||||||
def _process_commands(self):
|
def _process_commands(self):
|
||||||
@@ -358,7 +383,7 @@ def fetch_channel_logo_photo(item):
|
|||||||
try:
|
try:
|
||||||
photo_image = ImageTk.PhotoImage(background)
|
photo_image = ImageTk.PhotoImage(background)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("Unable to create Tk logo image:", exc, flush=True)
|
logger.warning("Unable to create Tk logo image: %s", exc)
|
||||||
return None, f"Tk image creation failed: {exc}"
|
return None, f"Tk image creation failed: {exc}"
|
||||||
|
|
||||||
logo_cache[cache_key] = photo_image
|
logo_cache[cache_key] = photo_image
|
||||||
@@ -482,7 +507,7 @@ def sync_recent_channels_from_server():
|
|||||||
if normalized_items:
|
if normalized_items:
|
||||||
return save_recent_channels_to_db(normalized_items)
|
return save_recent_channels_to_db(normalized_items)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("Unable to refresh recent channels from server:", exc, flush=True)
|
logger.warning("Unable to refresh recent channels from server: %s", exc)
|
||||||
|
|
||||||
return recent
|
return recent
|
||||||
|
|
||||||
@@ -494,7 +519,34 @@ def focus_vlc_player():
|
|||||||
try:
|
try:
|
||||||
media_principal.set_fullscreen(True)
|
media_principal.set_fullscreen(True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("Unable to focus VLC player:", exc, flush=True)
|
logger.warning("Unable to focus VLC player: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
|
STREAMING_ERROR_REPORT_MIN_INTERVAL_SECONDS = 5
|
||||||
|
_last_streaming_error_report_ts = 0
|
||||||
|
|
||||||
|
|
||||||
|
def report_streaming_error(message):
|
||||||
|
"""Log a critical streaming error and forward it to the server (best effort,
|
||||||
|
rate-limited so a burst of VLC errors doesn't flood the socket)."""
|
||||||
|
global _last_streaming_error_report_ts
|
||||||
|
|
||||||
|
logger.error(message)
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - _last_streaming_error_report_ts < STREAMING_ERROR_REPORT_MIN_INTERVAL_SECONDS:
|
||||||
|
return
|
||||||
|
_last_streaming_error_report_ts = now
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sio.connected:
|
||||||
|
sio.emit('streaming_error', {'IdDevice': IdDevice, 'message': str(message)})
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Unable to report streaming error to server: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
|
def on_vlc_encountered_error(event):
|
||||||
|
report_streaming_error("VLC encountered a playback error (channel: %s)" % mediaRunning)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_vlc_playback_start():
|
def wait_for_vlc_playback_start():
|
||||||
@@ -507,11 +559,11 @@ def wait_for_vlc_playback_start():
|
|||||||
focus_vlc_player()
|
focus_vlc_player()
|
||||||
return True
|
return True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("Unable to determine VLC playback state:", exc, flush=True)
|
logger.warning("Unable to determine VLC playback state: %s", exc)
|
||||||
break
|
break
|
||||||
time.sleep(VLC_PLAYBACK_POLL_INTERVAL_SECONDS)
|
time.sleep(VLC_PLAYBACK_POLL_INTERVAL_SECONDS)
|
||||||
|
|
||||||
print("VLC did not report playback start before timeout", flush=True)
|
report_streaming_error("VLC did not report playback start before timeout (channel: %s)" % mediaRunning)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -617,6 +669,7 @@ def new_media_player(url):
|
|||||||
media.add_option(f":vout={forced_vout}")
|
media.add_option(f":vout={forced_vout}")
|
||||||
|
|
||||||
player.set_media(media)
|
player.set_media(media)
|
||||||
|
player.event_manager().event_attach(vlc.EventType.MediaPlayerEncounteredError, on_vlc_encountered_error)
|
||||||
return player
|
return player
|
||||||
|
|
||||||
|
|
||||||
@@ -650,7 +703,7 @@ def capture_and_upload_snapshot():
|
|||||||
snapshot_path = get_snapshot_filepath()
|
snapshot_path = get_snapshot_filepath()
|
||||||
snapshot_status = media_principal.video_take_snapshot(0, snapshot_path, 0, 0)
|
snapshot_status = media_principal.video_take_snapshot(0, snapshot_path, 0, 0)
|
||||||
if snapshot_status != 0:
|
if snapshot_status != 0:
|
||||||
print('snapshot capture failed', flush=True)
|
logger.error("Snapshot capture failed")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
upload_snapshot_file(snapshot_path)
|
upload_snapshot_file(snapshot_path)
|
||||||
@@ -663,7 +716,7 @@ def snapshot_loop():
|
|||||||
if auto_snapshot_enabled and data_running:
|
if auto_snapshot_enabled and data_running:
|
||||||
capture_and_upload_snapshot()
|
capture_and_upload_snapshot()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('snapshot loop error', exc, flush=True)
|
logger.warning("Snapshot loop error: %s", exc)
|
||||||
time.sleep(SNAPSHOT_INTERVAL_SECONDS)
|
time.sleep(SNAPSHOT_INTERVAL_SECONDS)
|
||||||
|
|
||||||
def RepresentsInt(s):
|
def RepresentsInt(s):
|
||||||
@@ -677,7 +730,7 @@ def convertMillis(millis):
|
|||||||
seconds=(millis/1000)%60
|
seconds=(millis/1000)%60
|
||||||
minutes=(millis/(1000*60))%60
|
minutes=(millis/(1000*60))%60
|
||||||
hours=(millis/(1000*60*60))%24
|
hours=(millis/(1000*60*60))%24
|
||||||
print(str(seconds)+" "+str(minutes)+" "+ str(hours))
|
logger.debug("%s %s %s", seconds, minutes, hours)
|
||||||
return seconds, minutes, hours
|
return seconds, minutes, hours
|
||||||
|
|
||||||
def is_vpn_connected():
|
def is_vpn_connected():
|
||||||
@@ -730,12 +783,12 @@ def findInfo(tag, s):
|
|||||||
|
|
||||||
def restart_process():
|
def restart_process():
|
||||||
try:
|
try:
|
||||||
print('restart new process')
|
logger.info("Restarting process")
|
||||||
p = psutil.Process(os.getpid())
|
p = psutil.Process(os.getpid())
|
||||||
for handler in p.get_open_files() + p.connections():
|
for handler in p.get_open_files() + p.connections():
|
||||||
os.close(handler.fd)
|
os.close(handler.fd)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
python = sys.executable
|
python = sys.executable
|
||||||
os.execl(python, python, *sys.argv)
|
os.execl(python, python, *sys.argv)
|
||||||
@@ -768,7 +821,7 @@ def display_overlay_message(message, duration_ms=10000):
|
|||||||
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, duration_ms)
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, duration_ms)
|
||||||
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8)
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('Unable to display overlay message:', exc, flush=True)
|
logger.warning("Unable to display overlay message: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
def playMedia(id_channel, url_channel, name_channel, id_playlist):
|
def playMedia(id_channel, url_channel, name_channel, id_playlist):
|
||||||
@@ -781,20 +834,20 @@ def playMedia(id_channel, url_channel, name_channel, id_playlist):
|
|||||||
media_principal.stop()
|
media_principal.stop()
|
||||||
media_principal.release()
|
media_principal.release()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Unable to reset VLC player:", e, flush=True)
|
logger.warning("Unable to reset VLC player: %s", e)
|
||||||
|
|
||||||
media_principal = new_media_player(url_channel)
|
media_principal = new_media_player(url_channel)
|
||||||
try:
|
try:
|
||||||
media_principal.set_fullscreen(True)
|
media_principal.set_fullscreen(True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Unable to enable fullscreen:", e, flush=True)
|
logger.warning("Unable to enable fullscreen: %s", e)
|
||||||
media_principal.audio_set_volume(200)
|
media_principal.audio_set_volume(200)
|
||||||
media_principal.play()
|
media_principal.play()
|
||||||
start_playback_transition()
|
start_playback_transition()
|
||||||
mediaRunning = name_channel
|
mediaRunning = name_channel
|
||||||
data_running = {'name_channel':name_channel, 'id_channel':id_channel, 'id_playlist':id_playlist, 'url_channel': url_channel, 'progress':0}
|
data_running = {'name_channel':name_channel, 'id_channel':id_channel, 'id_playlist':id_playlist, 'url_channel': url_channel, 'progress':0}
|
||||||
prepend_recent_channel(id_channel, url_channel, name_channel, id_playlist)
|
prepend_recent_channel(id_channel, url_channel, name_channel, id_playlist)
|
||||||
print(' DATA : ', data_running, flush=True)
|
logger.info("Playback started: %s", data_running)
|
||||||
|
|
||||||
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Enable, 1)
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Enable, 1)
|
||||||
media_principal.video_set_marquee_string(vlc.VideoMarqueeOption.Text, name_channel)
|
media_principal.video_set_marquee_string(vlc.VideoMarqueeOption.Text, name_channel)
|
||||||
@@ -813,7 +866,7 @@ def update_source():
|
|||||||
updated_file.write(r.content)
|
updated_file.write(r.content)
|
||||||
os.replace(temp_path, path + 'IPTV.py')
|
os.replace(temp_path, path + 'IPTV.py')
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('error update_source', exc, flush=True)
|
logger.error("Error during update_source: %s", exc)
|
||||||
if os.path.exists(temp_path):
|
if os.path.exists(temp_path):
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
raise
|
raise
|
||||||
@@ -821,14 +874,14 @@ def update_source():
|
|||||||
|
|
||||||
def wait_for_internet_connection():
|
def wait_for_internet_connection():
|
||||||
while True:
|
while True:
|
||||||
print("Checking connexion ...", flush=True)
|
logger.info("Checking connexion ...")
|
||||||
for check_url, verify_ssl in CONNECTIVITY_CHECK_TARGETS:
|
for check_url, verify_ssl in CONNECTIVITY_CHECK_TARGETS:
|
||||||
try:
|
try:
|
||||||
requests.get(check_url, timeout=CONNECTIVITY_CHECK_TIMEOUT_SECONDS, verify=verify_ssl)
|
requests.get(check_url, timeout=CONNECTIVITY_CHECK_TIMEOUT_SECONDS, verify=verify_ssl)
|
||||||
print(f"Connectivity check succeeded with {check_url}", flush=True)
|
logger.info("Connectivity check succeeded with %s", check_url)
|
||||||
return
|
return
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
print(f"Connectivity check failed for {check_url}: {exc}", flush=True)
|
logger.warning("Connectivity check failed for %s: %s", check_url, exc)
|
||||||
|
|
||||||
time.sleep(CONNECTIVITY_CHECK_RETRY_SECONDS)
|
time.sleep(CONNECTIVITY_CHECK_RETRY_SECONDS)
|
||||||
|
|
||||||
@@ -856,18 +909,18 @@ def ensure_socket_connection(force_reset=False):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
wait_for_internet_connection()
|
wait_for_internet_connection()
|
||||||
print('Attempting socket connection...', flush=True)
|
logger.info("Attempting socket connection...")
|
||||||
sio.connect(
|
sio.connect(
|
||||||
url_update_server,
|
url_update_server,
|
||||||
wait=True,
|
wait=True,
|
||||||
wait_timeout=SOCKET_CONNECT_TIMEOUT,
|
wait_timeout=SOCKET_CONNECT_TIMEOUT,
|
||||||
transports=['websocket', 'polling']
|
transports=['websocket', 'polling']
|
||||||
)
|
)
|
||||||
print('Socket connection ready', flush=True)
|
logger.info("Socket connection ready")
|
||||||
server_connection_ready = True
|
server_connection_ready = True
|
||||||
return True
|
return True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('Socket reconnection failed', exc, flush=True)
|
logger.warning("Socket reconnection failed: %s", exc)
|
||||||
server_connection_ready = False
|
server_connection_ready = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -876,7 +929,7 @@ def polling_server():
|
|||||||
global media_principal
|
global media_principal
|
||||||
global data_running
|
global data_running
|
||||||
global status_vpn
|
global status_vpn
|
||||||
print('polling_server')
|
logger.info("Polling loop started")
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if not sio.connected:
|
if not sio.connected:
|
||||||
@@ -890,7 +943,7 @@ def polling_server():
|
|||||||
percentage = media_principal.get_position()*100
|
percentage = media_principal.get_position()*100
|
||||||
|
|
||||||
if data_running:
|
if data_running:
|
||||||
print(data_running_tmp)
|
logger.debug("Current playback state: %s", data_running_tmp)
|
||||||
data_running = {'name_channel':data_running_tmp['name_channel'], 'id_channel':data_running_tmp['id_channel'], 'id_playlist':data_running_tmp['id_playlist'], 'url_channel':data_running_tmp['url_channel'], 'progress':milli, 'percentage':percentage}
|
data_running = {'name_channel':data_running_tmp['name_channel'], 'id_channel':data_running_tmp['id_channel'], 'id_playlist':data_running_tmp['id_playlist'], 'url_channel':data_running_tmp['url_channel'], 'progress':milli, 'percentage':percentage}
|
||||||
sio.emit('polling', {
|
sio.emit('polling', {
|
||||||
'name': name_device,
|
'name': name_device,
|
||||||
@@ -902,7 +955,7 @@ def polling_server():
|
|||||||
'snapshot_interval_seconds': SNAPSHOT_INTERVAL_SECONDS
|
'snapshot_interval_seconds': SNAPSHOT_INTERVAL_SECONDS
|
||||||
})
|
})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print('Polling failed, will reconnect', exc, flush=True)
|
logger.warning("Polling failed, will reconnect: %s", exc)
|
||||||
ensure_socket_connection(force_reset=True)
|
ensure_socket_connection(force_reset=True)
|
||||||
time.sleep(POLLING_INTERVAL_SECONDS)
|
time.sleep(POLLING_INTERVAL_SECONDS)
|
||||||
|
|
||||||
@@ -911,23 +964,23 @@ def polling_server():
|
|||||||
|
|
||||||
def sendMessage(fromSidDevice, toSidDevice, typeCommand, payload, num_packet):
|
def sendMessage(fromSidDevice, toSidDevice, typeCommand, payload, num_packet):
|
||||||
new_data = {'fromSidDevice': fromSidDevice, 'toSidDevice': toSidDevice, 'typeCommand': typeCommand, 'payload': payload, 'packet':num_packet}
|
new_data = {'fromSidDevice': fromSidDevice, 'toSidDevice': toSidDevice, 'typeCommand': typeCommand, 'payload': payload, 'packet':num_packet}
|
||||||
print(new_data)
|
logger.info("Sending message: %s", new_data)
|
||||||
if sio.connected or ensure_socket_connection():
|
if sio.connected or ensure_socket_connection():
|
||||||
sio.emit('communication', new_data)
|
sio.emit('communication', new_data)
|
||||||
else:
|
else:
|
||||||
print('Socket disconnected, unable to send message', flush=True)
|
logger.warning("Socket disconnected, unable to send message")
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
def connect():
|
def connect():
|
||||||
global server_connection_ready
|
global server_connection_ready
|
||||||
server_connection_ready = True
|
server_connection_ready = True
|
||||||
print('connection established')
|
logger.info("Connection established")
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
def connect_error(data):
|
def connect_error(data):
|
||||||
print('connection error', data, flush=True)
|
logger.error("Connection error: %s", data)
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
@@ -973,14 +1026,14 @@ def communication(data):
|
|||||||
# PLAY A CHANNEL ON
|
# PLAY A CHANNEL ON
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
if typeCommand == "on":
|
if typeCommand == "on":
|
||||||
print ("switch on")
|
logger.info("Switch on requested")
|
||||||
data_channel = data['payload']
|
data_channel = data['payload']
|
||||||
name_channel = data_channel['name']
|
name_channel = data_channel['name']
|
||||||
url_channel = data_channel['url']
|
url_channel = data_channel['url']
|
||||||
id_channel = data_channel['id_channel']
|
id_channel = data_channel['id_channel']
|
||||||
id_playlist = data_channel['id_playlist']
|
id_playlist = data_channel['id_playlist']
|
||||||
|
|
||||||
print(id_channel, name_channel, url_channel, flush=True)
|
logger.info("Playing channel id=%s name=%s url=%s", id_channel, name_channel, url_channel)
|
||||||
playMedia(id_channel, url_channel, name_channel, id_playlist)
|
playMedia(id_channel, url_channel, name_channel, id_playlist)
|
||||||
sendMessage(toSidDevice, fromSidDevice, 'playing', name_channel, 0)
|
sendMessage(toSidDevice, fromSidDevice, 'playing', name_channel, 0)
|
||||||
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Starting media', 0)
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Starting media', 0)
|
||||||
@@ -1105,7 +1158,7 @@ def communication(data):
|
|||||||
if typeCommand == "changeDeviceName":
|
if typeCommand == "changeDeviceName":
|
||||||
global name_device
|
global name_device
|
||||||
name_device = data['payload']
|
name_device = data['payload']
|
||||||
print(name_device)
|
logger.info("Change device name to: %s", name_device)
|
||||||
query = "UPDATE settings SET valueParameter = '" + name_device + "' WHERE nameParameter = 'deviceName'"
|
query = "UPDATE settings SET valueParameter = '" + name_device + "' WHERE nameParameter = 'deviceName'"
|
||||||
cur_ = con.cursor()
|
cur_ = con.cursor()
|
||||||
cur_.execute(query)
|
cur_.execute(query)
|
||||||
@@ -1134,7 +1187,7 @@ def communication(data):
|
|||||||
# --------------------------------
|
# --------------------------------
|
||||||
if typeCommand == "takeSnapshot":
|
if typeCommand == "takeSnapshot":
|
||||||
try:
|
try:
|
||||||
print('take snapshot')
|
logger.info("Take snapshot requested")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
capture_and_upload_snapshot()
|
capture_and_upload_snapshot()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1152,22 +1205,19 @@ def communication(data):
|
|||||||
# Command line
|
# Command line
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
if typeCommand == "command_line":
|
if typeCommand == "command_line":
|
||||||
|
logger.info("Command line requested: %s", data['payload'])
|
||||||
records = re.split(r' ', data['payload'])
|
records = re.split(r' ', data['payload'])
|
||||||
#result = subprocess.run(records, capture_output=True, text=True)
|
process = subprocess.Popen(records, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||||
process = subprocess.Popen(records, stdout=subprocess.PIPE, text=True)
|
|
||||||
for line in process.stdout:
|
for line in process.stdout:
|
||||||
print(line, end="")
|
logger.info("command_line output: %s", line.rstrip())
|
||||||
sendMessage(toSidDevice, fromSidDevice, 'display_command_result', line, 0)
|
sendMessage(toSidDevice, fromSidDevice, 'display_command_result', line, 0)
|
||||||
|
|
||||||
|
|
||||||
#print(result.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
def disconnect():
|
def disconnect():
|
||||||
global server_connection_ready
|
global server_connection_ready
|
||||||
server_connection_ready = False
|
server_connection_ready = False
|
||||||
print('Disconnected from server', flush=True)
|
logger.warning("Disconnected from server")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1181,7 +1231,7 @@ def initDB():
|
|||||||
cur = con.cursor()
|
cur = con.cursor()
|
||||||
cur.execute('SELECT SQLITE_VERSION()')
|
cur.execute('SELECT SQLITE_VERSION()')
|
||||||
data = cur.fetchone()
|
data = cur.fetchone()
|
||||||
print ("Connexion succeed : SQLite version: %s" % data)
|
logger.info("Connexion succeed : SQLite version: %s", data)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS recent_channels_cache (
|
CREATE TABLE IF NOT EXISTS recent_channels_cache (
|
||||||
@@ -1197,7 +1247,7 @@ def initDB():
|
|||||||
con.commit()
|
con.commit()
|
||||||
|
|
||||||
except (lite.Error, e):
|
except (lite.Error, e):
|
||||||
print ("Error %s:" % e.args[0])
|
logger.error("Error %s:", e.args[0])
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -1217,9 +1267,9 @@ def initIPTV():
|
|||||||
record = cur_.fetchone()
|
record = cur_.fetchone()
|
||||||
if record:
|
if record:
|
||||||
IdDevice = record[0]
|
IdDevice = record[0]
|
||||||
print(IdDevice)
|
logger.info("Device ID: %s", IdDevice)
|
||||||
else:
|
else:
|
||||||
print("No ID device found")
|
logger.info("No ID device found, generating one")
|
||||||
IdDevice = str(uuid.uuid4())
|
IdDevice = str(uuid.uuid4())
|
||||||
query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('IdDevice', '"+IdDevice+"')"
|
query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('IdDevice', '"+IdDevice+"')"
|
||||||
cur_ = con.cursor()
|
cur_ = con.cursor()
|
||||||
@@ -1234,7 +1284,7 @@ def initIPTV():
|
|||||||
valueParameter = cur_.fetchone()
|
valueParameter = cur_.fetchone()
|
||||||
if valueParameter is not None:
|
if valueParameter is not None:
|
||||||
name_device = valueParameter
|
name_device = valueParameter
|
||||||
print('Update device name : ' + str(name_device))
|
logger.info("Update device name : %s", name_device)
|
||||||
|
|
||||||
|
|
||||||
# SAVE PID
|
# SAVE PID
|
||||||
@@ -1252,7 +1302,7 @@ def initIPTV():
|
|||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
num_version='5.0.4'
|
num_version='5.0.6'
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
|
|
||||||
@@ -1266,7 +1316,7 @@ db_file = 'config.db'
|
|||||||
path = os.path.dirname(os.path.realpath(__file__))+'/'
|
path = os.path.dirname(os.path.realpath(__file__))+'/'
|
||||||
con = None
|
con = None
|
||||||
pid = os.getppid()
|
pid = os.getppid()
|
||||||
print('PID : ', pid)
|
logger.info("PID : %s", pid)
|
||||||
|
|
||||||
channels = []
|
channels = []
|
||||||
playlists = []
|
playlists = []
|
||||||
@@ -1317,12 +1367,8 @@ auth_file = "/home/pi/Documents/auth.txt"
|
|||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# ----------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
# (logging configuré plus haut dans le fichier, juste après les imports)
|
||||||
logging.basicConfig(
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# LOOP PRINCIPAL
|
# LOOP PRINCIPAL
|
||||||
|
|||||||
+112
-4
@@ -961,6 +961,8 @@
|
|||||||
<li><a onclick="openSnapshotIntervalSettings()" class="dropdown-item" href="#">Snapshot interval</a></li>
|
<li><a onclick="openSnapshotIntervalSettings()" class="dropdown-item" href="#">Snapshot interval</a></li>
|
||||||
<li><a onclick="openGeminiKeySettings()" class="dropdown-item" href="#">Gemini API Key</a></li>
|
<li><a onclick="openGeminiKeySettings()" class="dropdown-item" href="#">Gemini API Key</a></li>
|
||||||
<li><a onclick="openTerminal()" class="dropdown-item" href="#">Terminal</a></li>
|
<li><a onclick="openTerminal()" class="dropdown-item" href="#">Terminal</a></li>
|
||||||
|
<li><a onclick="viewDeviceLogs()" class="dropdown-item" href="#">View logs</a></li>
|
||||||
|
<li><a onclick="openRecentErrors()" class="dropdown-item" href="#">Recent errors</a></li>
|
||||||
<li><a onclick="takeSnapshot()" class="dropdown-item" href="#">Take Snapshot</a></li>
|
<li><a onclick="takeSnapshot()" class="dropdown-item" href="#">Take Snapshot</a></li>
|
||||||
<li><a onclick="getIP()" class="dropdown-item" href="#">Get IP</a></li>
|
<li><a onclick="getIP()" class="dropdown-item" href="#">Get IP</a></li>
|
||||||
<li class="dropdown-submenu">
|
<li class="dropdown-submenu">
|
||||||
@@ -1152,19 +1154,35 @@
|
|||||||
|
|
||||||
<!-- ZONE MODAL TERMINAL -->
|
<!-- ZONE MODAL TERMINAL -->
|
||||||
<div class="modal fade" id="modalTerminal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
<div class="modal fade" id="modalTerminal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-sm" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Terminal </h5>
|
<h5 class="modal-title">Terminal </h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<span class="input-group-text" id="basic-addon1">Command</span>
|
<span class="input-group-text" id="basic-addon1">Command</span>
|
||||||
<input type="text" class="form-control" placeholder="Text Command" aria-label="Command" aria-describedby="basic-addon1" id="txt_command">
|
<input type="text" class="form-control" placeholder="Text Command" aria-label="Command" aria-describedby="basic-addon1" id="txt_command">
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary">Send command</button>
|
<button type="button" class="btn btn-primary" id="btn_send_terminal_command">Send command</button>
|
||||||
<div class="alert alert-secondary" role="alert" id="txt_result_command">
|
<button type="button" class="btn btn-outline-secondary" id="btn_view_device_logs">View streaming logs</button>
|
||||||
</div>
|
<pre class="alert alert-secondary mt-3 mb-0" role="alert" id="txt_result_command" style="max-height: 320px; overflow-y: auto; white-space: pre-wrap; text-align: left;"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ZONE MODAL RECENT ERRORS -->
|
||||||
|
<div class="modal fade" id="modalRecentErrors" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Recent streaming errors</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="recent_errors_list" style="max-height: 320px; overflow-y: auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1511,6 +1529,7 @@
|
|||||||
var playlistImportProgress = 0
|
var playlistImportProgress = 0
|
||||||
var playlistImportInFlight = false
|
var playlistImportInFlight = false
|
||||||
var selectedDeviceSnapshotIntervalSeconds = 10
|
var selectedDeviceSnapshotIntervalSeconds = 10
|
||||||
|
var currentSelectedDeviceData = null
|
||||||
|
|
||||||
var socket = io('https://iptv.mrk.ovh', {
|
var socket = io('https://iptv.mrk.ovh', {
|
||||||
secure: true,
|
secure: true,
|
||||||
@@ -1549,6 +1568,13 @@
|
|||||||
updateListDevices()
|
updateListDevices()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RECEPTION TERMINAL COMMAND OUTPUT
|
||||||
|
if (data['typeCommand'] == 'display_command_result') {
|
||||||
|
var resultBox = document.getElementById('txt_result_command');
|
||||||
|
$(resultBox).text($(resultBox).text() + data['payload']);
|
||||||
|
resultBox.scrollTop = resultBox.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// PROGRESS BAR DISPLAY
|
// PROGRESS BAR DISPLAY
|
||||||
if (data['typeCommand'] == 'progress_analysis') {
|
if (data['typeCommand'] == 'progress_analysis') {
|
||||||
$('#progress-container').show()
|
$('#progress-container').show()
|
||||||
@@ -2661,6 +2687,7 @@
|
|||||||
if (isCurrentDevice) {
|
if (isCurrentDevice) {
|
||||||
currentDeviceUpdated = true;
|
currentDeviceUpdated = true;
|
||||||
selectedDeviceSnapshotData = dataJSON[i];
|
selectedDeviceSnapshotData = dataJSON[i];
|
||||||
|
currentSelectedDeviceData = dataJSON[i];
|
||||||
|
|
||||||
$('#sidDevice').val( dataJSON[i].sid)
|
$('#sidDevice').val( dataJSON[i].sid)
|
||||||
var data_running = dataJSON[i].data_running || {}
|
var data_running = dataJSON[i].data_running || {}
|
||||||
@@ -2684,6 +2711,7 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
if (!currentDeviceUpdated) {
|
if (!currentDeviceUpdated) {
|
||||||
|
currentSelectedDeviceData = null;
|
||||||
updateCurrentDeviceStream({label: 'No data', className: 'bg-secondary'}, false);
|
updateCurrentDeviceStream({label: 'No data', className: 'bg-secondary'}, false);
|
||||||
updateSelectedDeviceSnapshot(null, false);
|
updateSelectedDeviceSnapshot(null, false);
|
||||||
updateAutoSnapshotToggle(null);
|
updateAutoSnapshotToggle(null);
|
||||||
@@ -3170,6 +3198,86 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- TERMINAL / DEVICE LOGS ---
|
||||||
|
var DEVICE_LOG_FILE_PATH = '/home/pi/Documents/iptv.log';
|
||||||
|
|
||||||
|
function openTerminal() {
|
||||||
|
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
|
||||||
|
$('#noDeviceModal').modal('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#txt_command').val('');
|
||||||
|
$('#txt_result_command').text('');
|
||||||
|
$('#modalTerminal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTerminalCommand(command) {
|
||||||
|
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
|
||||||
|
$('#modalTerminal').modal('hide');
|
||||||
|
$('#noDeviceModal').modal('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = (typeof command === 'string') ? command : $('#txt_command').val().trim();
|
||||||
|
if (!cmd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#txt_command').val(cmd);
|
||||||
|
$('#txt_result_command').text('');
|
||||||
|
socket.emit('communication', {
|
||||||
|
fromSidDevice: $('#sidClient').val(),
|
||||||
|
toSidDevice: $('#sidDevice').val(),
|
||||||
|
typeCommand: 'command_line',
|
||||||
|
payload: cmd,
|
||||||
|
packet: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#btn_send_terminal_command').click(function () {
|
||||||
|
sendTerminalCommand();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btn_view_device_logs').click(function () {
|
||||||
|
sendTerminalCommand('tail -n 200 ' + DEVICE_LOG_FILE_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
|
function viewDeviceLogs() {
|
||||||
|
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
|
||||||
|
$('#noDeviceModal').modal('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#txt_result_command').text('');
|
||||||
|
$('#modalTerminal').modal('show');
|
||||||
|
sendTerminalCommand('tail -n 200 ' + DEVICE_LOG_FILE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#txt_command').on('keypress', function (e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
sendTerminalCommand();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function openRecentErrors() {
|
||||||
|
if ($('#sidDevice').val().length === 0 || $('#id_device').val().length === 0) {
|
||||||
|
$('#noDeviceModal').modal('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors = (currentSelectedDeviceData && currentSelectedDeviceData.last_errors) || [];
|
||||||
|
var html = '';
|
||||||
|
if (errors.length === 0) {
|
||||||
|
html = '<div class="text-muted">No recent errors.</div>';
|
||||||
|
} else {
|
||||||
|
errors.slice().reverse().forEach(function (err) {
|
||||||
|
var when = err.timestamp ? new Date(err.timestamp * 1000).toLocaleString() : '';
|
||||||
|
html += '<div class="alert alert-danger py-2 mb-2"><strong>' + when + '</strong><br>' + $('<div>').text(err.message).html() + '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('#recent_errors_list').html(html);
|
||||||
|
$('#modalRecentErrors').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
// --- AI SEARCH ---
|
// --- AI SEARCH ---
|
||||||
function openAiSearch() {
|
function openAiSearch() {
|
||||||
$('#aiSearchAlert').html('');
|
$('#aiSearchAlert').html('');
|
||||||
|
|||||||
Reference in New Issue
Block a user