From 37922a44aba919c04aeb5ef4d8cb714619947ecb Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 30 Apr 2026 20:33:30 -0500 Subject: [PATCH] Enhancement: Update user connection reporting in xc_get_info to reflect active connections and max connections based on user stream limits. (Fixes #990) --- CHANGELOG.md | 1 + apps/output/views.py | 11 ++++++++++- apps/proxy/utils.py | 8 ++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f1e54d0..2c94de6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Xtream Codes `player_api.php` missing `active_cons` and reporting wrong `max_connections`**. The `user_info` block returned by the XC API did not include the `active_cons` field, which Enigma2 clients (XStreamity, XKlass) read unconditionally and crash with `KeyError: 'active_cons'` when it is absent. `max_connections` was also hardcoded to the system-wide tuner count for every user, ignoring per-user `stream_limit` configuration. `xc_get_info` now reports `max_connections` as the user's `stream_limit` when set, falling back to the system tuner count for unlimited users; `active_cons` is the user's own active connection count when they have a per-user limit, or the system-wide active connection count when they do not (so unlimited clients can still see how much of the global tuner pool is in use). The existing `get_user_active_connections` helper was generalized to accept `user_id=None` for the system-wide query rather than duplicating its Redis scan logic. (Fixes #990) - **Plugin discovery re-running on every connect event**. The `PluginManager` cache hit check used `if self._registry and ...`, which always evaluated false when zero plugins were installed because an empty dict is falsy. Every Connect event (`recording_start`, `client_connect`, `channel_start`, etc.) was therefore triggering a full filesystem walk of `/data/plugins` and emitting `Discovering plugins (no DB sync) in /data/plugins` / `Discovered 0 plugin(s)` log lines. Tracked separately via a new `_discovery_completed` flag so the cache short-circuits subsequent calls regardless of registry contents, dropping discovery to once per worker process lifetime. - **Empty show/season folders left behind after deleting a recording**. Deleting a recording removes the MKV (or HLS working directory if still in progress) but previously left the parent show / season directories on disk even when they no longer contained any other recordings. After file cleanup, `RecordingViewSet.destroy` now walks up from the deleted file's parent directory and removes any now-empty directories, stopping at the `/data/recordings` library root so the root itself is never touched. - **Premature DVR concat on graceful Celery worker shutdown**. A `worker_shutting_down` signal handler in `apps/channels/tasks.py` now sets a module-level `_DVR_SHUTTING_DOWN` flag. The `run_recording` loop checks this flag after FFmpeg exits and, if the recording's `end_time` has not yet passed, skips concatenation and persists `status="interrupted"` with `interrupted_reason="server_shutdown"`. The HLS working directory is preserved across the restart so the recovery path on the next worker startup can resume segment numbering from where it left off rather than truncating the show. diff --git a/apps/output/views.py b/apps/output/views.py index 5b061f1f..01c7ee60 100644 --- a/apps/output/views.py +++ b/apps/output/views.py @@ -20,6 +20,7 @@ import logging from django.db.models.functions import Lower import os from apps.m3u.utils import calculate_tuner_count +from apps.proxy.utils import get_user_active_connections import regex from core.utils import log_system_event import hashlib @@ -1945,6 +1946,13 @@ def xc_get_info(request, full=False): hostname = raw_host port = "443" if request.is_secure() else "80" + if user.stream_limit and user.stream_limit > 0: + active_cons = len(get_user_active_connections(user.id)) + max_connections = user.stream_limit + else: + active_cons = len(get_user_active_connections(None)) + max_connections = calculate_tuner_count(minimum=1, unlimited_default=50) + info = { "user_info": { "username": request.GET.get("username"), @@ -1953,7 +1961,8 @@ def xc_get_info(request, full=False): "auth": 1, "status": "Active", "exp_date": str(int(time.time()) + (90 * 24 * 60 * 60)), - "max_connections": str(calculate_tuner_count(minimum=1, unlimited_default=50)), + "active_cons": str(active_cons), + "max_connections": str(max_connections), "allowed_output_formats": [ "ts", ], diff --git a/apps/proxy/utils.py b/apps/proxy/utils.py index 6503d1df..3e571cc1 100644 --- a/apps/proxy/utils.py +++ b/apps/proxy/utils.py @@ -83,6 +83,10 @@ def attempt_stream_termination(user_id, requesting_client_id, active_connections return False def get_user_active_connections(user_id): + """Return active stream connections for a single user. + + Pass `user_id=None` to return all active connections across the system. + """ redis_client = RedisClient.get_client() connections = [] @@ -101,7 +105,7 @@ def get_user_active_connections(user_id): logger.debug(f"[stream limits] channel_id = {channel_id}") logger.debug(f"[stream limits] client_id = {client_id}") - if client_user_id and int(client_user_id) == user_id: + if user_id is None or (client_user_id and int(client_user_id) == user_id): try: logger.debug(f"[stream limits] Found LIVE connection for user {user_id} on channel {channel_id} with client ID {client_id}") connected_at = float(connected_at) if connected_at else 0 @@ -127,7 +131,7 @@ def get_user_active_connections(user_id): logger.debug(f"[stream limits] user_id = {user_id}") logger.debug(f"[stream limits] client_id = {client_id}") - if client_user_id and int(client_user_id) == user_id: + if user_id is None or (client_user_id and int(client_user_id) == user_id): try: logger.debug(f"[stream limits] Found VOD connection for user {user_id} on content {content_uuid} with client ID {client_id}") connected_at = float(connected_at) if connected_at else 0