diff --git a/apps/proxy/ts_proxy/channel_status.py b/apps/proxy/ts_proxy/channel_status.py index fe5b1f3b..43e01df8 100644 --- a/apps/proxy/ts_proxy/channel_status.py +++ b/apps/proxy/ts_proxy/channel_status.py @@ -65,6 +65,25 @@ class ChannelStatus: except ValueError: logger.warning(f"Invalid stream_id format in Redis: {stream_id_bytes}") + # Add M3U profile information + m3u_profile_id_bytes = metadata.get(ChannelMetadataField.M3U_PROFILE.encode('utf-8')) + if m3u_profile_id_bytes: + try: + m3u_profile_id = int(m3u_profile_id_bytes.decode('utf-8')) + info['m3u_profile_id'] = m3u_profile_id + + # Look up M3U profile name from database + try: + from apps.m3u.models import M3UAccountProfile + m3u_profile = M3UAccountProfile.objects.filter(id=m3u_profile_id).first() + if m3u_profile: + info['m3u_profile_name'] = m3u_profile.name + logger.debug(f"Added M3U profile name '{m3u_profile.name}' for profile ID {m3u_profile_id}") + except (ImportError, DatabaseError) as e: + logger.warning(f"Failed to get M3U profile name for ID {m3u_profile_id}: {e}") + except ValueError: + logger.warning(f"Invalid m3u_profile_id format in Redis: {m3u_profile_id_bytes}") + # Add timing information state_changed_field = ChannelMetadataField.STATE_CHANGED_AT.encode('utf-8') if state_changed_field in metadata: @@ -380,6 +399,25 @@ class ChannelStatus: # Add clients to info info['clients'] = clients + # Add M3U profile information + m3u_profile_id_bytes = metadata.get(ChannelMetadataField.M3U_PROFILE.encode('utf-8')) + if m3u_profile_id_bytes: + try: + m3u_profile_id = int(m3u_profile_id_bytes.decode('utf-8')) + info['m3u_profile_id'] = m3u_profile_id + + # Look up M3U profile name from database + try: + from apps.m3u.models import M3UAccountProfile + m3u_profile = M3UAccountProfile.objects.filter(id=m3u_profile_id).first() + if m3u_profile: + info['m3u_profile_name'] = m3u_profile.name + logger.debug(f"Added M3U profile name '{m3u_profile.name}' for profile ID {m3u_profile_id}") + except (ImportError, DatabaseError) as e: + logger.warning(f"Failed to get M3U profile name for ID {m3u_profile_id}: {e}") + except ValueError: + logger.warning(f"Invalid m3u_profile_id format in Redis: {m3u_profile_id_bytes}") + return info except Exception as e: logger.error(f"Error getting channel info: {e}") diff --git a/apps/proxy/ts_proxy/services/channel_service.py b/apps/proxy/ts_proxy/services/channel_service.py index 3ac62af4..9aa8c66b 100644 --- a/apps/proxy/ts_proxy/services/channel_service.py +++ b/apps/proxy/ts_proxy/services/channel_service.py @@ -7,7 +7,7 @@ import logging import time import json from django.shortcuts import get_object_or_404 -from apps.channels.models import Channel +from apps.channels.models import Channel, Stream from apps.proxy.config import TSConfig as Config from ..server import ProxyServer from ..redis_keys import RedisKeys @@ -58,7 +58,7 @@ class ChannelService: # Verify the stream_id was set stream_id_value = proxy_server.redis_client.hget(metadata_key, ChannelMetadataField.STREAM_ID) if stream_id_value: - logger.info(f"Verified stream_id {stream_id_value.decode('utf-8')} is now set in Redis") + logger.debug(f"Verified stream_id {stream_id_value.decode('utf-8')} is now set in Redis") else: logger.error(f"Failed to set stream_id {stream_id} in Redis before initialization") @@ -82,7 +82,7 @@ class ChannelService: return success @staticmethod - def change_stream_url(channel_id, new_url=None, user_agent=None, target_stream_id=None): + def change_stream_url(channel_id, new_url=None, user_agent=None, target_stream_id=None, m3u_profile_id=None): """ Change the URL of an existing stream. @@ -91,6 +91,7 @@ class ChannelService: new_url: New stream URL (optional if target_stream_id is provided) user_agent: Optional user agent to update target_stream_id: Optional target stream ID to switch to + m3u_profile_id: Optional M3U profile ID to update Returns: dict: Result information including success status and diagnostics @@ -109,6 +110,10 @@ class ChannelService: new_url = stream_info['url'] user_agent = stream_info['user_agent'] stream_id = target_stream_id + # Extract M3U profile ID from stream info if available + if 'm3u_profile_id' in stream_info: + m3u_profile_id = stream_info['m3u_profile_id'] + logger.info(f"Found M3U profile ID {m3u_profile_id} for stream ID {stream_id}") elif target_stream_id: # If we have both URL and target_stream_id, use the target_stream_id stream_id = target_stream_id @@ -163,7 +168,7 @@ class ChannelService: # Update metadata in Redis regardless of ownership if proxy_server.redis_client: try: - ChannelService._update_channel_metadata(channel_id, new_url, user_agent, stream_id) + ChannelService._update_channel_metadata(channel_id, new_url, user_agent, stream_id, m3u_profile_id) result['metadata_updated'] = True except Exception as e: logger.error(f"Error updating Redis metadata: {e}", exc_info=True) @@ -188,7 +193,7 @@ class ChannelService: # If we're not the owner, publish an event for the owner to pick up logger.info(f"Not the owner, requesting URL change via Redis PubSub") if proxy_server.redis_client: - ChannelService._publish_stream_switch_event(channel_id, new_url, user_agent, stream_id) + ChannelService._publish_stream_switch_event(channel_id, new_url, user_agent, stream_id, m3u_profile_id) result.update({ 'direct_update': False, 'event_published': True, @@ -413,7 +418,7 @@ class ChannelService: # Helper methods for Redis operations @staticmethod - def _update_channel_metadata(channel_id, url, user_agent=None, stream_id=None): + def _update_channel_metadata(channel_id, url, user_agent=None, stream_id=None, m3u_profile_id=None): """Update channel metadata in Redis""" proxy_server = ProxyServer.get_instance() @@ -432,7 +437,8 @@ class ChannelService: metadata[ChannelMetadataField.USER_AGENT] = user_agent if stream_id: metadata[ChannelMetadataField.STREAM_ID] = str(stream_id) - logger.info(f"Updating stream ID to {stream_id} in Redis for channel {channel_id}") + if m3u_profile_id: + metadata[ChannelMetadataField.M3U_PROFILE] = str(m3u_profile_id) # Use the appropriate method based on the key type if key_type == 'hash': @@ -448,11 +454,11 @@ class ChannelService: switch_key = RedisKeys.switch_request(channel_id) proxy_server.redis_client.setex(switch_key, 30, url) # 30 second TTL - logger.info(f"Updated metadata for channel {channel_id} in Redis") + logger.debug(f"Updated metadata for channel {channel_id} in Redis") return True @staticmethod - def _publish_stream_switch_event(channel_id, new_url, user_agent=None, stream_id=None): + def _publish_stream_switch_event(channel_id, new_url, user_agent=None, stream_id=None, m3u_profile_id=None): """Publish a stream switch event to Redis pubsub""" proxy_server = ProxyServer.get_instance() @@ -465,6 +471,7 @@ class ChannelService: "url": new_url, "user_agent": user_agent, "stream_id": stream_id, + "m3u_profile_id": m3u_profile_id, "requester": proxy_server.worker_id, "timestamp": time.time() } diff --git a/apps/proxy/ts_proxy/views.py b/apps/proxy/ts_proxy/views.py index d71da8d4..87a8e51b 100644 --- a/apps/proxy/ts_proxy/views.py +++ b/apps/proxy/ts_proxy/views.py @@ -351,6 +351,7 @@ def change_stream(request, channel_id): # Use the info from the stream new_url = stream_info['url'] user_agent = stream_info['user_agent'] + m3u_profile_id = stream_info.get('m3u_profile_id') # Stream ID will be passed to change_stream_url later elif not new_url: return JsonResponse({'error': 'Either url or stream_id must be provided'}, status=400) @@ -359,7 +360,7 @@ def change_stream(request, channel_id): # Use the service layer instead of direct implementation # Pass stream_id to ensure proper connection tracking - result = ChannelService.change_stream_url(channel_id, new_url, user_agent, stream_id) + result = ChannelService.change_stream_url(channel_id, new_url, user_agent, stream_id, m3u_profile_id) # Get the stream manager before updating URL stream_manager = proxy_server.stream_managers.get(channel_id) diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index e2e90a7d..ef15d605 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -82,12 +82,23 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel const [availableStreams, setAvailableStreams] = useState([]); const [isLoadingStreams, setIsLoadingStreams] = useState(false); const [activeStreamId, setActiveStreamId] = useState(null); + const [currentM3UProfile, setCurrentM3UProfile] = useState(null); // Add state for current M3U profile // Safety check - if channel doesn't have required data, don't render if (!channel || !channel.channel_id) { return null; } + // Update M3U profile information when channel data changes + useEffect(() => { + // If the channel data includes M3U profile information, update our state + if (channel.m3u_profile || channel.m3u_profile_name) { + setCurrentM3UProfile({ + name: channel.m3u_profile?.name || channel.m3u_profile_name || 'Default M3U' + }); + } + }, [channel.m3u_profile, channel.m3u_profile_name, channel.stream_id]); + // Fetch available streams for this channel useEffect(() => { const fetchStreams = async () => { @@ -110,6 +121,11 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel if (matchingStream) { setActiveStreamId(matchingStream.id.toString()); + + // If the stream has M3U profile info, save it + if (matchingStream.m3u_profile) { + setCurrentM3UProfile(matchingStream.m3u_profile); + } } } } @@ -138,6 +154,14 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel // Update the local active stream ID immediately setActiveStreamId(streamId); + // Update M3U profile information if available in the response + if (response && response.m3u_profile) { + setCurrentM3UProfile(response.m3u_profile); + } else if (selectedStream && selectedStream.m3u_profile) { + // Fallback to the profile from the selected stream + setCurrentM3UProfile(selectedStream.m3u_profile); + } + // Show detailed notification with stream name notifications.show({ title: 'Stream switching', @@ -152,6 +176,12 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel if (channelId) { const updatedStreamData = await API.getChannelStreams(channelId); console.log("Channel streams after switch:", updatedStreamData); + + // Update current stream information with fresh data + const updatedStream = updatedStreamData.find(s => s.id.toString() === streamId); + if (updatedStream && updatedStream.m3u_profile) { + setCurrentM3UProfile(updatedStream.m3u_profile); + } } } catch (error) { console.error("Error checking streams after switch:", error); @@ -305,6 +335,12 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel const avgBitrate = channel.avg_bitrate || '0 Kbps'; const streamProfileName = channel.stream_profile?.name || 'Unknown Profile'; + // Use currentM3UProfile if available, otherwise fall back to channel data + const m3uProfileName = currentM3UProfile?.name || + channel.m3u_profile?.name || + channel.m3u_profile_name || + 'Default M3U'; + // Create select options for available streams const streamOptions = availableStreams.map(stream => ({ value: stream.id.toString(), @@ -377,6 +413,16 @@ const ChannelCard = ({ channel, clients, stopClient, stopChannel, logos, channel + {/* Display M3U profile information */} + + + + + {m3uProfileName} + + + + {/* Add stream selection dropdown */} {availableStreams.length > 0 && (