diff --git a/CHANGELOG.md b/CHANGELOG.md index b1738429..599fb986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Only polls when active channel list changes, not on stats refresh - Channel preview button: Added preview functionality to active stream cards on stats page - Unassociated streams filter: Added "Only Unassociated" filter option to streams table for quickly finding streams not assigned to any channels - Thanks [@JeffreyBytes](https://github.com/JeffreyBytes) (Closes #667) +- Streams table: Added "Hide Stale" filter to quickly hide streams marked as stale. - Client-side logo caching: Added `Cache-Control` and `Last-Modified` headers to logo responses, enabling browsers to cache logos locally for 4 hours (local files) and respecting upstream cache headers (remote logos). This reduces network traffic and nginx load while providing faster page loads through browser-level caching that complements the existing nginx server-side cache - Thanks [@DawtCom](https://github.com/DawtCom) - DVR recording remux fallback strategy: Implemented two-stage TS→MP4→MKV fallback when direct TS→MKV conversion fails due to timestamp issues. On remux failure, system now attempts TS→MP4 conversion (MP4 container handles broken timestamps better) followed by MP4→MKV conversion, automatically recovering from provider timestamp corruption. Failed conversions now properly clean up partial files and preserve source TS for manual recovery. - Mature content filtering support: diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index f9daf0d0..11940177 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -149,7 +149,7 @@ class StreamViewSet(viewsets.ModelViewSet): qs = qs.filter(channels__id=assigned) unassigned = self.request.query_params.get("unassigned") - if unassigned == "1": + if unassigned and str(unassigned).lower() in ("1", "true", "yes", "on"): # Use annotation with Count for better performance on large datasets qs = qs.annotate(channel_count=Count('channels')).filter(channel_count=0) @@ -158,6 +158,11 @@ class StreamViewSet(viewsets.ModelViewSet): group_names = channel_group.split(",") qs = qs.filter(channel_group__name__in=group_names) + # Allow client to hide stale streams (streams marked as is_stale=True) + hide_stale = self.request.query_params.get("hide_stale") + if hide_stale and str(hide_stale).lower() in ("1", "true", "yes", "on"): + qs = qs.filter(is_stale=False) + return qs def list(self, request, *args, **kwargs): diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx index 22e7cb67..5b9182b2 100644 --- a/frontend/src/components/tables/StreamsTable.jsx +++ b/frontend/src/components/tables/StreamsTable.jsx @@ -232,7 +232,8 @@ const StreamsTable = ({ onReady }) => { name: '', channel_group: '', m3u_account: '', - unassigned: '', + unassigned: false, + hide_stale: false, }); const [columnSizing, setColumnSizing] = useLocalStorage( 'streams-table-column-sizing', @@ -398,7 +399,14 @@ const StreamsTable = ({ onReady }) => { const toggleUnassignedOnly = () => { setFilters((prev) => ({ ...prev, - unassigned: prev.unassigned === '1' ? '' : '1', + unassigned: !prev.unassigned, + })); + }; + + const toggleHideStale = () => { + setFilters((prev) => ({ + ...prev, + hide_stale: !prev.hide_stale, })); }; @@ -426,9 +434,13 @@ const StreamsTable = ({ onReady }) => { params.append('ordering', `${sortDirection}${sortField}`); } - // Apply debounced filters + // Apply debounced filters; send boolean filters as 'true' when set Object.entries(debouncedFilters).forEach(([key, value]) => { - if (value) params.append(key, value); + if (typeof value === 'boolean') { + if (value) params.append(key, 'true'); + } else if (value !== null && value !== undefined && value !== '') { + params.append(key, String(value)); + } }); try { @@ -1188,7 +1200,7 @@ const StreamsTable = ({ onReady }) => {