mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
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:
parent
967704d7a0
commit
47853bdb5b
3 changed files with 77 additions and 1 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue