From e6c580c9bd6a5dff9ee101ddc9bd7055ef1452be Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 5 May 2026 08:40:05 -0500 Subject: [PATCH] feat: Implement delete series rule functionality with title parameter and update related components --- apps/channels/api_urls.py | 2 - apps/channels/api_views.py | 107 +++++++++--------- frontend/src/api.js | 8 +- .../forms/ProgramRecordingModal.jsx | 2 +- .../forms/SeriesRuleEditorModal.jsx | 2 +- .../src/utils/cards/RecordingCardUtils.js | 28 +++-- frontend/src/utils/guideUtils.js | 4 +- 7 files changed, 73 insertions(+), 80 deletions(-) diff --git a/apps/channels/api_urls.py b/apps/channels/api_urls.py index 33e33ed9..ff00e0f7 100644 --- a/apps/channels/api_urls.py +++ b/apps/channels/api_urls.py @@ -18,7 +18,6 @@ from .api_views import ( GetChannelStreamStatsAPIView, SeriesRulesAPIView, SeriesRulePreviewAPIView, - DeleteSeriesRuleAPIView, EvaluateSeriesRulesAPIView, BulkRemoveSeriesRecordingsAPIView, BulkDeleteUpcomingRecordingsAPIView, @@ -51,7 +50,6 @@ urlpatterns = [ path('series-rules/preview/', SeriesRulePreviewAPIView.as_view(), name='series_rules_preview'), path('series-rules/evaluate/', EvaluateSeriesRulesAPIView.as_view(), name='evaluate_series_rules'), path('series-rules/bulk-remove/', BulkRemoveSeriesRecordingsAPIView.as_view(), name='bulk_remove_series_recordings'), - path('series-rules//', DeleteSeriesRuleAPIView.as_view(), name='delete_series_rule'), path('recordings/bulk-delete-upcoming/', BulkDeleteUpcomingRecordingsAPIView.as_view(), name='bulk_delete_upcoming_recordings'), path( 'recordings//hls/', diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 32b7e897..882e90b9 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -14,7 +14,6 @@ from django.db.models import Q import os, json, requests, logging, mimetypes, threading, time from datetime import timedelta from django.utils.http import http_date -from urllib.parse import unquote from apps.accounts.permissions import ( Authenticated, IsAdmin, @@ -3988,6 +3987,52 @@ class SeriesRulesAPIView(APIView): # avoid a race that creates duplicate recordings. return Response({"success": True, "rules": rules}) + @extend_schema( + summary="Delete a series rule", + description="Remove a series recording rule by tvg_id + title and clean up future scheduled recordings.", + parameters=[ + OpenApiParameter('tvg_id', str, OpenApiParameter.QUERY, required=False, description='Channel TVG ID (may be blank for title-only rules)'), + OpenApiParameter('title', str, OpenApiParameter.QUERY, required=False, description='Series title'), + ], + ) + def delete(self, request): + tvg_id = str(request.query_params.get("tvg_id") or "").strip() + title = request.query_params.get("title") + + rules = CoreSettings.get_dvr_series_rules() + + def _matches(r): + tvg_match = str(r.get("tvg_id") or "") == tvg_id + title_match = title is None or str(r.get("title") or "") == title + return tvg_match and title_match + + deleted_rule = next((r for r in rules if _matches(r)), None) + remaining = [r for r in rules if not _matches(r)] + CoreSettings.set_dvr_series_rules(remaining) + + removed = 0 + if deleted_rule: + from .models import Recording + qs = Recording.objects.filter(start_time__gte=timezone.now()) + rule_tvg_id = deleted_rule.get("tvg_id") or "" + if rule_tvg_id: + qs = qs.filter(custom_properties__program__tvg_id=rule_tvg_id) + rule_title = deleted_rule.get("title") or "" + if rule_title: + qs = qs.filter(custom_properties__program__title=rule_title) + removed = qs.count() + qs.delete() + + try: + from core.utils import send_websocket_update + send_websocket_update('updates', 'update', { + "success": True, "type": "recordings_refreshed", "removed": removed, + }) + except Exception: + pass + + return Response({"success": True, "rules": remaining, "removed": removed}) + class SeriesRulePreviewAPIView(APIView): """Preview which upcoming programs a series rule would match. @@ -4101,55 +4146,6 @@ class SeriesRulePreviewAPIView(APIView): }) -class DeleteSeriesRuleAPIView(APIView): - def get_permissions(self): - try: - return [perm() for perm in permission_classes_by_method[self.request.method]] - except KeyError: - return [Authenticated()] - - @extend_schema( - summary="Delete a series rule", - description="Remove a series recording rule by TVG ID and clean up future scheduled recordings.", - parameters=[ - OpenApiParameter('tvg_id', str, OpenApiParameter.PATH, required=True, description='Channel TVG ID'), - ], - ) - def delete(self, request, tvg_id): - tvg_id = unquote(str(tvg_id or "")) - - # Find the rule before removing to retain the title for cleanup - rules = CoreSettings.get_dvr_series_rules() - deleted_rule = next((r for r in rules if str(r.get("tvg_id")) == tvg_id), None) - remaining = [r for r in rules if str(r.get("tvg_id")) != tvg_id] - CoreSettings.set_dvr_series_rules(remaining) - - # Delete only FUTURE recordings — preserve previously recorded episodes - removed = 0 - if deleted_rule: - from .models import Recording - qs = Recording.objects.filter( - start_time__gte=timezone.now(), - custom_properties__program__tvg_id=tvg_id, - ) - title = deleted_rule.get("title") - if title: - qs = qs.filter(custom_properties__program__title=title) - removed = qs.count() - qs.delete() - - # Notify frontend to refresh recordings list - try: - from core.utils import send_websocket_update - send_websocket_update('updates', 'update', { - "success": True, "type": "recordings_refreshed", "removed": removed, - }) - except Exception: - pass - - return Response({"success": True, "rules": remaining, "removed": removed}) - - class EvaluateSeriesRulesAPIView(APIView): def get_permissions(self): try: @@ -4205,13 +4201,12 @@ class BulkRemoveSeriesRecordingsAPIView(APIView): tvg_id = str(request.data.get("tvg_id") or "").strip() title = request.data.get("title") scope = (request.data.get("scope") or "title").lower() - if not tvg_id: - return Response({"error": "tvg_id is required"}, status=status.HTTP_400_BAD_REQUEST) + if not tvg_id and not title: + return Response({"error": "tvg_id or title is required"}, status=status.HTTP_400_BAD_REQUEST) - qs = Recording.objects.filter( - start_time__gte=timezone.now(), - custom_properties__program__tvg_id=tvg_id, - ) + qs = Recording.objects.filter(start_time__gte=timezone.now()) + if tvg_id: + qs = qs.filter(custom_properties__program__tvg_id=tvg_id) if scope == "title" and title: qs = qs.filter(custom_properties__program__title=title) diff --git a/frontend/src/api.js b/frontend/src/api.js index a8cbdd48..6c003bf3 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -3074,10 +3074,12 @@ export default class API { } } - static async deleteSeriesRule(tvgId) { + static async deleteSeriesRule(tvgId, title) { try { - const encodedTvgId = encodeURIComponent(tvgId); - await request(`${host}/api/channels/series-rules/${encodedTvgId}/`, { + const params = new URLSearchParams(); + if (tvgId) params.set('tvg_id', tvgId); + if (title) params.set('title', title); + await request(`${host}/api/channels/series-rules/?${params}`, { method: 'DELETE', }); notifications.show({ title: 'Series rule removed' }); diff --git a/frontend/src/components/forms/ProgramRecordingModal.jsx b/frontend/src/components/forms/ProgramRecordingModal.jsx index 68ebb187..1ce58fb9 100644 --- a/frontend/src/components/forms/ProgramRecordingModal.jsx +++ b/frontend/src/components/forms/ProgramRecordingModal.jsx @@ -39,7 +39,7 @@ export default function ProgramRecordingModal({ }; const handleRemoveSeriesRule = async () => { - await deleteSeriesRuleByTvgId(program.tvg_id); + await deleteSeriesRuleByTvgId(program.tvg_id, program.title); onExistingRuleModeChange(null); onClose(); }; diff --git a/frontend/src/components/forms/SeriesRuleEditorModal.jsx b/frontend/src/components/forms/SeriesRuleEditorModal.jsx index b4fd1392..201f285f 100644 --- a/frontend/src/components/forms/SeriesRuleEditorModal.jsx +++ b/frontend/src/components/forms/SeriesRuleEditorModal.jsx @@ -238,7 +238,7 @@ export default function SeriesRuleEditorModal({ { export const deleteSeriesAndRule = async (seriesInfo) => { const { tvg_id, title } = seriesInfo; - if (tvg_id) { - try { - await API.bulkRemoveSeriesRecordings({ - tvg_id, - title, - scope: 'title', - }); - } catch (error) { - console.error('Failed to remove series recordings', error); - } - try { - await API.deleteSeriesRule(tvg_id); - } catch (error) { - console.error('Failed to delete series rule', error); - } + try { + await API.bulkRemoveSeriesRecordings({ + tvg_id: tvg_id || '', + title, + scope: 'title', + }); + } catch (error) { + console.error('Failed to remove series recordings', error); + } + try { + await API.deleteSeriesRule(tvg_id, title); + } catch (error) { + console.error('Failed to delete series rule', error); } }; diff --git a/frontend/src/utils/guideUtils.js b/frontend/src/utils/guideUtils.js index 9b4ba2e2..58831713 100644 --- a/frontend/src/utils/guideUtils.js +++ b/frontend/src/utils/guideUtils.js @@ -420,8 +420,8 @@ export const formatSeasonEpisode = (season, episode) => { return null; }; -export const deleteSeriesRuleByTvgId = async (tvg_id) => { - await API.deleteSeriesRule(tvg_id); +export const deleteSeriesRuleByTvgId = async (tvg_id, title) => { + await API.deleteSeriesRule(tvg_id, title); }; export const evaluateSeriesRulesByTvgId = async (tvg_id) => {