diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c365565..4155bb68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.15.0] - 2025-12-20 + ### Added - VOD client stop button in Stats page: Users can now disconnect individual VOD clients from the Stats view, similar to the existing channel client disconnect functionality. @@ -27,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Docker init script now validates DISPATCHARR_PORT is an integer before using it, preventing sed errors when Kubernetes sets it to a service URL like `tcp://10.98.37.10:80`. Falls back to default port 9191 when invalid (Fixes #737) - M3U Profile form now properly resets local state for search and replace patterns after saving, preventing validation errors when adding multiple profiles in a row - DVR series rule deletion now properly handles TVG IDs that contain slashes by encoding them in the URL path (Fixes #697) - VOD episode processing now correctly handles duplicate episodes (same episode in multiple languages/qualities) by reusing Episode records across multiple M3UEpisodeRelation entries instead of attempting to create duplicates (Fixes #556) diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index aebb74a3..1f98358e 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -8,7 +8,6 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from django.shortcuts import get_object_or_404, get_list_or_404 from django.db import transaction -from django.db.models import Q import os, json, requests, logging from urllib.parse import unquote from apps.accounts.permissions import ( @@ -421,36 +420,10 @@ class ChannelViewSet(viewsets.ModelViewSet): group_names = channel_group.split(",") qs = qs.filter(channel_group__name__in=group_names) - filters = {} - q_filters = Q() - - channel_profile_id = self.request.query_params.get("channel_profile_id") - show_disabled_param = self.request.query_params.get("show_disabled", None) - only_streamless = self.request.query_params.get("only_streamless", None) - - if channel_profile_id: - try: - profile_id_int = int(channel_profile_id) - filters["channelprofilemembership__channel_profile_id"] = profile_id_int - - if show_disabled_param is None: - filters["channelprofilemembership__enabled"] = True - except (ValueError, TypeError): - # Ignore invalid profile id values - pass - - if only_streamless: - q_filters &= Q(streams__isnull=True) - if self.request.user.user_level < 10: - filters["user_level__lte"] = self.request.user.user_level + qs = qs.filter(user_level__lte=self.request.user.user_level) - if filters: - qs = qs.filter(**filters) - if q_filters: - qs = qs.filter(q_filters) - - return qs.distinct() + return qs def get_serializer_context(self): context = super().get_serializer_context() diff --git a/apps/output/views.py b/apps/output/views.py index 635bb9d9..c0d72bfb 100644 --- a/apps/output/views.py +++ b/apps/output/views.py @@ -2326,7 +2326,7 @@ def xc_get_epg(request, user, short=False): if short == False: program_output["now_playing"] = 1 if start <= django_timezone.now() <= end else 0 - program_output["has_archive"] = 0 + program_output["has_archive"] = "0" output['epg_listings'].append(program_output) diff --git a/docker/init/03-init-dispatcharr.sh b/docker/init/03-init-dispatcharr.sh index da7d4484..c9eaf18b 100644 --- a/docker/init/03-init-dispatcharr.sh +++ b/docker/init/03-init-dispatcharr.sh @@ -30,15 +30,11 @@ if [ "$(id -u)" = "0" ] && [ -d "/app" ]; then fi fi # Configure nginx port -sed -i "s/NGINX_PORT/${DISPATCHARR_PORT}/g" /etc/nginx/sites-enabled/default - -# Configure nginx based on IPv6 availability -if ip -6 addr show | grep -q "inet6"; then - echo "✅ IPv6 is available, enabling IPv6 in nginx" -else - echo "⚠️ IPv6 not available, disabling IPv6 in nginx" - sed -i '/listen \[::\]:/d' /etc/nginx/sites-enabled/default +if ! [[ "$DISPATCHARR_PORT" =~ ^[0-9]+$ ]]; then + echo "⚠️ Warning: DISPATCHARR_PORT is not a valid integer, using default port 9191" + DISPATCHARR_PORT=9191 fi +sed -i "s/NGINX_PORT/${DISPATCHARR_PORT}/g" /etc/nginx/sites-enabled/default # NOTE: mac doesn't run as root, so only manage permissions # if this script is running as root diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index b025d2d5..1b2e73d7 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -68,7 +68,7 @@ const epgUrlBase = `${window.location.protocol}//${window.location.host}/output/ const hdhrUrlBase = `${window.location.protocol}//${window.location.host}/hdhr`; const ChannelEnabledSwitch = React.memo( - ({ rowId, selectedProfileId, selectedTableIds, setSelectedTableIds }) => { + ({ rowId, selectedProfileId, selectedTableIds }) => { // Directly extract the channels set once to avoid re-renders on every change. const isEnabled = useChannelsStore( useCallback( @@ -79,20 +79,16 @@ const ChannelEnabledSwitch = React.memo( ) ); - const handleToggle = async () => { + const handleToggle = () => { if (selectedTableIds.length > 1) { - await API.updateProfileChannels( + API.updateProfileChannels( selectedTableIds, selectedProfileId, !isEnabled ); } else { - await API.updateProfileChannel(rowId, selectedProfileId, !isEnabled); + API.updateProfileChannel(rowId, selectedProfileId, !isEnabled); } - - setSelectedTableIds([]); - - return API.requeryChannels(); }; return ( @@ -381,15 +377,6 @@ const ChannelsTable = ({}) => { params.append('page', pagination.pageIndex + 1); params.append('page_size', pagination.pageSize); params.append('include_streams', 'true'); - if (selectedProfileId !== '0') { - params.append('channel_profile_id', selectedProfileId); - } - if (showDisabled === true) { - params.append('show_disabled', true); - } - if (showOnlyStreamlessChannels === true) { - params.append('only_streamless', true); - } // Apply sorting if (sorting.length > 0) { @@ -425,14 +412,7 @@ const ChannelsTable = ({}) => { pageSize: pagination.pageSize, }); setAllRowIds(ids); - }, [ - pagination, - sorting, - debouncedFilters, - showDisabled, - selectedProfileId, - showOnlyStreamlessChannels, - ]); + }, [pagination, sorting, debouncedFilters]); const stopPropagation = useCallback((e) => { e.stopPropagation(); @@ -759,7 +739,6 @@ const ChannelsTable = ({}) => { rowId={row.original.id} selectedProfileId={selectedProfileId} selectedTableIds={table.getState().selectedTableIds} - setSelectedTableIds={table.setSelectedTableIds} /> ); }, @@ -1358,10 +1337,6 @@ const ChannelsTable = ({}) => { deleteChannels={deleteChannels} selectedTableIds={table.selectedTableIds} table={table} - showDisabled={showDisabled} - setShowDisabled={setShowDisabled} - showOnlyStreamlessChannels={showOnlyStreamlessChannels} - setShowOnlyStreamlessChannels={setShowOnlyStreamlessChannels} /> {/* Table or ghost empty state inside Paper */} diff --git a/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx b/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx index 460ab12a..b7e04d7d 100644 --- a/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx +++ b/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx @@ -12,22 +12,20 @@ import { Text, TextInput, Tooltip, + UnstyledButton, useMantineTheme, } from '@mantine/core'; import { ArrowDown01, Binary, + Check, CircleCheck, + Ellipsis, EllipsisVertical, SquareMinus, SquarePen, SquarePlus, Settings, - Eye, - EyeOff, - Filter, - Square, - SquareCheck, } from 'lucide-react'; import API from '../../../api'; import { notifications } from '@mantine/notifications'; @@ -104,10 +102,6 @@ const ChannelTableHeader = ({ editChannel, deleteChannels, selectedTableIds, - showDisabled, - setShowDisabled, - showOnlyStreamlessChannels, - setShowOnlyStreamlessChannels, }) => { const theme = useMantineTheme(); @@ -214,14 +208,6 @@ const ChannelTableHeader = ({ ); }; - const toggleShowDisabled = () => { - setShowDisabled(!showDisabled); - }; - - const toggleShowOnlyStreamlessChannels = () => { - setShowOnlyStreamlessChannels(!showOnlyStreamlessChannels); - }; - return ( @@ -250,41 +236,6 @@ const ChannelTableHeader = ({ }} > - - - - - - - : - } - disabled={selectedProfileId === '0'} - > - - {showDisabled ? 'Hide Disabled' : 'Show Disabled'} - - - - - ) : ( - - ) - } - > - Only Empty Channels - - - -