Got rid of extra api call to query if any channels had no EPG assigned. Moved query to an EXISTS lookup during channel query.

This commit is contained in:
SergeantPanda 2026-03-02 17:20:55 -06:00
parent 565d335403
commit b098deae76
6 changed files with 20 additions and 45 deletions

View file

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- EPG filter regression in channel table (introduced in 0.20.0 channel store refactor): The EPG filter dropdown was showing all EPG sources regardless of whether they had any channels assigned, and the "No EPG" option was never displayed. Fixed by annotating EPGSource records with a `has_channels` flag (via a lightweight `EXISTS` subquery) so only EPG sources actually in use appear as filter options. "No EPG" now appears only when at least one channel has no EPG assigned, determined via a `page_size=1` query against the existing channel filter endpoint.
- EPG filter regression in channel table (introduced in 0.20.0 channel store refactor): The EPG filter dropdown was showing all EPG sources regardless of whether they had any channels assigned, and the "No EPG" option was never displayed. Fixed by annotating EPGSource records with a `has_channels` flag (via a lightweight `EXISTS` subquery) so only active EPG sources with at least one channel assigned appear as filter options. "No EPG" now appears only when at least one channel globally has no EPG assigned; this is determined by a second `EXISTS` query embedded directly in the paginated channel response (`has_unassigned_epg_channels`), avoiding any additional network requests.
- Stale stream rows missing hover effect: Stale streams in the streams table had no hover color change, unlike channels with no streams assigned. Converted the inline `backgroundColor` style to a CSS class (`stale-stream-row`) so the `:hover` rule can apply correctly. Applied the same fix to the channel-streams sub-table, where the teal expanded-row background caused the semi-transparent red tint to visually mismatch; the sub-table now uses a pre-blended solid color via `color-mix()` to match the appearance of stale rows in the main streams table.
- Channel table onboarding shown when filter returns zero results: The channel store refactor changed to loading only channel IDs instead of full channel objects, leaving `Object.keys(channels).length` always `0` and incorrectly triggering the onboarding state on any empty filter. Fixed by checking `channelIds.length` instead.
- TV Guide scrolls to position 0 when a filter yields no results: Applying any filter that temporarily empties the channel list (e.g. switching directly between two channel groups, or typing a search query that matches nothing) caused the guide to show a blank/empty view with no programs visible. The `VariableSizeList` unmounts when `filteredChannels` becomes empty, destroying its DOM node and resetting `scrollLeft` to 0. On remount the scroll position was never restored because `initialScrollComplete` was still `true`. Fixed by saving the user's current scroll position when the channel list empties mid-transition, then restoring it once new channels have loaded. On first load the guide still scrolls to the current time as before.

View file

@ -413,6 +413,13 @@ class ChannelPagination(PageNumberPagination):
return super().paginate_queryset(queryset, request, view)
def get_paginated_response(self, data):
from django.db.models import Exists, OuterRef
has_unassigned = Channel.objects.filter(epg_data__isnull=True).exists()
response = super().get_paginated_response(data)
response.data['has_unassigned_epg_channels'] = has_unassigned
return response
class EPGFilter(django_filters.Filter):
"""

View file

@ -364,9 +364,6 @@ export default class API {
.queryChannels(response, API.lastQueryParams);
useChannelsTableStore.getState().setAllQueryIds(ids);
// Refresh the EPG stats since channel EPG assignments may have changed
useEPGsStore.getState().refreshEPGStats();
return response;
} catch (e) {
// Handle invalid page error by resetting to page 1 and retrying
@ -1323,19 +1320,6 @@ export default class API {
}
}
static async getEPGStats() {
try {
// Query channels with no EPG assigned using the existing filter — page_size=1
// so we only need the `count` field, not any actual channel data.
const response = await request(
`${host}/api/channels/channels/?epg=null&page=1&page_size=1`
);
return { has_unassigned_epg_channels: (response?.count ?? 0) > 0 };
} catch (e) {
errorNotification('Failed to retrieve EPG stats', e);
}
}
static async getEPGData() {
try {
const response = await request(`${host}/api/epg/epgdata/`);

View file

@ -251,7 +251,7 @@ const ChannelsTable = ({ onReady }) => {
const tvgsById = useEPGsStore((s) => s.tvgsById);
const epgs = useEPGsStore((s) => s.epgs);
const tvgsLoaded = useEPGsStore((s) => s.tvgsLoaded);
const hasUnassignedEPGChannels = useEPGsStore(
const hasUnassignedEPGChannels = useChannelsTableStore(
(s) => s.hasUnassignedEPGChannels
);

View file

@ -4,6 +4,7 @@ const useChannelsTableStore = create((set, get) => ({
channels: [],
pageCount: 0,
totalCount: 0,
hasUnassignedEPGChannels: false,
sorting: [{ id: 'channel_number', desc: false }],
pagination: {
pageIndex: 0,
@ -14,14 +15,15 @@ const useChannelsTableStore = create((set, get) => ({
allQueryIds: [],
isUnlocked: false,
queryChannels: ({ results, count }, params) => {
set((state) => {
return {
channels: results,
totalCount: count,
pageCount: Math.ceil(count / params.get('page_size')),
};
});
queryChannels: ({ results, count, has_unassigned_epg_channels }, params) => {
set((state) => ({
channels: results,
totalCount: count,
pageCount: Math.ceil(count / params.get('page_size')),
...(has_unassigned_epg_channels !== undefined && {
hasUnassignedEPGChannels: has_unassigned_epg_channels,
}),
}));
},
setAllQueryIds: (allQueryIds) => {

View file

@ -15,7 +15,6 @@ const useEPGsStore = create((set) => ({
tvgs: [],
tvgsById: {},
tvgsLoaded: false,
hasUnassignedEPGChannels: false,
isLoading: false,
error: null,
refreshProgress: {},
@ -23,18 +22,12 @@ const useEPGsStore = create((set) => ({
fetchEPGs: async () => {
set({ isLoading: true, error: null });
try {
const [sources, stats] = await Promise.all([
api.getEPGs(),
api.getEPGStats(),
]);
const hasUnassignedEPGChannels =
stats?.has_unassigned_epg_channels ?? false;
const sources = await api.getEPGs();
set({
epgs: (sources ?? []).reduce((acc, epg) => {
acc[epg.id] = epg;
return acc;
}, {}),
hasUnassignedEPGChannels,
isLoading: false,
});
} catch (error) {
@ -43,17 +36,6 @@ const useEPGsStore = create((set) => ({
}
},
refreshEPGStats: async () => {
try {
const stats = await api.getEPGStats();
set({
hasUnassignedEPGChannels: stats?.has_unassigned_epg_channels ?? false,
});
} catch (error) {
console.error('Failed to refresh EPG stats:', error);
}
},
fetchEPGData: async () => {
set({ isLoading: true, error: null });
try {