Enhancement: Added "Hide Stale" filter to quickly hide streams marked as stale.
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run

This commit is contained in:
SergeantPanda 2026-01-20 18:51:13 -06:00
parent c9b454431c
commit 2fc2486c34
3 changed files with 36 additions and 6 deletions

View file

@ -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 - Only polls when active channel list changes, not on stats refresh
- Channel preview button: Added preview functionality to active stream cards on stats page - 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) - 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) - 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. - 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: - Mature content filtering support:

View file

@ -149,7 +149,7 @@ class StreamViewSet(viewsets.ModelViewSet):
qs = qs.filter(channels__id=assigned) qs = qs.filter(channels__id=assigned)
unassigned = self.request.query_params.get("unassigned") 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 # Use annotation with Count for better performance on large datasets
qs = qs.annotate(channel_count=Count('channels')).filter(channel_count=0) qs = qs.annotate(channel_count=Count('channels')).filter(channel_count=0)
@ -158,6 +158,11 @@ class StreamViewSet(viewsets.ModelViewSet):
group_names = channel_group.split(",") group_names = channel_group.split(",")
qs = qs.filter(channel_group__name__in=group_names) 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 return qs
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):

View file

@ -232,7 +232,8 @@ const StreamsTable = ({ onReady }) => {
name: '', name: '',
channel_group: '', channel_group: '',
m3u_account: '', m3u_account: '',
unassigned: '', unassigned: false,
hide_stale: false,
}); });
const [columnSizing, setColumnSizing] = useLocalStorage( const [columnSizing, setColumnSizing] = useLocalStorage(
'streams-table-column-sizing', 'streams-table-column-sizing',
@ -398,7 +399,14 @@ const StreamsTable = ({ onReady }) => {
const toggleUnassignedOnly = () => { const toggleUnassignedOnly = () => {
setFilters((prev) => ({ setFilters((prev) => ({
...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}`); params.append('ordering', `${sortDirection}${sortField}`);
} }
// Apply debounced filters // Apply debounced filters; send boolean filters as 'true' when set
Object.entries(debouncedFilters).forEach(([key, value]) => { 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 { try {
@ -1188,7 +1200,7 @@ const StreamsTable = ({ onReady }) => {
<Menu.Item <Menu.Item
onClick={toggleUnassignedOnly} onClick={toggleUnassignedOnly}
leftSection={ leftSection={
filters.unassigned === '1' ? ( filters.unassigned === true ? (
<SquareCheck size={18} /> <SquareCheck size={18} />
) : ( ) : (
<Square size={18} /> <Square size={18} />
@ -1197,6 +1209,18 @@ const StreamsTable = ({ onReady }) => {
> >
<Text size="xs">Only Unassociated</Text> <Text size="xs">Only Unassociated</Text>
</Menu.Item> </Menu.Item>
<Menu.Item
onClick={toggleHideStale}
leftSection={
filters.hide_stale === true ? (
<SquareCheck size={18} />
) : (
<Square size={18} />
)
}
>
<Text size="xs">Hide Stale</Text>
</Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>