feat: Implement delete series rule functionality with title parameter and update related components

This commit is contained in:
SergeantPanda 2026-05-05 08:40:05 -05:00
parent 0822c9aad2
commit e6c580c9bd
7 changed files with 73 additions and 80 deletions

View file

@ -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/<path:tvg_id>/', DeleteSeriesRuleAPIView.as_view(), name='delete_series_rule'),
path('recordings/bulk-delete-upcoming/', BulkDeleteUpcomingRecordingsAPIView.as_view(), name='bulk_delete_upcoming_recordings'),
path(
'recordings/<int:pk>/hls/<path:seg_path>',

View file

@ -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)

View file

@ -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' });

View file

@ -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();
};

View file

@ -238,7 +238,7 @@ export default function SeriesRuleEditorModal({
<Modal
opened={opened}
onClose={onClose}
title={initialRule?.tvg_id ? 'Edit Series Rule' : 'New Series Rule'}
title={initialRule ? 'Edit Series Rule' : 'New Series Rule'}
centered
size="lg"
radius="md"

View file

@ -84,21 +84,19 @@ export const extendRecordingById = async (recordingId, extraMinutes) => {
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);
}
};

View file

@ -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) => {