diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py
index 63ef2a7f..e3893dc1 100644
--- a/apps/m3u/tasks.py
+++ b/apps/m3u/tasks.py
@@ -881,6 +881,7 @@ def sync_auto_channels(account_id, scan_start_time=None):
override_group_id = None
name_regex_pattern = None
name_replace_pattern = None
+ name_match_regex = None
if group_relation.custom_properties:
try:
group_custom_props = json.loads(group_relation.custom_properties)
@@ -888,11 +889,13 @@ def sync_auto_channels(account_id, scan_start_time=None):
override_group_id = group_custom_props.get("group_override")
name_regex_pattern = group_custom_props.get("name_regex_pattern")
name_replace_pattern = group_custom_props.get("name_replace_pattern")
+ name_match_regex = group_custom_props.get("name_match_regex")
except Exception:
force_dummy_epg = False
override_group_id = None
name_regex_pattern = None
name_replace_pattern = None
+ name_match_regex = None
# Determine which group to use for created channels
target_group = channel_group
@@ -912,6 +915,16 @@ def sync_auto_channels(account_id, scan_start_time=None):
last_seen__gte=scan_start_time
).order_by('name')
+ # --- FILTER STREAMS BY NAME MATCH REGEX IF SPECIFIED ---
+ if name_match_regex:
+ try:
+ compiled_name_match_regex = re.compile(name_match_regex, re.IGNORECASE)
+ current_streams = current_streams.filter(
+ name__iregex=name_match_regex
+ )
+ except re.error as e:
+ logger.warning(f"Invalid name_match_regex '{name_match_regex}' for group '{channel_group.name}': {e}. Skipping name filter.")
+
# Get existing auto-created channels for this account (regardless of current group)
# We'll find them by their stream associations instead of just group location
existing_channels = Channel.objects.filter(
diff --git a/frontend/src/components/forms/M3UGroupFilter.jsx b/frontend/src/components/forms/M3UGroupFilter.jsx
index 6be574ad..0db7ff1c 100644
--- a/frontend/src/components/forms/M3UGroupFilter.jsx
+++ b/frontend/src/components/forms/M3UGroupFilter.jsx
@@ -1,5 +1,5 @@
// Modal.js
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, forwardRef } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import API from '../../api';
@@ -26,12 +26,22 @@ import {
Alert,
Box,
MultiSelect,
+ Tooltip,
} from '@mantine/core';
import { Info } from 'lucide-react';
import useChannelsStore from '../../store/channels';
import { CircleCheck, CircleX } from 'lucide-react';
import { notifications } from '@mantine/notifications';
+// Custom item component for MultiSelect with tooltip
+const OptionWithTooltip = forwardRef(({ label, description, ...others }, ref) => (
+
+
+ {label}
+
+
+));
+
const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
const channelGroups = useChannelsStore((s) => s.channelGroups);
const [groupStates, setGroupStates] = useState([]);
@@ -254,7 +264,7 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
{/* Auto Sync Controls */}
-
+
{
label="Advanced Options"
placeholder="Select options..."
data={[
- { value: 'force_dummy_epg', label: 'Force Dummy EPG' },
- { value: 'group_override', label: 'Override Channel Group' },
- { value: 'name_regex', label: 'Channel Name Regex' },
+ {
+ value: 'force_dummy_epg',
+ label: 'Force Dummy EPG',
+ description: 'Assign a dummy EPG to all channels in this group if no EPG is matched',
+ },
+ {
+ value: 'group_override',
+ label: 'Override Channel Group',
+ description: 'Override the group assignment for all channels in this group',
+ },
+ {
+ value: 'name_regex',
+ label: 'Channel Name Find & Replace (Regex)',
+ description: 'Find and replace part of the channel name using a regex pattern',
+ },
+ {
+ value: 'name_match_regex',
+ label: 'Channel Name Filter (Regex)',
+ description: 'Only include channels whose names match this regex pattern',
+ },
]}
+ itemComponent={OptionWithTooltip}
value={(() => {
const selectedValues = [];
if (group.custom_properties?.force_dummy_epg) {
@@ -294,10 +322,15 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
if (group.custom_properties?.group_override !== undefined) {
selectedValues.push('group_override');
}
- if (group.custom_properties?.name_regex_pattern !== undefined ||
- group.custom_properties?.name_replace_pattern !== undefined) {
+ if (
+ group.custom_properties?.name_regex_pattern !== undefined ||
+ group.custom_properties?.name_replace_pattern !== undefined
+ ) {
selectedValues.push('name_regex');
}
+ if (group.custom_properties?.name_match_regex !== undefined) {
+ selectedValues.push('name_match_regex');
+ }
return selectedValues;
})()}
onChange={(values) => {
@@ -338,6 +371,15 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
delete newCustomProps.name_replace_pattern;
}
+ // Handle name_match_regex
+ if (selectedOptions.includes('name_match_regex')) {
+ if (newCustomProps.name_match_regex === undefined) {
+ newCustomProps.name_match_regex = '';
+ }
+ } else {
+ delete newCustomProps.name_match_regex;
+ }
+
return {
...state,
custom_properties: newCustomProps,
@@ -353,87 +395,133 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
{/* Show group select only if group_override is selected */}
{group.custom_properties?.group_override !== undefined && (
-