diff --git a/apps/epg/migrations/0020_migrate_time_to_starttime_placeholders.py b/apps/epg/migrations/0020_migrate_time_to_starttime_placeholders.py
new file mode 100644
index 00000000..8f53bb0a
--- /dev/null
+++ b/apps/epg/migrations/0020_migrate_time_to_starttime_placeholders.py
@@ -0,0 +1,119 @@
+# Generated migration to replace {time} placeholders with {starttime}
+
+import re
+from django.db import migrations
+
+
+def migrate_time_placeholders(apps, schema_editor):
+ """
+ Replace {time} with {starttime} and {time24} with {starttime24}
+ in all dummy EPG source custom_properties templates.
+ """
+ EPGSource = apps.get_model('epg', 'EPGSource')
+
+ # Fields that contain templates with placeholders
+ template_fields = [
+ 'title_template',
+ 'description_template',
+ 'upcoming_title_template',
+ 'upcoming_description_template',
+ 'ended_title_template',
+ 'ended_description_template',
+ 'channel_logo_url',
+ 'program_poster_url',
+ ]
+
+ # Get all dummy EPG sources
+ dummy_sources = EPGSource.objects.filter(source_type='dummy')
+
+ updated_count = 0
+ for source in dummy_sources:
+ if not source.custom_properties:
+ continue
+
+ modified = False
+ custom_props = source.custom_properties.copy()
+
+ for field in template_fields:
+ if field in custom_props and custom_props[field]:
+ original_value = custom_props[field]
+
+ # Replace {time24} first (before {time}) to avoid double replacement
+ # e.g., {time24} shouldn't become {starttime24} via {time} -> {starttime}
+ new_value = original_value
+ new_value = re.sub(r'\{time24\}', '{starttime24}', new_value)
+ new_value = re.sub(r'\{time\}', '{starttime}', new_value)
+
+ if new_value != original_value:
+ custom_props[field] = new_value
+ modified = True
+
+ if modified:
+ source.custom_properties = custom_props
+ source.save(update_fields=['custom_properties'])
+ updated_count += 1
+
+ if updated_count > 0:
+ print(f"Migration complete: Updated {updated_count} dummy EPG source(s) with new placeholder names.")
+ else:
+ print("No dummy EPG sources needed placeholder updates.")
+
+
+def reverse_migration(apps, schema_editor):
+ """
+ Reverse the migration by replacing {starttime} back to {time}.
+ """
+ EPGSource = apps.get_model('epg', 'EPGSource')
+
+ template_fields = [
+ 'title_template',
+ 'description_template',
+ 'upcoming_title_template',
+ 'upcoming_description_template',
+ 'ended_title_template',
+ 'ended_description_template',
+ 'channel_logo_url',
+ 'program_poster_url',
+ ]
+
+ dummy_sources = EPGSource.objects.filter(source_type='dummy')
+
+ updated_count = 0
+ for source in dummy_sources:
+ if not source.custom_properties:
+ continue
+
+ modified = False
+ custom_props = source.custom_properties.copy()
+
+ for field in template_fields:
+ if field in custom_props and custom_props[field]:
+ original_value = custom_props[field]
+
+ # Reverse the replacements
+ new_value = original_value
+ new_value = re.sub(r'\{starttime24\}', '{time24}', new_value)
+ new_value = re.sub(r'\{starttime\}', '{time}', new_value)
+
+ if new_value != original_value:
+ custom_props[field] = new_value
+ modified = True
+
+ if modified:
+ source.custom_properties = custom_props
+ source.save(update_fields=['custom_properties'])
+ updated_count += 1
+
+ if updated_count > 0:
+ print(f"Reverse migration complete: Reverted {updated_count} dummy EPG source(s) to old placeholder names.")
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('epg', '0019_alter_programdata_sub_title'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_time_placeholders, reverse_migration),
+ ]
diff --git a/apps/output/views.py b/apps/output/views.py
index f4b79058..1fc98d8e 100644
--- a/apps/output/views.py
+++ b/apps/output/views.py
@@ -571,13 +571,13 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
minute = temp_date_output.minute
logger.debug(f"Converted display time from {source_tz} to {output_tz}: {hour_24}:{minute:02d}")
- # Format 24-hour time string - only include minutes if non-zero
+ # Format 24-hour start time string - only include minutes if non-zero
if minute > 0:
- all_groups['time24'] = f"{hour_24}:{minute:02d}"
+ all_groups['starttime24'] = f"{hour_24}:{minute:02d}"
else:
- all_groups['time24'] = f"{hour_24:02d}:00"
+ all_groups['starttime24'] = f"{hour_24:02d}:00"
- # Convert 24-hour to 12-hour format for {time} placeholder
+ # Convert 24-hour to 12-hour format for {starttime} placeholder
# Note: hour_24 is ALWAYS in 24-hour format at this point (converted earlier if needed)
ampm = 'AM' if hour_24 < 12 else 'PM'
hour_12 = hour_24
@@ -586,11 +586,40 @@ def generate_custom_dummy_programs(channel_id, channel_name, now, num_days, cust
elif hour_24 > 12:
hour_12 = hour_24 - 12
- # Format 12-hour time string - only include minutes if non-zero
+ # Format 12-hour start time string - only include minutes if non-zero
if minute > 0:
- all_groups['time'] = f"{hour_12}:{minute:02d} {ampm}"
+ all_groups['starttime'] = f"{hour_12}:{minute:02d} {ampm}"
else:
- all_groups['time'] = f"{hour_12} {ampm}"
+ all_groups['starttime'] = f"{hour_12} {ampm}"
+
+ # Calculate end time based on program duration
+ # Create a datetime for calculations
+ temp_start = datetime.now(source_tz).replace(hour=hour_24, minute=minute, second=0, microsecond=0)
+ temp_end = temp_start + timedelta(minutes=program_duration)
+
+ # Extract end time components (already in correct timezone if output_tz was applied above)
+ end_hour_24 = temp_end.hour
+ end_minute = temp_end.minute
+
+ # Format 24-hour end time string - only include minutes if non-zero
+ if end_minute > 0:
+ all_groups['endtime24'] = f"{end_hour_24}:{end_minute:02d}"
+ else:
+ all_groups['endtime24'] = f"{end_hour_24:02d}:00"
+
+ # Convert 24-hour to 12-hour format for {endtime} placeholder
+ end_ampm = 'AM' if end_hour_24 < 12 else 'PM'
+ end_hour_12 = end_hour_24
+ if end_hour_24 == 0:
+ end_hour_12 = 12
+ elif end_hour_24 > 12:
+ end_hour_12 = end_hour_24 - 12
+
+ # Format 12-hour end time string - only include minutes if non-zero
+ if end_minute > 0:
+ all_groups['endtime'] = f"{end_hour_12}:{end_minute:02d} {end_ampm}"
+ else:
+ all_groups['endtime'] = f"{end_hour_12} {end_ampm}"
# Generate programs
programs = []
diff --git a/frontend/src/components/forms/DummyEPG.jsx b/frontend/src/components/forms/DummyEPG.jsx
index 9b5b3178..003cd9b3 100644
--- a/frontend/src/components/forms/DummyEPG.jsx
+++ b/frontend/src/components/forms/DummyEPG.jsx
@@ -106,6 +106,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
titleGroups: {},
timeGroups: {},
dateGroups: {},
+ calculatedPlaceholders: {},
formattedTitle: '',
formattedDescription: '',
formattedUpcomingTitle: '',
@@ -223,11 +224,11 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
hour24 = outputDate.hour();
const convertedMinute = outputDate.minute();
- // Format 24-hour time string with converted time
+ // Format 24-hour start time string with converted time
if (convertedMinute > 0) {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:${convertedMinute.toString().padStart(2, '0')}`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:${convertedMinute.toString().padStart(2, '0')}`;
} else {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:00`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:00`;
}
// Convert to 12-hour format with converted time
@@ -239,19 +240,19 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
hour12 = hour24 - 12;
}
- // Format 12-hour time string with converted time
+ // Format 12-hour start time string with converted time
if (convertedMinute > 0) {
- allGroups.time = `${hour12}:${convertedMinute.toString().padStart(2, '0')} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12}:${convertedMinute.toString().padStart(2, '0')} ${ampmDisplay}`;
} else {
- allGroups.time = `${hour12} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12} ${ampmDisplay}`;
}
} else {
// No timezone conversion - use original logic
- // Format 24-hour time string
+ // Format 24-hour start time string
if (minute > 0) {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
} else {
- allGroups.time24 = `${hour24.toString().padStart(2, '0')}:00`;
+ allGroups.starttime24 = `${hour24.toString().padStart(2, '0')}:00`;
}
// Convert to 12-hour format
@@ -263,15 +264,57 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
hour12 = hour24 - 12;
}
- // Format 12-hour time string
+ // Format 12-hour start time string
if (minute > 0) {
- allGroups.time = `${hour12}:${minute.toString().padStart(2, '0')} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12}:${minute.toString().padStart(2, '0')} ${ampmDisplay}`;
} else {
- allGroups.time = `${hour12} ${ampmDisplay}`;
+ allGroups.starttime = `${hour12} ${ampmDisplay}`;
}
}
+
+ // Calculate end time based on program duration
+ const programDuration =
+ form.values.custom_properties?.program_duration || 180;
+
+ // Calculate end time by adding duration to start time
+ const startMinutes = hour24 * 60 + minute;
+ const endMinutes = startMinutes + programDuration;
+
+ let endHour24 = Math.floor(endMinutes / 60) % 24; // Wrap around 24 hours
+ const endMinute = endMinutes % 60;
+
+ // Format 24-hour end time string
+ if (endMinute > 0) {
+ allGroups.endtime24 = `${endHour24.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}`;
+ } else {
+ allGroups.endtime24 = `${endHour24.toString().padStart(2, '0')}:00`;
+ }
+
+ // Convert to 12-hour format for endtime
+ const endAmpmDisplay = endHour24 < 12 ? 'AM' : 'PM';
+ let endHour12 = endHour24;
+ if (endHour24 === 0) {
+ endHour12 = 12;
+ } else if (endHour24 > 12) {
+ endHour12 = endHour24 - 12;
+ }
+
+ // Format 12-hour end time string
+ if (endMinute > 0) {
+ allGroups.endtime = `${endHour12}:${endMinute.toString().padStart(2, '0')} ${endAmpmDisplay}`;
+ } else {
+ allGroups.endtime = `${endHour12} ${endAmpmDisplay}`;
+ }
+
+ // Store calculated placeholders for display in preview
+ result.calculatedPlaceholders = {
+ starttime: allGroups.starttime,
+ starttime24: allGroups.starttime24,
+ endtime: allGroups.endtime,
+ endtime24: allGroups.endtime24,
+ };
} catch (e) {
- // If parsing fails, leave time/time24 as placeholders
+ // If parsing fails, leave starttime/endtime as placeholders
console.error('Error formatting time:', e);
}
}
@@ -367,6 +410,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
programPosterUrl,
form.values.custom_properties?.timezone,
form.values.custom_properties?.output_timezone,
+ form.values.custom_properties?.program_duration,
]);
useEffect(() => {
@@ -608,7 +652,7 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
id="title_template"
name="title_template"
label="Title Template"
- description="Format the EPG title using extracted groups. Use {time} (12-hour: '10 PM') or {time24} (24-hour: '22:00'). Example: {league} - {team1} vs {team2} @ {time}"
+ description="Format the EPG title using extracted groups. Use {starttime} (12-hour: '10 PM'), {starttime24} (24-hour: '22:00'), {endtime} (12-hour end), or {endtime24} (24-hour end). Example: {league} - {team1} vs {team2} ({starttime}-{endtime})"
placeholder="{league} - {team1} vs {team2}"
value={titleTemplate}
onChange={(e) => {
@@ -622,8 +666,8 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
id="description_template"
name="description_template"
label="Description Template"
- description="Format the EPG description using extracted groups. Use {time} (12-hour) or {time24} (24-hour). Example: Watch {team1} take on {team2} at {time}!"
- placeholder="Watch {team1} take on {team2} in this exciting {league} matchup at {time}!"
+ description="Format the EPG description using extracted groups. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), or {endtime24} (24-hour end). Example: Watch {team1} take on {team2} from {starttime} to {endtime}!"
+ placeholder="Watch {team1} take on {team2} in this exciting {league} matchup from {starttime} to {endtime}!"
minRows={2}
value={descriptionTemplate}
onChange={(e) => {
@@ -652,8 +696,8 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
id="upcoming_title_template"
name="upcoming_title_template"
label="Upcoming Title Template"
- description="Title for programs before the event starts. Use {time} (12-hour) or {time24} (24-hour). Example: {team1} vs {team2} starting at {time}."
- placeholder="{team1} vs {team2} starting at {time}."
+ description="Title for programs before the event starts. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), or {endtime24} (24-hour end). Example: {team1} vs {team2} starting at {starttime}."
+ placeholder="{team1} vs {team2} starting at {starttime}."
value={upcomingTitleTemplate}
onChange={(e) => {
const value = e.target.value;
@@ -669,8 +713,8 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
id="upcoming_description_template"
name="upcoming_description_template"
label="Upcoming Description Template"
- description="Description for programs before the event. Use {time} (12-hour) or {time24} (24-hour). Example: Upcoming: Watch the {league} match up where the {team1} take on the {team2} at {time}!"
- placeholder="Upcoming: Watch the {league} match up where the {team1} take on the {team2} at {time}!"
+ description="Description for programs before the event. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), or {endtime24} (24-hour end). Example: Upcoming: Watch the {league} match up where the {team1} take on the {team2} from {starttime} to {endtime}!"
+ placeholder="Upcoming: Watch the {league} match up where the {team1} take on the {team2} from {starttime} to {endtime}!"
minRows={2}
value={upcomingDescriptionTemplate}
onChange={(e) => {
@@ -687,8 +731,8 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
id="ended_title_template"
name="ended_title_template"
label="Ended Title Template"
- description="Title for programs after the event has ended. Use {time} (12-hour) or {time24} (24-hour). Example: {team1} vs {team2} started at {time}."
- placeholder="{team1} vs {team2} started at {time}."
+ description="Title for programs after the event has ended. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), or {endtime24} (24-hour end). Example: {team1} vs {team2} started at {starttime}."
+ placeholder="{team1} vs {team2} started at {starttime}."
value={endedTitleTemplate}
onChange={(e) => {
const value = e.target.value;
@@ -704,8 +748,8 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
id="ended_description_template"
name="ended_description_template"
label="Ended Description Template"
- description="Description for programs after the event. Use {time} (12-hour) or {time24} (24-hour). Example: The {league} match between {team1} and {team2} started at {time}."
- placeholder="The {league} match between {team1} and {team2} started at {time}."
+ description="Description for programs after the event. Use {starttime} (12-hour), {starttime24} (24-hour), {endtime} (12-hour end), or {endtime24} (24-hour end). Example: The {league} match between {team1} and {team2} ran from {starttime} to {endtime}."
+ placeholder="The {league} match between {team1} and {team2} ran from {starttime} to {endtime}."
minRows={2}
value={endedDescriptionTemplate}
onChange={(e) => {
@@ -934,6 +978,42 @@ const DummyEPGForm = ({ epg, isOpen, onClose }) => {
)}
+ {/* Show calculated time placeholders when time is extracted */}
+ {patternValidation.timeMatch &&
+ Object.keys(patternValidation.calculatedPlaceholders || {})
+ .length > 0 && (
+
+
+ Available Time Placeholders:
+
+
+ {Object.entries(
+ patternValidation.calculatedPlaceholders
+ ).map(([key, value]) => (
+
+
+ {'{' + key + '}'}:
+
+
+ {value}
+
+
+ ))}
+
+
+ )}
+
{patternValidation.dateMatch && (