diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2e001f..58e5d90d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Connection cards on the Stats page now show the **username** of the connected user. For live channel connections a new User column appears between IP Address and Connected; for VOD connections the username is shown inline next to the IP address in the Client summary row. The username is resolved from the user store using the `user_id` stored in Redis client metadata. (Closes #766, Closes #586) - `ip_address` and `user_id` were not included in the client info returned by `get_detailed_channel_info()` despite being available in the Redis hash. Both fields are now extracted and returned. `user_id` is now also included in the VOD stats response. - Web UI stream preview now sends an `Authorization: Bearer` header with each mpegts.js request, identifying the logged-in user. Live channel previews initiated from the web UI now appear on the Stats page with the correct username rather than as unknown user. +- `client_connect` and `client_disconnect` system events now include the **username** of the connected user. The username is stored alongside the client metadata in Redis and included in the event payload for `log_system_event` calls (making it available to webhook and script integrations). - Donate button added to the sidebar footer. A heart icon links to the project's Open Collective page, visible in both expanded and collapsed states. Hovering shows a "Support Dispatcharr" tooltip. The version string is also now clickable to copy it to the clipboard. - User stream limits: administrators can now set a maximum number of concurrent streams per user account. When a user reaches their limit, the system can automatically terminate an existing stream to free a slot based on configurable rules. Limit enforcement applies to both live channels and VOD. (Closes #544) - Each user account has a new **Stream Limit** field (0 = unlimited) configurable from the user edit form in Settings → Users. diff --git a/apps/proxy/ts_proxy/client_manager.py b/apps/proxy/ts_proxy/client_manager.py index e1c92256..af7eb7d3 100644 --- a/apps/proxy/ts_proxy/client_manager.py +++ b/apps/proxy/ts_proxy/client_manager.py @@ -278,7 +278,8 @@ class ClientManager: "channel_id": self.channel_id, "client_id": client_id, "worker_id": self.worker_id or "unknown", - "timestamp": time.time() + "timestamp": time.time(), + "username": user.username if user is not None else "unknown" } if user_agent: @@ -319,8 +320,11 @@ class ClientManager: self.last_active_time = time.time() if self.redis_client: - # Get client IP before removing the data + # Get client data before removing the data client_key = f"ts_proxy:channel:{self.channel_id}:clients:{client_id}" + client_username = self.redis_client.hget(client_key, "username") or "unknown" + if isinstance(client_username, bytes): + client_username = client_username.decode("utf-8") # Remove from channel's client set self.redis_client.srem(self.client_set_key, client_id) @@ -361,7 +365,8 @@ class ClientManager: "client_id": client_id, "worker_id": self.worker_id or "unknown", "timestamp": time.time(), - "remaining_clients": remaining + "remaining_clients": remaining, + "username": client_username }) self.redis_client.publish(RedisKeys.events_channel(self.channel_id), event_data) diff --git a/apps/proxy/ts_proxy/stream_generator.py b/apps/proxy/ts_proxy/stream_generator.py index d58e4ea2..0036be5c 100644 --- a/apps/proxy/ts_proxy/stream_generator.py +++ b/apps/proxy/ts_proxy/stream_generator.py @@ -25,7 +25,7 @@ class StreamGenerator: data delivery, and cleanup. """ - def __init__(self, channel_id, client_id, client_ip, client_user_agent, channel_initializing=False): + def __init__(self, channel_id, client_id, client_ip, client_user_agent, channel_initializing=False, user=None): """ Initialize the stream generator with client and channel details. @@ -35,12 +35,14 @@ class StreamGenerator: client_ip: Client's IP address client_user_agent: User agent string from client channel_initializing: Whether the channel is still initializing + user: Authenticated user making the request """ self.channel_id = channel_id self.client_id = client_id self.client_ip = client_ip self.client_user_agent = client_user_agent self.channel_initializing = channel_initializing + self.user = user # Performance and state tracking self.stream_start_time = time.time() @@ -112,7 +114,8 @@ class StreamGenerator: channel_name=channel_obj.name, client_ip=self.client_ip, client_id=self.client_id, - user_agent=self.client_user_agent[:100] if self.client_user_agent else None + user_agent=self.client_user_agent[:100] if self.client_user_agent else None, + username=self.user.username if self.user else None ) except Exception as e: logger.error(f"Could not log client connect event: {e}") @@ -593,7 +596,8 @@ class StreamGenerator: client_id=self.client_id, user_agent=self.client_user_agent[:100] if self.client_user_agent else None, duration=round(elapsed, 2), - bytes_sent=self.bytes_sent + bytes_sent=self.bytes_sent, + username=self.user.username if self.user else None ) except Exception as e: logger.error(f"Could not log client disconnect event: {e}") @@ -628,10 +632,10 @@ class StreamGenerator: gevent.spawn(delayed_shutdown) -def create_stream_generator(channel_id, client_id, client_ip, client_user_agent, channel_initializing=False): +def create_stream_generator(channel_id, client_id, client_ip, client_user_agent, channel_initializing=False, user=None): """ Factory function to create a new stream generator. Returns a function that can be passed to StreamingHttpResponse. """ - generator = StreamGenerator(channel_id, client_id, client_ip, client_user_agent, channel_initializing) + generator = StreamGenerator(channel_id, client_id, client_ip, client_user_agent, channel_initializing, user=user) return generator.generate diff --git a/apps/proxy/ts_proxy/views.py b/apps/proxy/ts_proxy/views.py index c8699191..5750616b 100644 --- a/apps/proxy/ts_proxy/views.py +++ b/apps/proxy/ts_proxy/views.py @@ -532,7 +532,7 @@ def stream_ts(request, channel_id, user=None): # Create a stream generator for this client generate = create_stream_generator( - channel_id, client_id, client_ip, client_user_agent, channel_initializing + channel_id, client_id, client_ip, client_user_agent, channel_initializing, user=user ) # Return the StreamingHttpResponse from the main function