mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Adds the ability to auto channel sync to only add channels where the stream name matches a regex.
This commit is contained in:
parent
935f4c1da2
commit
f6fa90178d
2 changed files with 181 additions and 80 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<Tooltip label={description} withArrow>
|
||||
<div ref={ref} {...others}>
|
||||
{label}
|
||||
</div>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
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 }) => {
|
|||
</Button>
|
||||
|
||||
{/* Auto Sync Controls */}
|
||||
<Stack spacing={4}>
|
||||
<Stack spacing={4} >
|
||||
<Flex align="center" gap="xs">
|
||||
<Checkbox
|
||||
label="Auto Channel Sync"
|
||||
|
|
@ -282,10 +292,28 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
|
|||
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 && (
|
||||
<Select
|
||||
label="Override Channel Group"
|
||||
placeholder="Choose group..."
|
||||
value={group.custom_properties?.group_override?.toString() || null}
|
||||
onChange={(value) => {
|
||||
const newValue = value ? parseInt(value) : null;
|
||||
setGroupStates(
|
||||
groupStates.map((state) => {
|
||||
if (state.channel_group === group.channel_group) {
|
||||
return {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
group_override: newValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
})
|
||||
);
|
||||
}}
|
||||
data={Object.values(channelGroups).map((g) => ({
|
||||
value: g.id.toString(),
|
||||
label: g.name,
|
||||
}))}
|
||||
clearable
|
||||
searchable
|
||||
size="xs"
|
||||
/>
|
||||
<Tooltip
|
||||
label="Select a group to override the assignment for all channels in this group."
|
||||
withArrow
|
||||
>
|
||||
<Select
|
||||
label="Override Channel Group"
|
||||
placeholder="Choose group..."
|
||||
value={group.custom_properties?.group_override?.toString() || null}
|
||||
onChange={(value) => {
|
||||
const newValue = value ? parseInt(value) : null;
|
||||
setGroupStates(
|
||||
groupStates.map((state) => {
|
||||
if (state.channel_group === group.channel_group) {
|
||||
return {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
group_override: newValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
})
|
||||
);
|
||||
}}
|
||||
data={Object.values(channelGroups).map((g) => ({
|
||||
value: g.id.toString(),
|
||||
label: g.name,
|
||||
}))}
|
||||
clearable
|
||||
searchable
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Show regex fields only if name_regex is selected */}
|
||||
{(group.custom_properties?.name_regex_pattern !== undefined ||
|
||||
group.custom_properties?.name_replace_pattern !== undefined) && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Channel Name Regex"
|
||||
placeholder="e.g. ^.*? - PPV\\d+ - (.+)$"
|
||||
value={group.custom_properties?.name_regex_pattern || ''}
|
||||
onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
setGroupStates(
|
||||
groupStates.map(state =>
|
||||
state.channel_group === group.channel_group
|
||||
? {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
name_regex_pattern: val,
|
||||
},
|
||||
}
|
||||
: state
|
||||
)
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
<TextInput
|
||||
label="Channel Name Replace"
|
||||
placeholder="e.g. $1"
|
||||
value={group.custom_properties?.name_replace_pattern || ''}
|
||||
onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
setGroupStates(
|
||||
groupStates.map(state =>
|
||||
state.channel_group === group.channel_group
|
||||
? {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
name_replace_pattern: val,
|
||||
},
|
||||
}
|
||||
: state
|
||||
)
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
<Tooltip
|
||||
label="Regex pattern to find in the channel name. Example: ^.*? - PPV\\d+ - (.+)$"
|
||||
withArrow
|
||||
>
|
||||
<TextInput
|
||||
label="Channel Name Find (Regex)"
|
||||
placeholder="e.g. ^.*? - PPV\\d+ - (.+)$"
|
||||
value={group.custom_properties?.name_regex_pattern || ''}
|
||||
onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
setGroupStates(
|
||||
groupStates.map(state =>
|
||||
state.channel_group === group.channel_group
|
||||
? {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
name_regex_pattern: val,
|
||||
},
|
||||
}
|
||||
: state
|
||||
)
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label="Replacement pattern for the channel name. Example: $1"
|
||||
withArrow
|
||||
>
|
||||
<TextInput
|
||||
label="Channel Name Replace"
|
||||
placeholder="e.g. $1"
|
||||
value={group.custom_properties?.name_replace_pattern || ''}
|
||||
onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
setGroupStates(
|
||||
groupStates.map(state =>
|
||||
state.channel_group === group.channel_group
|
||||
? {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
name_replace_pattern: val,
|
||||
},
|
||||
}
|
||||
: state
|
||||
)
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Show name_match_regex field only if selected */}
|
||||
{group.custom_properties?.name_match_regex !== undefined && (
|
||||
<Tooltip
|
||||
label="Only channels whose names match this regex will be included. Example: ^Sports.*"
|
||||
withArrow
|
||||
>
|
||||
<TextInput
|
||||
label="Channel Name Filter (Regex)"
|
||||
placeholder="e.g. ^Sports.*"
|
||||
value={group.custom_properties?.name_match_regex || ''}
|
||||
onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
setGroupStates(
|
||||
groupStates.map(state =>
|
||||
state.channel_group === group.channel_group
|
||||
? {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
name_match_regex: val,
|
||||
},
|
||||
}
|
||||
: state
|
||||
)
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue