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>
1391 lines
44 KiB
Python
1391 lines
44 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
# pylint: disable=W0613, C0116
|
|
# type: ignore[union-attr]
|
|
|
|
|
|
import logging
|
|
import logging.handlers
|
|
import vlc
|
|
import os
|
|
import re
|
|
import sys
|
|
import json
|
|
import queue
|
|
import psutil
|
|
import requests
|
|
import time
|
|
import threading
|
|
import tempfile
|
|
import socketio
|
|
import uuid
|
|
import subprocess
|
|
import tkinter as tk
|
|
from io import BytesIO
|
|
from wifi import Cell, Scheme
|
|
import sqlite3 as lite
|
|
from PIL import Image
|
|
from datetime import datetime
|
|
import urllib3
|
|
|
|
try:
|
|
from PIL import ImageTk
|
|
PIL_IMAGETK_AVAILABLE = True
|
|
except Exception:
|
|
ImageTk = None
|
|
PIL_IMAGETK_AVAILABLE = False
|
|
|
|
sio = socketio.Client(
|
|
ssl_verify=False,
|
|
reconnection=True,
|
|
reconnection_attempts=0,
|
|
reconnection_delay=2,
|
|
reconnection_delay_max=15,
|
|
)
|
|
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:
|
|
def __init__(self):
|
|
self.root = None
|
|
self.thread = None
|
|
self.command_queue = queue.Queue()
|
|
self.started = False
|
|
self.visible = False
|
|
self.enabled = True
|
|
self.clock_var = None
|
|
self.subtitle_var = None
|
|
self.footer_var = None
|
|
self.cards_frame = None
|
|
self.empty_var = None
|
|
self.empty_label = None
|
|
self.logo_refs = []
|
|
|
|
def start(self):
|
|
if self.started or not self.enabled or not idle_screen_supported():
|
|
return
|
|
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
self.thread.start()
|
|
self.started = True
|
|
|
|
def show(self):
|
|
if not idle_screen_supported():
|
|
return
|
|
self.start()
|
|
self.command_queue.put(("show", None))
|
|
|
|
def hide(self):
|
|
if not self.started:
|
|
return
|
|
self.command_queue.put(("hide", None))
|
|
|
|
def refresh(self):
|
|
if not self.started:
|
|
return
|
|
self.command_queue.put(("refresh", None))
|
|
|
|
def _run(self):
|
|
try:
|
|
self.root = tk.Tk()
|
|
self.root.configure(bg="#08111d")
|
|
self.root.title("IPTV Idle Screen")
|
|
self.root.attributes("-fullscreen", True)
|
|
self.root.attributes("-topmost", True)
|
|
self.root.bind("<Escape>", lambda event: self.root.attributes("-fullscreen", False))
|
|
|
|
header = tk.Frame(self.root, bg="#08111d")
|
|
header.pack(fill="x", padx=60, pady=(45, 20))
|
|
|
|
title = tk.Label(
|
|
header,
|
|
text="Latest Channels",
|
|
fg="#f7fbff",
|
|
bg="#08111d",
|
|
font=("Helvetica", 34, "bold"),
|
|
anchor="w",
|
|
)
|
|
title.pack(fill="x")
|
|
|
|
self.subtitle_var = tk.StringVar(value="No channel running")
|
|
subtitle = tk.Label(
|
|
header,
|
|
textvariable=self.subtitle_var,
|
|
fg="#9ab4c8",
|
|
bg="#08111d",
|
|
font=("Helvetica", 18),
|
|
anchor="w",
|
|
)
|
|
subtitle.pack(fill="x", pady=(8, 0))
|
|
|
|
self.clock_var = tk.StringVar(value="")
|
|
clock_label = tk.Label(
|
|
header,
|
|
textvariable=self.clock_var,
|
|
fg="#6ee7b7",
|
|
bg="#08111d",
|
|
font=("Helvetica", 18, "bold"),
|
|
anchor="e",
|
|
)
|
|
clock_label.pack(fill="x", pady=(12, 0))
|
|
|
|
recent_frame = tk.Frame(self.root, bg="#0f1b2d", highlightbackground="#16324b", highlightthickness=1)
|
|
recent_frame.pack(fill="both", expand=True, padx=60, pady=(10, 20))
|
|
|
|
self.empty_var = tk.StringVar(value="Loading recent channels...")
|
|
self.empty_label = tk.Label(
|
|
recent_frame,
|
|
textvariable=self.empty_var,
|
|
fg="#f7fbff",
|
|
bg="#0f1b2d",
|
|
font=("Helvetica", 20),
|
|
justify="center",
|
|
anchor="center",
|
|
padx=28,
|
|
pady=28,
|
|
)
|
|
self.empty_label.pack(fill="both", expand=True)
|
|
|
|
self.cards_frame = tk.Frame(recent_frame, bg="#0f1b2d")
|
|
self.cards_frame.pack(fill="both", expand=True, padx=18, pady=18)
|
|
for column_index in range(IDLE_SCREEN_CARD_COLUMNS):
|
|
self.cards_frame.grid_columnconfigure(column_index, weight=1, uniform="idle_cards")
|
|
|
|
self.footer_var = tk.StringVar(value="Waiting for a channel selection")
|
|
footer = tk.Label(
|
|
self.root,
|
|
textvariable=self.footer_var,
|
|
fg="#7b95aa",
|
|
bg="#08111d",
|
|
font=("Helvetica", 14),
|
|
anchor="w",
|
|
)
|
|
footer.pack(fill="x", padx=60, pady=(0, 30))
|
|
|
|
self.root.withdraw()
|
|
self.root.after(250, self._process_commands)
|
|
self.root.after(1000, self._tick_clock)
|
|
self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)
|
|
self.root.mainloop()
|
|
except Exception as exc:
|
|
logger.warning("Unable to start idle screen: %s", exc)
|
|
self.enabled = False
|
|
|
|
def _process_commands(self):
|
|
if self.root is None:
|
|
return
|
|
|
|
while True:
|
|
try:
|
|
action, payload = self.command_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
if action == "show":
|
|
self.visible = True
|
|
self._render_recent_channels()
|
|
self.root.deiconify()
|
|
self.root.attributes("-fullscreen", True)
|
|
self.root.attributes("-topmost", True)
|
|
self.root.lift()
|
|
elif action == "hide":
|
|
self.visible = False
|
|
self.root.withdraw()
|
|
elif action == "refresh":
|
|
self._render_recent_channels()
|
|
|
|
self.root.after(250, self._process_commands)
|
|
|
|
def _tick_clock(self):
|
|
if self.root is None:
|
|
return
|
|
|
|
self.clock_var.set(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
self.root.after(1000, self._tick_clock)
|
|
|
|
def _refresh_recent_data(self):
|
|
if self.root is None:
|
|
return
|
|
|
|
if self.visible:
|
|
sync_recent_channels_from_server()
|
|
self._render_recent_channels()
|
|
|
|
self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)
|
|
|
|
def _render_recent_channels(self):
|
|
items = recent[:MAX_RECENT_CHANNELS]
|
|
if self.cards_frame is None or self.empty_label is None:
|
|
return
|
|
|
|
self.logo_refs = []
|
|
for child in self.cards_frame.winfo_children():
|
|
child.destroy()
|
|
|
|
if not items:
|
|
self.empty_var.set("No recent channel history available.")
|
|
self.empty_label.lift()
|
|
self.footer_var.set("The screen will update when the server sends history or a channel is played locally.")
|
|
return
|
|
|
|
self.empty_label.lower()
|
|
for index, item in enumerate(items, start=1):
|
|
card = tk.Frame(
|
|
self.cards_frame,
|
|
bg="#13253a",
|
|
highlightbackground="#21425f",
|
|
highlightthickness=1,
|
|
padx=18,
|
|
pady=18,
|
|
)
|
|
row_index = (index - 1) // IDLE_SCREEN_CARD_COLUMNS
|
|
column_index = (index - 1) % IDLE_SCREEN_CARD_COLUMNS
|
|
card.grid(row=row_index, column=column_index, sticky="nsew", padx=12, pady=12)
|
|
|
|
logo_image, logo_status = fetch_channel_logo_photo(item)
|
|
logo_label = tk.Label(card, bg="#13253a")
|
|
logo_label.pack(pady=(0, 16))
|
|
if logo_image is not None:
|
|
logo_label.configure(image=logo_image)
|
|
self.logo_refs.append(logo_image)
|
|
else:
|
|
logo_label.configure(
|
|
text="No Logo",
|
|
fg="#9ab4c8",
|
|
font=("Helvetica", 16, "bold"),
|
|
)
|
|
|
|
name = item.get("name_channel") or item.get("name") or "Unknown channel"
|
|
name_label = tk.Label(
|
|
card,
|
|
text=name,
|
|
fg="#f7fbff",
|
|
bg="#13253a",
|
|
font=("Helvetica", 18, "bold"),
|
|
wraplength=240,
|
|
justify="center",
|
|
)
|
|
name_label.pack(fill="x")
|
|
|
|
last_played_at = format_recent_timestamp(item.get("last_played_at"))
|
|
meta_label = tk.Label(
|
|
card,
|
|
text=f"Last played: {last_played_at}",
|
|
fg="#89a5ba",
|
|
bg="#13253a",
|
|
font=("Helvetica", 13),
|
|
justify="center",
|
|
)
|
|
meta_label.pack(fill="x", pady=(10, 0))
|
|
|
|
self.footer_var.set(f"Showing {len(items)} recent channels cached locally and refreshed from the server")
|
|
|
|
|
|
def idle_screen_supported():
|
|
if sys.platform.startswith("linux"):
|
|
return bool(os.environ.get("DISPLAY", "").strip())
|
|
return True
|
|
|
|
|
|
def format_recent_timestamp(timestamp_value):
|
|
if not timestamp_value:
|
|
return "-"
|
|
|
|
normalized_value = str(timestamp_value).replace("Z", "+00:00")
|
|
for parser in (datetime.fromisoformat,):
|
|
try:
|
|
return parser(normalized_value).strftime("%Y-%m-%d %H:%M")
|
|
except ValueError:
|
|
pass
|
|
|
|
for pattern in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
|
|
try:
|
|
return datetime.strptime(str(timestamp_value), pattern).strftime("%Y-%m-%d %H:%M")
|
|
except ValueError:
|
|
pass
|
|
|
|
return str(timestamp_value)
|
|
|
|
|
|
def build_logo_urls(item):
|
|
id_channel = item.get("id_channel", "").strip()
|
|
if not id_channel:
|
|
return []
|
|
|
|
return [url_update_server + f"logos/{id_channel}.jpg"]
|
|
|
|
|
|
def create_placeholder_logo():
|
|
return Image.new("RGB", IDLE_SCREEN_LOGO_SIZE, color="#20364d")
|
|
|
|
|
|
def fetch_channel_logo_photo(item):
|
|
if not server_connection_ready:
|
|
return None, "Logo skipped: server connection not ready"
|
|
|
|
if not PIL_IMAGETK_AVAILABLE:
|
|
return None, "Logo skipped: PIL ImageTk unavailable"
|
|
|
|
cache_key = f"{item.get('id_channel', '')}:{item.get('id_playlist', '')}"
|
|
cached_logo = logo_cache.get(cache_key)
|
|
if cached_logo is not None:
|
|
return cached_logo, "Logo loaded from cache"
|
|
|
|
image_obj = None
|
|
last_error = "Logo fetch failed"
|
|
for logo_url in build_logo_urls(item):
|
|
try:
|
|
response = requests.get(logo_url, timeout=LOGO_REQUEST_TIMEOUT_SECONDS, verify=False)
|
|
if not response.ok:
|
|
last_error = f"HTTP error {response.status_code}"
|
|
continue
|
|
|
|
image_obj = Image.open(BytesIO(response.content)).convert("RGB")
|
|
last_error = f"HTTP {response.status_code}, {len(response.content)} bytes"
|
|
break
|
|
except Exception as exc:
|
|
last_error = f"Request/decode error: {exc}"
|
|
image_obj = None
|
|
|
|
if image_obj is None:
|
|
return None, last_error
|
|
|
|
image_obj.thumbnail(IDLE_SCREEN_LOGO_SIZE, IMAGE_RESAMPLING_LANCZOS)
|
|
background = Image.new("RGB", IDLE_SCREEN_LOGO_SIZE, color="#0c1827")
|
|
offset_x = max((IDLE_SCREEN_LOGO_SIZE[0] - image_obj.width) // 2, 0)
|
|
offset_y = max((IDLE_SCREEN_LOGO_SIZE[1] - image_obj.height) // 2, 0)
|
|
background.paste(image_obj, (offset_x, offset_y))
|
|
|
|
try:
|
|
photo_image = ImageTk.PhotoImage(background)
|
|
except Exception as exc:
|
|
logger.warning("Unable to create Tk logo image: %s", exc)
|
|
return None, f"Tk image creation failed: {exc}"
|
|
|
|
logo_cache[cache_key] = photo_image
|
|
return photo_image, last_error
|
|
|
|
|
|
def normalize_recent_channel(item):
|
|
return {
|
|
"id_channel": str(item.get("id_channel") or item.get("_id") or ""),
|
|
"id_playlist": str(item.get("id_playlist") or ""),
|
|
"name_channel": str(item.get("name_channel") or item.get("name") or "Unknown channel"),
|
|
"url_channel": str(item.get("url_channel") or item.get("url") or ""),
|
|
"last_played_at": str(item.get("last_played_at") or item.get("datetime") or datetime.now().isoformat(timespec="seconds")),
|
|
}
|
|
|
|
|
|
def load_recent_channels_from_db():
|
|
global recent
|
|
|
|
with db_lock:
|
|
cur_ = con.cursor()
|
|
cur_.execute(
|
|
"""
|
|
SELECT id_channel, id_playlist, name_channel, url_channel, last_played_at
|
|
FROM recent_channels_cache
|
|
ORDER BY position ASC
|
|
"""
|
|
)
|
|
rows = cur_.fetchall()
|
|
|
|
recent = [
|
|
{
|
|
"id_channel": str(row[0] or ""),
|
|
"id_playlist": str(row[1] or ""),
|
|
"name_channel": str(row[2] or ""),
|
|
"url_channel": str(row[3] or ""),
|
|
"last_played_at": str(row[4] or ""),
|
|
}
|
|
for row in rows
|
|
]
|
|
return recent
|
|
|
|
|
|
def save_recent_channels_to_db(items):
|
|
global recent
|
|
|
|
normalized_items = [normalize_recent_channel(item) for item in items[:MAX_RECENT_CHANNELS]]
|
|
|
|
with db_lock:
|
|
cur_ = con.cursor()
|
|
cur_.execute("DELETE FROM recent_channels_cache")
|
|
for position, item in enumerate(normalized_items):
|
|
cur_.execute(
|
|
"""
|
|
INSERT INTO recent_channels_cache (
|
|
position, id_channel, id_playlist, name_channel, url_channel, last_played_at
|
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
position,
|
|
item["id_channel"],
|
|
item["id_playlist"],
|
|
item["name_channel"],
|
|
item["url_channel"],
|
|
item["last_played_at"],
|
|
),
|
|
)
|
|
con.commit()
|
|
|
|
recent = normalized_items
|
|
idle_screen_manager.refresh()
|
|
return normalized_items
|
|
|
|
|
|
def prepend_recent_channel(id_channel, url_channel, name_channel, id_playlist):
|
|
current_items = load_recent_channels_from_db()
|
|
new_item = normalize_recent_channel(
|
|
{
|
|
"id_channel": id_channel,
|
|
"id_playlist": id_playlist,
|
|
"name_channel": name_channel,
|
|
"url_channel": url_channel,
|
|
"last_played_at": datetime.now().isoformat(timespec="seconds"),
|
|
}
|
|
)
|
|
|
|
filtered_items = [
|
|
item for item in current_items
|
|
if not (
|
|
item.get("id_channel") == new_item["id_channel"]
|
|
and item.get("url_channel") == new_item["url_channel"]
|
|
)
|
|
]
|
|
save_recent_channels_to_db([new_item] + filtered_items)
|
|
|
|
|
|
def parse_server_json_payload(response):
|
|
payload = response.json()
|
|
if isinstance(payload, str):
|
|
payload = json.loads(payload)
|
|
return payload
|
|
|
|
|
|
def sync_recent_channels_from_server():
|
|
if not server_connection_ready:
|
|
return recent
|
|
|
|
try:
|
|
response = requests.get(
|
|
url_update_server + "recents",
|
|
headers={"Cache-Control": "no-cache"},
|
|
timeout=10,
|
|
verify=False,
|
|
)
|
|
response.raise_for_status()
|
|
payload = parse_server_json_payload(response)
|
|
if not isinstance(payload, list):
|
|
return recent
|
|
|
|
normalized_items = [normalize_recent_channel(item) for item in payload[:MAX_RECENT_CHANNELS]]
|
|
if normalized_items:
|
|
return save_recent_channels_to_db(normalized_items)
|
|
except Exception as exc:
|
|
logger.warning("Unable to refresh recent channels from server: %s", exc)
|
|
|
|
return recent
|
|
|
|
|
|
def focus_vlc_player():
|
|
global media_principal
|
|
|
|
idle_screen_manager.hide()
|
|
try:
|
|
media_principal.set_fullscreen(True)
|
|
except Exception as exc:
|
|
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():
|
|
global media_principal
|
|
|
|
deadline = time.time() + VLC_PLAYBACK_START_TIMEOUT_SECONDS
|
|
while time.time() < deadline:
|
|
try:
|
|
if media_principal.is_playing():
|
|
focus_vlc_player()
|
|
return True
|
|
except Exception as exc:
|
|
logger.warning("Unable to determine VLC playback state: %s", exc)
|
|
break
|
|
time.sleep(VLC_PLAYBACK_POLL_INTERVAL_SECONDS)
|
|
|
|
report_streaming_error("VLC did not report playback start before timeout (channel: %s)" % mediaRunning)
|
|
return False
|
|
|
|
|
|
def start_playback_transition():
|
|
threading.Thread(target=wait_for_vlc_playback_start, daemon=True).start()
|
|
|
|
|
|
def show_idle_screen():
|
|
if mediaRunning == '':
|
|
idle_screen_manager.show()
|
|
|
|
|
|
def hide_idle_screen():
|
|
idle_screen_manager.hide()
|
|
|
|
|
|
def pulse_audio_available():
|
|
pulse_server = os.environ.get("PULSE_SERVER", "").strip()
|
|
if pulse_server:
|
|
return True
|
|
|
|
runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "").strip()
|
|
pulse_candidates = []
|
|
if runtime_dir:
|
|
pulse_candidates.append(os.path.join(runtime_dir, "pulse", "native"))
|
|
|
|
pulse_candidates.append(f"/run/user/{os.getuid()}/pulse/native")
|
|
return any(os.path.exists(candidate) for candidate in pulse_candidates)
|
|
|
|
|
|
def ensure_runtime_environment():
|
|
runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "").strip()
|
|
if not runtime_dir:
|
|
fallback_runtime = f"/tmp/iptv-runtime-{os.getuid()}"
|
|
os.makedirs(fallback_runtime, exist_ok=True)
|
|
os.environ["XDG_RUNTIME_DIR"] = fallback_runtime
|
|
|
|
|
|
def default_vlc_vout():
|
|
forced_vout = os.environ.get("IPTV_VLC_VOUT", "").strip()
|
|
if forced_vout:
|
|
return forced_vout
|
|
|
|
platform = sys.platform.lower()
|
|
if platform.startswith("linux"):
|
|
if os.environ.get("DISPLAY", "").strip():
|
|
return "xcb_x11"
|
|
return "fb"
|
|
|
|
return ""
|
|
|
|
def build_vlc_instance():
|
|
vlc_options = [
|
|
"--intf=dummy",
|
|
"--fullscreen",
|
|
"--no-osd",
|
|
"--no-video-title-show",
|
|
"--no-snapshot-preview",
|
|
|
|
# network/live buffering
|
|
"--network-caching=3000",
|
|
"--live-caching=3000",
|
|
"--file-caching=1000",
|
|
|
|
# late-frame handling
|
|
"--drop-late-frames",
|
|
"--skip-frames",
|
|
|
|
# reduce timing pressure on live streams
|
|
"--clock-jitter=0",
|
|
"--clock-synchro=0",
|
|
|
|
# troubleshooting
|
|
"--verbose=2",
|
|
]
|
|
|
|
forced_aout = os.environ.get("IPTV_VLC_AOUT", "").strip()
|
|
if forced_aout:
|
|
vlc_options.append(f"--aout={forced_aout}")
|
|
elif not pulse_audio_available():
|
|
vlc_options.append("--aout=alsa")
|
|
|
|
forced_vout = os.environ.get("IPTV_VLC_VOUT", "").strip()
|
|
if forced_vout:
|
|
vlc_options.append(f"--vout={forced_vout}")
|
|
|
|
return vlc.Instance(*vlc_options)
|
|
|
|
|
|
def new_media_player(url):
|
|
player = vlc_instance.media_player_new()
|
|
media = vlc_instance.media_new(url)
|
|
|
|
media.add_option(":network-caching=3000")
|
|
media.add_option(":live-caching=3000")
|
|
media.add_option(":drop-late-frames")
|
|
media.add_option(":skip-frames")
|
|
media.add_option(":clock-jitter=0")
|
|
media.add_option(":clock-synchro=0")
|
|
|
|
forced_vout = os.environ.get("IPTV_VLC_VOUT", "").strip()
|
|
if forced_vout:
|
|
media.add_option(f":vout={forced_vout}")
|
|
|
|
player.set_media(media)
|
|
player.event_manager().event_attach(vlc.EventType.MediaPlayerEncounteredError, on_vlc_encountered_error)
|
|
return player
|
|
|
|
|
|
def get_snapshot_filepath():
|
|
return os.path.join(tempfile.gettempdir(), f"iptv_snapshot_{IdDevice}.jpg")
|
|
|
|
|
|
def upload_snapshot_file(snapshot_path):
|
|
if not os.path.exists(snapshot_path):
|
|
return False
|
|
|
|
with open(snapshot_path, "rb") as snapshot_file:
|
|
response = requests.post(
|
|
url_update_server + 'upload_snapshot',
|
|
data={'id_device': IdDevice},
|
|
files={'file': (os.path.basename(snapshot_path), snapshot_file, 'image/jpeg')},
|
|
timeout=10,
|
|
verify=False
|
|
)
|
|
response.raise_for_status()
|
|
return True
|
|
|
|
|
|
def capture_and_upload_snapshot():
|
|
global media_principal
|
|
global data_running
|
|
|
|
if not data_running:
|
|
return False
|
|
|
|
snapshot_path = get_snapshot_filepath()
|
|
snapshot_status = media_principal.video_take_snapshot(0, snapshot_path, 0, 0)
|
|
if snapshot_status != 0:
|
|
logger.error("Snapshot capture failed")
|
|
return False
|
|
|
|
upload_snapshot_file(snapshot_path)
|
|
return True
|
|
|
|
|
|
def snapshot_loop():
|
|
while True:
|
|
try:
|
|
if auto_snapshot_enabled and data_running:
|
|
capture_and_upload_snapshot()
|
|
except Exception as exc:
|
|
logger.warning("Snapshot loop error: %s", exc)
|
|
time.sleep(SNAPSHOT_INTERVAL_SECONDS)
|
|
|
|
def RepresentsInt(s):
|
|
try:
|
|
int(s)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
def convertMillis(millis):
|
|
seconds=(millis/1000)%60
|
|
minutes=(millis/(1000*60))%60
|
|
hours=(millis/(1000*60*60))%24
|
|
logger.debug("%s %s %s", seconds, minutes, hours)
|
|
return seconds, minutes, hours
|
|
|
|
def is_vpn_connected():
|
|
try:
|
|
output = subprocess.check_output("ip a | grep tun0", shell=True)
|
|
return "tun0" in output.decode()
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
|
|
def connect_vpn():
|
|
global vpn_process
|
|
|
|
if is_vpn_connected():
|
|
return "VPN already connected"
|
|
|
|
subprocess.call("sudo pkill openvpn", shell=True)
|
|
subprocess.call("sudo ip link delete tun0", shell=True)
|
|
|
|
time.sleep(1)
|
|
vpn_process = subprocess.Popen(
|
|
["sudo", "openvpn", "--config", vpn_config, "--auth-user-pass", auth_file],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True
|
|
)
|
|
|
|
# Wait for connection confirmation
|
|
for _ in range(30):
|
|
if is_vpn_connected():
|
|
return "VPN connected"
|
|
time.sleep(1)
|
|
|
|
return "VPN connection failed"
|
|
|
|
def disconnect_vpn():
|
|
global vpn_process
|
|
subprocess.call("sudo pkill openvpn", shell=True)
|
|
subprocess.call("sudo ip link delete tun0", shell=True)
|
|
vpn_process = None
|
|
return "VPN disconnected"
|
|
|
|
def findInfo(tag, s):
|
|
end = '"'
|
|
if (s.find(tag)>-1):
|
|
t = s[s.find(tag)+len(tag)+2:s.rfind(end)]
|
|
return t[0:t.find(end)]
|
|
else:
|
|
return ""
|
|
|
|
|
|
def restart_process():
|
|
try:
|
|
logger.info("Restarting process")
|
|
p = psutil.Process(os.getpid())
|
|
for handler in p.get_open_files() + p.connections():
|
|
os.close(handler.fd)
|
|
except Exception as e:
|
|
logger.error(e)
|
|
|
|
python = sys.executable
|
|
os.execl(python, python, *sys.argv)
|
|
|
|
|
|
def restart_main_iptv_process():
|
|
python = sys.executable
|
|
main_script = os.path.join(path, 'IPTV.py')
|
|
os.execl(python, python, main_script)
|
|
|
|
|
|
def stop():
|
|
global media_principal
|
|
global mediaRunning
|
|
global data_running
|
|
data_running = {}
|
|
mediaRunning = ''
|
|
media_principal.stop()
|
|
sync_recent_channels_from_server()
|
|
show_idle_screen()
|
|
|
|
|
|
def display_overlay_message(message, duration_ms=10000):
|
|
global media_principal
|
|
|
|
try:
|
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Enable, 1)
|
|
media_principal.video_set_marquee_string(vlc.VideoMarqueeOption.Text, message)
|
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Size, 42)
|
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, duration_ms)
|
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8)
|
|
except Exception as exc:
|
|
logger.warning("Unable to display overlay message: %s", exc)
|
|
|
|
|
|
def playMedia(id_channel, url_channel, name_channel, id_playlist):
|
|
global media_principal
|
|
global mediaRunning
|
|
global recent
|
|
global data_running
|
|
|
|
try:
|
|
media_principal.stop()
|
|
media_principal.release()
|
|
except Exception as e:
|
|
logger.warning("Unable to reset VLC player: %s", e)
|
|
|
|
media_principal = new_media_player(url_channel)
|
|
try:
|
|
media_principal.set_fullscreen(True)
|
|
except Exception as e:
|
|
logger.warning("Unable to enable fullscreen: %s", e)
|
|
media_principal.audio_set_volume(200)
|
|
media_principal.play()
|
|
start_playback_transition()
|
|
mediaRunning = name_channel
|
|
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)
|
|
logger.info("Playback started: %s", data_running)
|
|
|
|
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_int(vlc.VideoMarqueeOption.Size, 48) # Font size
|
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, 7000) # Duration in milliseconds
|
|
media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8) # Position on screen
|
|
|
|
|
|
def update_source():
|
|
global path
|
|
r = requests.get(url_update_server+'get_source', headers={'Cache-Control': 'no-cache'}, verify=False)
|
|
r.raise_for_status()
|
|
temp_path = path + 'IPTV.py.tmp'
|
|
try:
|
|
with open(temp_path, 'wb') as updated_file:
|
|
updated_file.write(r.content)
|
|
os.replace(temp_path, path + 'IPTV.py')
|
|
except Exception as exc:
|
|
logger.error("Error during update_source: %s", exc)
|
|
if os.path.exists(temp_path):
|
|
os.remove(temp_path)
|
|
raise
|
|
|
|
|
|
def wait_for_internet_connection():
|
|
while True:
|
|
logger.info("Checking connexion ...")
|
|
for check_url, verify_ssl in CONNECTIVITY_CHECK_TARGETS:
|
|
try:
|
|
requests.get(check_url, timeout=CONNECTIVITY_CHECK_TIMEOUT_SECONDS, verify=verify_ssl)
|
|
logger.info("Connectivity check succeeded with %s", check_url)
|
|
return
|
|
except requests.RequestException as exc:
|
|
logger.warning("Connectivity check failed for %s: %s", check_url, exc)
|
|
|
|
time.sleep(CONNECTIVITY_CHECK_RETRY_SECONDS)
|
|
|
|
|
|
def ensure_socket_connection(force_reset=False):
|
|
global last_connect_attempt_ts
|
|
global server_connection_ready
|
|
|
|
with socket_connect_lock:
|
|
if sio.connected and not force_reset:
|
|
server_connection_ready = True
|
|
return True
|
|
|
|
now = time.time()
|
|
if not force_reset and (now - last_connect_attempt_ts) < SOCKET_RETRY_MIN_INTERVAL:
|
|
return sio.connected
|
|
|
|
last_connect_attempt_ts = now
|
|
|
|
try:
|
|
if force_reset:
|
|
try:
|
|
sio.disconnect()
|
|
except Exception:
|
|
pass
|
|
|
|
wait_for_internet_connection()
|
|
logger.info("Attempting socket connection...")
|
|
sio.connect(
|
|
url_update_server,
|
|
wait=True,
|
|
wait_timeout=SOCKET_CONNECT_TIMEOUT,
|
|
transports=['websocket', 'polling']
|
|
)
|
|
logger.info("Socket connection ready")
|
|
server_connection_ready = True
|
|
return True
|
|
except Exception as exc:
|
|
logger.warning("Socket reconnection failed: %s", exc)
|
|
server_connection_ready = False
|
|
return False
|
|
|
|
|
|
def polling_server():
|
|
global media_principal
|
|
global data_running
|
|
global status_vpn
|
|
logger.info("Polling loop started")
|
|
while True:
|
|
try:
|
|
if not sio.connected:
|
|
ensure_socket_connection()
|
|
time.sleep(POLLING_INTERVAL_SECONDS)
|
|
continue
|
|
|
|
#r = requests.get(url_update_server+'ping?name=hirondelle&', allow_redirects=True)
|
|
data_running_tmp = data_running
|
|
milli = media_principal.get_time()
|
|
percentage = media_principal.get_position()*100
|
|
|
|
if data_running:
|
|
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}
|
|
sio.emit('polling', {
|
|
'name': name_device,
|
|
'IdDevice': IdDevice,
|
|
'mediaRunning': mediaRunning,
|
|
'vpn_status': is_vpn_connected(),
|
|
'data_running': data_running,
|
|
'auto_snapshot_enabled': auto_snapshot_enabled,
|
|
'snapshot_interval_seconds': SNAPSHOT_INTERVAL_SECONDS
|
|
})
|
|
except Exception as exc:
|
|
logger.warning("Polling failed, will reconnect: %s", exc)
|
|
ensure_socket_connection(force_reset=True)
|
|
time.sleep(POLLING_INTERVAL_SECONDS)
|
|
|
|
|
|
|
|
|
|
def sendMessage(fromSidDevice, toSidDevice, typeCommand, payload, num_packet):
|
|
new_data = {'fromSidDevice': fromSidDevice, 'toSidDevice': toSidDevice, 'typeCommand': typeCommand, 'payload': payload, 'packet':num_packet}
|
|
logger.info("Sending message: %s", new_data)
|
|
if sio.connected or ensure_socket_connection():
|
|
sio.emit('communication', new_data)
|
|
else:
|
|
logger.warning("Socket disconnected, unable to send message")
|
|
|
|
|
|
@sio.event
|
|
def connect():
|
|
global server_connection_ready
|
|
server_connection_ready = True
|
|
logger.info("Connection established")
|
|
|
|
|
|
@sio.event
|
|
def connect_error(data):
|
|
logger.error("Connection error: %s", data)
|
|
|
|
|
|
@sio.event
|
|
def communication(data):
|
|
|
|
toSidDevice = data['toSidDevice']
|
|
fromSidDevice = data['fromSidDevice']
|
|
typeCommand = data['typeCommand']
|
|
|
|
global recent
|
|
global media_principal
|
|
global mediaRunning
|
|
global data_running
|
|
|
|
# --------------------------------
|
|
# OFF
|
|
# --------------------------------
|
|
if typeCommand == "off":
|
|
stop()
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Off', 0)
|
|
sendMessage(toSidDevice, fromSidDevice, 'playing', 'no channel', 0)
|
|
|
|
# --------------------------------
|
|
# VPN MANAGEMENT
|
|
# --------------------------------
|
|
if typeCommand == "vpn":
|
|
command_vpn = data['payload']
|
|
if command_vpn == "connect":
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Start VPN Connexion', 0)
|
|
result = connect_vpn()
|
|
elif command_vpn == "status":
|
|
result = "connected" if is_vpn_connected() else "disconnected"
|
|
elif command_vpn == "disconnect":
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Start VPN Disconnexion', 0)
|
|
result = disconnect_vpn()
|
|
else:
|
|
result = "Unknown command"
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', result, 0)
|
|
|
|
|
|
|
|
# --------------------------------
|
|
# PLAY A CHANNEL ON
|
|
# --------------------------------
|
|
if typeCommand == "on":
|
|
logger.info("Switch on requested")
|
|
data_channel = data['payload']
|
|
name_channel = data_channel['name']
|
|
url_channel = data_channel['url']
|
|
id_channel = data_channel['id_channel']
|
|
id_playlist = data_channel['id_playlist']
|
|
|
|
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)
|
|
sendMessage(toSidDevice, fromSidDevice, 'playing', name_channel, 0)
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Starting media', 0)
|
|
|
|
|
|
# --------------------------------
|
|
# GET IP ADDRESS
|
|
# --------------------------------
|
|
if typeCommand == "getIP":
|
|
my_IP = requests.get("https://api.ipify.org").text
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'IP : ' + my_IP, 0)
|
|
|
|
# --------------------------------
|
|
# GET CHANNEL RUNNING
|
|
# --------------------------------
|
|
if typeCommand == "getChannelRunning":
|
|
new_data ={}
|
|
if mediaRunning == '':
|
|
mr = 'no channel'
|
|
percentage = 0
|
|
milli = 0
|
|
id_channel = ''
|
|
else:
|
|
mr = mediaRunning
|
|
percentage = media_principal.get_position()*100
|
|
milli = media_principal.get_time()
|
|
id_channel = data_running['id_channel']
|
|
|
|
new_data = {'name': mr, 'percentage':percentage, 'millisec':milli, 'id_channel': id_channel}
|
|
sendMessage(toSidDevice, fromSidDevice, 'playing', new_data, 0)
|
|
|
|
# --------------------------------
|
|
# SET SUBTITLE
|
|
# --------------------------------
|
|
if typeCommand == "subtitle":
|
|
track = data['payload']
|
|
media_principal.video_set_spu(track)
|
|
sendMessage(data['toSidDevice'], data['fromSidDevice'], 'warning', 'Track Num ' + str(track), 0)
|
|
|
|
# --------------------------------
|
|
# RESUME
|
|
# --------------------------------
|
|
if typeCommand == "resume":
|
|
media_principal.set_pause(0)
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Resume done', 0)
|
|
|
|
# --------------------------------
|
|
# PAUSE
|
|
# --------------------------------
|
|
if typeCommand == "pause":
|
|
media_principal.set_pause(1)
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Pause done', 0)
|
|
|
|
# --------------------------------
|
|
# GET SOFTWARE VERSION
|
|
# --------------------------------
|
|
if typeCommand == "version":
|
|
version_message = 'Version ' + str(num_version)
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', version_message, 0)
|
|
display_overlay_message(version_message, 10000)
|
|
|
|
# --------------------------------
|
|
# AUTO SNAPSHOT TOGGLE
|
|
# --------------------------------
|
|
if typeCommand == "toggleAutoSnapshot":
|
|
global auto_snapshot_enabled
|
|
auto_snapshot_enabled = bool(data['payload'])
|
|
state_label = 'enabled' if auto_snapshot_enabled else 'disabled'
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Auto snapshot ' + state_label, 0)
|
|
|
|
# --------------------------------
|
|
# SNAPSHOT INTERVAL
|
|
# --------------------------------
|
|
if typeCommand == "setSnapshotInterval":
|
|
global SNAPSHOT_INTERVAL_SECONDS
|
|
try:
|
|
interval_seconds = int(data['payload'])
|
|
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)
|
|
else:
|
|
SNAPSHOT_INTERVAL_SECONDS = interval_seconds
|
|
sendMessage(
|
|
toSidDevice,
|
|
fromSidDevice,
|
|
'warning',
|
|
'Snapshot interval set to ' + str(SNAPSHOT_INTERVAL_SECONDS) + ' seconds',
|
|
0
|
|
)
|
|
|
|
# --------------------------------
|
|
# MOVETO
|
|
# --------------------------------
|
|
if typeCommand == "moveto":
|
|
percentage = data['payload']
|
|
new_position_value = float(percentage) / 100
|
|
media_principal.set_position(new_position_value)
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Moved to ' + str(percentage), 0)
|
|
|
|
# --------------------------------
|
|
# UPDATE SOFTWARE
|
|
# --------------------------------
|
|
if typeCommand == "update_source":
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Updating source and restarting client', 0)
|
|
time.sleep(1)
|
|
update_source()
|
|
restart_main_iptv_process()
|
|
|
|
# --------------------------------
|
|
# REBOOT
|
|
# --------------------------------
|
|
if typeCommand == "reboot":
|
|
os.system('sudo reboot')
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'start rebooting ...', 0)
|
|
|
|
|
|
# --------------------------------
|
|
# CHANGE DEVICE NAME AND REBUILD CONFIG FILE
|
|
# --------------------------------
|
|
if typeCommand == "changeDeviceName":
|
|
global name_device
|
|
name_device = data['payload']
|
|
logger.info("Change device name to: %s", name_device)
|
|
query = "UPDATE settings SET valueParameter = '" + name_device + "' WHERE nameParameter = 'deviceName'"
|
|
cur_ = con.cursor()
|
|
cur_.execute(query)
|
|
con.commit()
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Configuration saved', 0)
|
|
|
|
# --------------------------------
|
|
# GET LIST OF WIFI NETWORKS
|
|
# --------------------------------
|
|
if typeCommand == "listSSID":
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Retreiving Wifi data', 0)
|
|
cells = Cell.all('wlan0')
|
|
resWifi = []
|
|
for c in cells:
|
|
item = []
|
|
item.append(c.ssid)
|
|
item.append(str(c.signal))
|
|
item.append(str(c.quality))
|
|
item.append(str(c.encrypted))
|
|
resWifi.append(item)
|
|
sendMessage(toSidDevice, fromSidDevice, 'wifiupdate', resWifi, 0)
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Wifi data received', 0)
|
|
|
|
# --------------------------------
|
|
# TAKE SNAPSHOT
|
|
# --------------------------------
|
|
if typeCommand == "takeSnapshot":
|
|
try:
|
|
logger.info("Take snapshot requested")
|
|
time.sleep(1)
|
|
capture_and_upload_snapshot()
|
|
except Exception as e:
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', e, 0)
|
|
|
|
# --------------------------------
|
|
# GET NUMBER OF CHANNELS
|
|
# --------------------------------
|
|
if typeCommand == "numberOfChannels":
|
|
numberOfChannels = len(channels)
|
|
sendMessage(toSidDevice, fromSidDevice, 'warning', 'Number of channels : ' + str(numberOfChannels), 0)
|
|
|
|
|
|
# --------------------------------
|
|
# Command line
|
|
# --------------------------------
|
|
if typeCommand == "command_line":
|
|
logger.info("Command line requested: %s", data['payload'])
|
|
records = re.split(r' ', data['payload'])
|
|
process = subprocess.Popen(records, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
for line in process.stdout:
|
|
logger.info("command_line output: %s", line.rstrip())
|
|
sendMessage(toSidDevice, fromSidDevice, 'display_command_result', line, 0)
|
|
|
|
|
|
@sio.event
|
|
def disconnect():
|
|
global server_connection_ready
|
|
server_connection_ready = False
|
|
logger.warning("Disconnected from server")
|
|
|
|
|
|
|
|
# --------------------------------
|
|
# INITIALIZATION DATABASE
|
|
# --------------------------------
|
|
def initDB():
|
|
global con
|
|
try:
|
|
con = lite.connect(path+db_file, check_same_thread=False)
|
|
cur = con.cursor()
|
|
cur.execute('SELECT SQLITE_VERSION()')
|
|
data = cur.fetchone()
|
|
logger.info("Connexion succeed : SQLite version: %s", data)
|
|
cur.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS recent_channels_cache (
|
|
position INTEGER PRIMARY KEY,
|
|
id_channel TEXT,
|
|
id_playlist TEXT,
|
|
name_channel TEXT NOT NULL,
|
|
url_channel TEXT NOT NULL,
|
|
last_played_at TEXT NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
con.commit()
|
|
|
|
except (lite.Error, e):
|
|
logger.error("Error %s:", e.args[0])
|
|
sys.exit(1)
|
|
|
|
|
|
def initIPTV():
|
|
global token_telegram
|
|
global name_device
|
|
global channels
|
|
global playlists
|
|
global con
|
|
global IdDevice
|
|
global recent
|
|
|
|
# GET ID UNIQUE DEVICE OR CREATE IT
|
|
query = "SELECT valueParameter FROM settings WHERE nameParameter = 'IdDevice'"
|
|
cur_ = con.cursor()
|
|
cur_.execute(query)
|
|
record = cur_.fetchone()
|
|
if record:
|
|
IdDevice = record[0]
|
|
logger.info("Device ID: %s", IdDevice)
|
|
else:
|
|
logger.info("No ID device found, generating one")
|
|
IdDevice = str(uuid.uuid4())
|
|
query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('IdDevice', '"+IdDevice+"')"
|
|
cur_ = con.cursor()
|
|
cur_.execute(query)
|
|
con.commit()
|
|
|
|
|
|
# GET DEVICE NAME
|
|
query = "SELECT valueParameter FROM settings WHERE nameParameter = 'deviceName'"
|
|
cur_ = con.cursor()
|
|
cur_.execute(query)
|
|
valueParameter = cur_.fetchone()
|
|
if valueParameter is not None:
|
|
name_device = valueParameter
|
|
logger.info("Update device name : %s", name_device)
|
|
|
|
|
|
# SAVE PID
|
|
query = "DELETE FROM settings WHERE nameParameter = 'pid'"
|
|
cur_ = con.cursor()
|
|
cur_.execute(query)
|
|
con.commit()
|
|
pid = os.getpid()
|
|
query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('pid', '"+str(pid)+"')"
|
|
cur_ = con.cursor()
|
|
cur_.execute(query)
|
|
con.commit()
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------
|
|
# ----------------------------------------------------------------
|
|
num_version='5.0.6'
|
|
# ----------------------------------------------------------------
|
|
# ----------------------------------------------------------------
|
|
|
|
|
|
# ----------------------------------------------------------------
|
|
# VARIABLES GLOBAL
|
|
# ----------------------------------------------------------------
|
|
url_update_server = 'https://iptv.mrk.ovh/'
|
|
#url_update_server = 'https://tv.mrk.ovh:5011/'
|
|
db_file = 'config.db'
|
|
path = os.path.dirname(os.path.realpath(__file__))+'/'
|
|
con = None
|
|
pid = os.getppid()
|
|
logger.info("PID : %s", pid)
|
|
|
|
channels = []
|
|
playlists = []
|
|
IdDevice = "0"
|
|
NUM_MAX_CHANNEL = 500000
|
|
|
|
recent = []
|
|
mediaRunning = ''
|
|
status_vpn = 'disconnected'
|
|
data_running = {}
|
|
auto_snapshot_enabled = True
|
|
SNAPSHOT_INTERVAL_SECONDS = 10
|
|
POLLING_INTERVAL_SECONDS = 10
|
|
SOCKET_CONNECT_TIMEOUT = 10
|
|
SOCKET_RETRY_MIN_INTERVAL = 3
|
|
CONNECTIVITY_CHECK_TIMEOUT_SECONDS = 3
|
|
CONNECTIVITY_CHECK_RETRY_SECONDS = 2
|
|
CONNECTIVITY_CHECK_TARGETS = [
|
|
(url_update_server, False),
|
|
("https://www.google.com/generate_204", True),
|
|
("http://clients3.google.com/generate_204", False),
|
|
]
|
|
last_connect_attempt_ts = 0
|
|
server_connection_ready = False
|
|
socket_connect_lock = threading.Lock()
|
|
db_lock = threading.Lock()
|
|
MAX_RECENT_CHANNELS = 12
|
|
IDLE_SCREEN_REFRESH_INTERVAL_MS = 60000
|
|
idle_screen_manager = IdleScreenManager()
|
|
VLC_PLAYBACK_START_TIMEOUT_SECONDS = 8
|
|
VLC_PLAYBACK_POLL_INTERVAL_SECONDS = 0.2
|
|
IDLE_SCREEN_CARD_COLUMNS = 3
|
|
IDLE_SCREEN_LOGO_SIZE = (220, 124)
|
|
LOGO_REQUEST_TIMEOUT_SECONDS = 5
|
|
logo_cache = {}
|
|
IMAGE_RESAMPLING_LANCZOS = getattr(getattr(Image, "Resampling", Image), "LANCZOS")
|
|
|
|
|
|
ensure_runtime_environment()
|
|
vlc_instance = build_vlc_instance()
|
|
media_principal = new_media_player("http://mag.hi-ott.com:80/120643/NS3MV8YZTS/51648")
|
|
|
|
token_telegram = "token"
|
|
name_device = ''
|
|
vpn_process = None
|
|
vpn_config = "/home/pi/Documents/ca1629.nordvpn.com.tcp.ovpn"
|
|
auth_file = "/home/pi/Documents/auth.txt"
|
|
|
|
|
|
# ----------------------------------------------------------------
|
|
# ---------------------------------------------------------------
|
|
# (logging configuré plus haut dans le fichier, juste après les imports)
|
|
|
|
# ----------------------------------------------------------------
|
|
# LOOP PRINCIPAL
|
|
# ----------------------------------------------------------------
|
|
if __name__ == '__main__':
|
|
|
|
initDB()
|
|
initIPTV()
|
|
|
|
wait_for_internet_connection()
|
|
load_recent_channels_from_db()
|
|
show_idle_screen()
|
|
|
|
threading.Thread(target=snapshot_loop, daemon=True).start()
|
|
ensure_socket_connection(force_reset=True)
|
|
sync_recent_channels_from_server()
|
|
idle_screen_manager.refresh()
|
|
|
|
polling_server()
|