Enhancement: 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.

This commit is contained in:
SergeantPanda 2026-02-26 12:00:29 -06:00
parent 03c8d55ab9
commit 60c165f422
5 changed files with 32 additions and 65 deletions

View file

@ -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

View file

@ -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)

View file

@ -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;

View file

@ -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(() => {

View file

@ -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;
}
});
}