Enhancement: Added optional subtitle template field to custom dummy EPG configuration. Users can now define subtitle patterns using extracted regex groups and time/date placeholders (e.g., {starttime} - {endtime}). (Closes #942)

This commit is contained in:
SergeantPanda 2026-02-17 12:00:24 -06:00
parent 967704d7a0
commit 47853bdb5b
3 changed files with 77 additions and 1 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Custom Dummy EPG subtitle template support: Added optional subtitle template field to custom dummy EPG configuration. Users can now define subtitle patterns using extracted regex groups and time/date placeholders (e.g., `{starttime} - {endtime}`). (Closes #942)
- Event-driven webhooks and script execution (Connect): Added new Connect feature that enables event-driven execution of custom scripts and webhooks in response to system events. Supports multiple event types including channel lifecycle (start, stop, reconnect, error, failover), stream operations (switch), recording events (start, end), data refreshes (EPG, M3U), and client activity (connect, disconnect). Event data is available as environment variables in scripts (prefixed with `DISPATCHARR_`), POST payloads for webhooks, and plugin execution payloads. Plugins can now subscribe to events by specifying an `events` array in their action definitions. Includes connection testing endpoint with dummy payloads for validation. (Closes #203)
- Cron scheduling support for M3U and EPG refreshes: Added interactive cron expression builder with preset buttons and custom field editors, plus info popover with common cron examples. Refactored backup scheduling to use shared ScheduleInput component for consistency across all scheduling interfaces. (Closes #165)
- Channel numbering modes for auto channel sync: Added three channel numbering modes when auto-syncing channels from M3U groups:
@ -16,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Use Provider Number**: Use channel numbers from the M3U source (tvg-chno), with configurable fallback if provider number is missing
- **Next Available**: Auto-assign starting from 1, skipping all used channel numbers
Each mode includes its own configuration options accessible via the "Channel Numbering Mode" dropdown in auto sync settings. (Closes #956, #433)
- Legacy NumPy for modular Docker: Added entrypoint detection and automatic installation for the Celery container (use `USE_LEGACY_NUMPY`) to support older CPUs.- Thanks [@patrickjmcd](https://github.com/patrickjmcd)
- Legacy NumPy for modular Docker: Added entrypoint detection and automatic installation for the Celery container (use `USE_LEGACY_NUMPY`) to support older CPUs. - Thanks [@patrickjmcd](https://github.com/patrickjmcd)
### Changed

View file

@ -511,6 +511,7 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
output_timezone_value = custom_properties.get('output_timezone', '') # Optional: display times in different timezone
program_duration = custom_properties.get('program_duration', 180) # Minutes
title_template = custom_properties.get('title_template', '')
subtitle_template = custom_properties.get('subtitle_template', '')
description_template = custom_properties.get('description_template', '')
# Templates for upcoming/ended programs
@ -916,6 +917,11 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
title_parts.append(all_groups['title'])
main_event_title = ' - '.join(title_parts) if title_parts else channel_name
if subtitle_template:
main_event_subtitle = format_template(subtitle_template, all_groups)
else:
main_event_subtitle = None
if description_template:
main_event_description = format_template(description_template, all_groups)
else:
@ -966,6 +972,7 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
"start_time": program_start_utc,
"end_time": program_end_utc,
"title": upcoming_title,
"sub_title": None, # No subtitle for filler programs
"description": upcoming_description,
"custom_properties": program_custom_properties,
"channel_logo_url": channel_logo_url, # Pass channel logo for EPG generation
@ -1005,6 +1012,7 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
"start_time": event_start_utc,
"end_time": event_end_utc,
"title": main_event_title,
"sub_title": main_event_subtitle,
"description": main_event_description,
"custom_properties": main_event_custom_properties,
"channel_logo_url": channel_logo_url, # Pass channel logo for EPG generation
@ -1049,6 +1057,7 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
"start_time": program_start_utc,
"end_time": program_end_utc,
"title": ended_title,
"sub_title": None, # No subtitle for filler programs
"description": ended_description,
"custom_properties": program_custom_properties,
"channel_logo_url": channel_logo_url, # Pass channel logo for EPG generation
@ -1109,6 +1118,7 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
"start_time": program_start_utc,
"end_time": program_end_utc,
"title": program_title,
"sub_title": None, # No subtitle for filler programs
"description": program_description,
"custom_properties": program_custom_properties,
"channel_logo_url": channel_logo_url,
@ -1136,6 +1146,11 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
title_parts.append(all_groups['title'])
title = ' - '.join(title_parts) if title_parts else channel_name
if subtitle_template:
subtitle = format_template(subtitle_template, all_groups)
else:
subtitle = None
if description_template:
description = format_template(description_template, all_groups)
else:
@ -1172,6 +1187,7 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
"start_time": program_start_utc,
"end_time": program_end_utc,
"title": title,
"sub_title": subtitle,
"description": description,
"custom_properties": program_custom_properties,
"channel_logo_url": channel_logo_url, # Pass channel logo for EPG generation
@ -1211,6 +1227,11 @@ def generate_dummy_epg(
f' <programme start="{start_str}" stop="{stop_str}" channel="{html.escape(program["channel_id"])}">'
)
xml_lines.append(f" <title>{html.escape(program['title'])}</title>")
# Add subtitle if available
if program.get('sub_title'):
xml_lines.append(f" <sub-title>{html.escape(program['sub_title'])}</sub-title>")
xml_lines.append(f" <desc>{html.escape(program['description'])}</desc>")
# Add custom_properties if present
@ -1530,6 +1551,11 @@ def generate_epg(request, profile_name=None, user=None):
# Create program entry with escaped channel name
yield f' <programme start="{start_str}" stop="{stop_str}" channel="{html.escape(channel_id)}">\n'
yield f" <title>{html.escape(program['title'])}</title>\n"
# Add subtitle if available
if program.get('sub_title'):
yield f" <sub-title>{html.escape(program['sub_title'])}</sub-title>\n"
yield f" <desc>{html.escape(program['description'])}</desc>\n"
# Add custom_properties if present
@ -1579,6 +1605,11 @@ def generate_epg(request, profile_name=None, user=None):
yield f' <programme start="{start_str}" stop="{stop_str}" channel="{html.escape(channel_id)}">\n'
yield f" <title>{html.escape(program['title'])}</title>\n"
# Add subtitle if available
if program.get('sub_title'):
yield f" <sub-title>{html.escape(program['sub_title'])}</sub-title>\n"
yield f" <desc>{html.escape(program['description'])}</desc>\n"
# Add custom_properties if present

View file

@ -43,6 +43,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
const [datePattern, setDatePattern] = useState('');
const [sampleTitle, setSampleTitle] = useState('');
const [titleTemplate, setTitleTemplate] = useState('');
const [subtitleTemplate, setSubtitleTemplate] = useState('');
const [descriptionTemplate, setDescriptionTemplate] = useState('');
const [upcomingTitleTemplate, setUpcomingTitleTemplate] = useState('');
const [upcomingDescriptionTemplate, setUpcomingDescriptionTemplate] =
@ -71,6 +72,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
program_duration: 180,
sample_title: '',
title_template: '',
subtitle_template: '',
description_template: '',
upcoming_title_template: '',
upcoming_description_template: '',
@ -125,6 +127,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
dateGroups: {},
calculatedPlaceholders: {},
formattedTitle: '',
formattedSubtitle: '',
formattedDescription: '',
formattedUpcomingTitle: '',
formattedUpcomingDescription: '',
@ -459,6 +462,14 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
);
}
// Format subtitle template
if (subtitleTemplate && (result.titleMatch || result.timeMatch)) {
result.formattedSubtitle = subtitleTemplate.replace(
/\{(\w+)\}/g,
(match, key) => allGroups[key] || match
);
}
// Format description template
if (descriptionTemplate && (result.titleMatch || result.timeMatch)) {
result.formattedDescription = descriptionTemplate.replace(
@ -533,6 +544,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
datePattern,
sampleTitle,
titleTemplate,
subtitleTemplate,
descriptionTemplate,
upcomingTitleTemplate,
upcomingDescriptionTemplate,
@ -565,6 +577,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
program_duration: custom.program_duration || 180,
sample_title: custom.sample_title || '',
title_template: custom.title_template || '',
subtitle_template: custom.subtitle_template || '',
description_template: custom.description_template || '',
upcoming_title_template: custom.upcoming_title_template || '',
upcoming_description_template:
@ -591,6 +604,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
setDatePattern(custom.date_pattern || '');
setSampleTitle(custom.sample_title || '');
setTitleTemplate(custom.title_template || '');
setSubtitleTemplate(custom.subtitle_template || '');
setDescriptionTemplate(custom.description_template || '');
setUpcomingTitleTemplate(custom.upcoming_title_template || '');
setUpcomingDescriptionTemplate(
@ -611,6 +625,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
setDatePattern('');
setSampleTitle('');
setTitleTemplate('');
setSubtitleTemplate('');
setDescriptionTemplate('');
setUpcomingTitleTemplate('');
setUpcomingDescriptionTemplate('');
@ -682,6 +697,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
program_duration: custom.program_duration || 180,
sample_title: custom.sample_title || '',
title_template: custom.title_template || '',
subtitle_template: custom.subtitle_template || '',
description_template: custom.description_template || '',
upcoming_title_template: custom.upcoming_title_template || '',
upcoming_description_template:
@ -708,6 +724,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
setDatePattern(custom.date_pattern || '');
setSampleTitle(custom.sample_title || '');
setTitleTemplate(custom.title_template || '');
setSubtitleTemplate(custom.subtitle_template || '');
setDescriptionTemplate(custom.description_template || '');
setUpcomingTitleTemplate(custom.upcoming_title_template || '');
setUpcomingDescriptionTemplate(custom.upcoming_description_template || '');
@ -904,6 +921,20 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
}}
/>
<TextInput
id="subtitle_template"
name="subtitle_template"
label="Subtitle Template (Optional)"
description="Format the EPG subtitle using extracted groups. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), {endtime24} (24-hour end), {date} (YYYY-MM-DD), {month}, {day}, or {year}. Example: {starttime} - {endtime}"
placeholder="{starttime} - {endtime}"
value={subtitleTemplate}
onChange={(e) => {
const value = e.target.value;
setSubtitleTemplate(value);
form.setFieldValue('custom_properties.subtitle_template', value);
}}
/>
<Textarea
id="description_template"
name="description_template"
@ -1371,6 +1402,18 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
</>
)}
{subtitleTemplate && (
<>
<Text size="xs" c="dimmed" mt="xs">
EPG Subtitle:
</Text>
<Text size="sm" fw={500}>
{patternValidation.formattedSubtitle ||
'(no matching groups)'}
</Text>
</>
)}
{descriptionTemplate && (
<>
<Text size="xs" c="dimmed" mt="xs">
@ -1464,6 +1507,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
)}
{!titleTemplate &&
!subtitleTemplate &&
!descriptionTemplate &&
!upcomingTitleTemplate &&
!upcomingDescriptionTemplate &&