diff --git a/CHANGELOG.md b/CHANGELOG.md index 410baed8..9e28fc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Custom Dummy EPG form UI improvements: Reorganized the form into collapsible accordion sections (Pattern Configuration, Output Templates, Upcoming/Ended Templates, Fallback Templates, EPG Settings) for better organization. Field descriptions now appear in info icon popovers instead of taking up vertical space, making the form more compact and easier to navigate while keeping help text accessible. - XC API M3U stream URLs: M3U generation for Xtream Codes API endpoints now use proper XC-style stream URLs (`/live/username/password/channel_id`) instead of UUID-based stream endpoints, ensuring full compatibility with XC clients. (Fixes #839) - XC API `get_series` now includes `tmdb_id` and `imdb_id` fields, matching `get_vod_streams`. Clients that use TMDB enrichment (e.g. Chillio) can now resolve clean series titles and poster images. - Thanks [@firestaerter3](https://github.com/firestaerter3) +- Stats page "Now Playing" EPG lookup updated to use `channel_uuids` directly (the proxy stats already key active channels by UUID), removing the need for a UUID→integer ID conversion step introduced alongside the lazy channel-fetch refactor. Stream preview sessions (which use a content hash rather than a UUID as their channel ID) are now filtered out before any API call is made, preventing a backend `ValidationError` on both the `current-programs` and `by-uuids` endpoints when a stream preview is active on the Stats page. ### Fixed diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index 15613d4d..dbae6394 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -441,43 +441,32 @@ class CurrentProgramsAPIView(APIView): request=inline_serializer( name="CurrentProgramsRequest", fields={ - "channel_ids": serializers.ListField( - child=serializers.IntegerField(), + "channel_uuids": serializers.ListField( + child=serializers.CharField(), required=False, allow_null=True, - help_text="Array of channel IDs. If null or omitted, returns all channels with current programs.", + help_text="Array of channel UUIDs. If null or omitted, returns all channels with current programs.", ), }, ), responses={200: ProgramDataSerializer(many=True)}, ) def post(self, request, format=None): - # Get channel IDs from request body - channel_ids = request.data.get('channel_ids', None) - # Import Channel model from apps.channels.models import Channel # Build query for channels with EPG data query = Channel.objects.filter(epg_data__isnull=False) - # Filter by specific channel IDs if provided - if channel_ids is not None: - if not isinstance(channel_ids, list): + channel_uuids = request.data.get('channel_uuids', None) + + if channel_uuids is not None: + if not isinstance(channel_uuids, list): return Response( - {"error": "channel_ids must be an array of integers or null"}, + {"error": "channel_uuids must be an array of strings or null"}, status=status.HTTP_400_BAD_REQUEST ) - - try: - channel_ids = [int(id) for id in channel_ids] - except (ValueError, TypeError): - return Response( - {"error": "channel_ids must contain valid integers"}, - status=status.HTTP_400_BAD_REQUEST - ) - - query = query.filter(id__in=channel_ids) + query = query.filter(uuid__in=channel_uuids) # Get channels with EPG data channels = query.select_related('epg_data') @@ -497,9 +486,8 @@ class CurrentProgramsAPIView(APIView): ).first() if program: - # Serialize program and add channel_id for easy mapping program_data = ProgramDataSerializer(program).data - program_data['channel_id'] = channel.id + program_data['channel_uuid'] = str(channel.uuid) current_programs.append(program_data) return Response(current_programs, status=status.HTTP_200_OK) diff --git a/frontend/src/api.js b/frontend/src/api.js index 915633f7..7049b7b8 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1331,11 +1331,11 @@ export default class API { } } - static async getCurrentPrograms(channelIds = null) { + static async getCurrentPrograms(channelUUIDs = null) { try { const response = await request(`${host}/api/epg/current-programs/`, { method: 'POST', - body: { channel_ids: channelIds }, + body: { channel_uuids: channelUUIDs }, }); return response; diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index 3631a917..c1e2f7e7 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -109,28 +109,22 @@ const StatsPage = () => { const [channels, setChannels] = useState({}); // id -> channel const [channelsByUUID, setChannelsByUUID] = useState({}); // uuid -> id - // Use refs to hold latest values without triggering effects - const channelHistoryRef = useRef(channelHistory); - const channelsByUUIDRef = useRef(channelsByUUID); - - // Update refs when values change - useEffect(() => { - channelHistoryRef.current = channelHistory; - }, [channelHistory]); - - useEffect(() => { - channelsByUUIDRef.current = channelsByUUID; - }, [channelsByUUID]); - - // Compute needed channel UUIDs from the current active channels + // Compute needed channel UUIDs from the current active channels. + // Stream previews use a non-UUID hash as channel_id — filter those out. + const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const neededUUIDs = useMemo( - () => Object.keys(channelHistory || {}), + () => Object.keys(channelHistory || {}).filter((id) => UUID_REGEX.test(id)), [channelHistory] ); - console.log(channelHistory); + // Keep a ref so the programs poller always has the latest valid UUIDs + const neededUUIDsRef = useRef(neededUUIDs); + useEffect(() => { + neededUUIDsRef.current = neededUUIDs; + }, [neededUUIDs]); - // Fetch any missing channels by UUID when the needed set changes + // Fetch any missing channels by UUID when the needed set changes (for card name/logo) useEffect(() => { if (!neededUUIDs || neededUUIDs.length === 0) return; const missing = neededUUIDs.filter((u) => channelsByUUID[u] === undefined); @@ -160,7 +154,7 @@ const StatsPage = () => { return () => { cancelled = true; }; - }, [neededUUIDs.join(','), channelsByUUID]); + }, [neededUUIDs.join(',')]); // Use localStorage for stats refresh interval (in seconds) const [refreshIntervalSeconds, setRefreshIntervalSeconds] = useLocalStorage( @@ -301,11 +295,7 @@ const StatsPage = () => { let timer = null; const fetchPrograms = async () => { - // Use refs to get latest values without adding dependencies - const programs = await getCurrentPrograms( - channelHistoryRef.current, - channelsByUUIDRef.current - ); + const programs = await getCurrentPrograms(neededUUIDsRef.current); setCurrentPrograms(programs); // Schedule next fetch based on nearest program end time @@ -341,7 +331,7 @@ const StatsPage = () => { return () => { if (timer) clearTimeout(timer); }; - }, [activeChannelIds]); // Only depend on activeChannelIds + }, [activeChannelIds]); // Only re-run when active channel set changes // Combine active streams and VOD connections into a single mixed list const combinedConnections = useMemo(() => { diff --git a/frontend/src/utils/pages/StatsUtils.js b/frontend/src/utils/pages/StatsUtils.js index 05f8c393..b5392bc8 100644 --- a/frontend/src/utils/pages/StatsUtils.js +++ b/frontend/src/utils/pages/StatsUtils.js @@ -20,32 +20,20 @@ export const getVODStats = async () => { return await API.getVODStats(); }; -export const getCurrentPrograms = async (channelHistory, channelsByUUID) => { +export const getCurrentPrograms = async (activeChannelUUIDs) => { try { - // Get all active channel IDs that have actual channels (not just streams) - const activeChannelIds = Object.values(channelHistory) - .filter( - (ch) => ch.name && channelsByUUID && channelsByUUID[ch.channel_id] - ) - .map((ch) => channelsByUUID[ch.channel_id]) - .filter((id) => id !== undefined); - - if (activeChannelIds.length === 0) { + if (!activeChannelUUIDs || activeChannelUUIDs.length === 0) { return {}; } - const programs = await API.getCurrentPrograms(activeChannelIds); + const programs = await API.getCurrentPrograms(activeChannelUUIDs); // Convert array to map keyed by channel UUID for easy lookup const programsMap = {}; if (programs && Array.isArray(programs)) { programs.forEach((program) => { - // Find the channel UUID from the channel ID - const channelEntry = Object.entries(channelsByUUID).find( - ([uuid, id]) => id === program.channel_id - ); - if (channelEntry) { - programsMap[channelEntry[0]] = program; + if (program.channel_uuid) { + programsMap[program.channel_uuid] = program; } }); }