diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py
index d3062171..4fcf5706 100644
--- a/apps/epg/tasks.py
+++ b/apps/epg/tasks.py
@@ -1612,6 +1612,11 @@ def extract_custom_properties(prog):
if categories:
custom_props['categories'] = categories
+ # Extract keywords (new)
+ keywords = [kw.text.strip() for kw in prog.findall('keyword') if kw.text and kw.text.strip()]
+ if keywords:
+ custom_props['keywords'] = keywords
+
# Extract episode numbers
for ep_num in prog.findall('episode-num'):
system = ep_num.get('system', '')
@@ -1637,6 +1642,9 @@ def extract_custom_properties(prog):
elif system == 'dd_progid' and ep_num.text:
# Store the dd_progid format
custom_props['dd_progid'] = ep_num.text.strip()
+ # Add support for other systems like thetvdb.com, themoviedb.org, imdb.com
+ elif system in ['thetvdb.com', 'themoviedb.org', 'imdb.com'] and ep_num.text:
+ custom_props[f'{system}_id'] = ep_num.text.strip()
# Extract ratings more efficiently
rating_elem = prog.find('rating')
@@ -1647,37 +1655,172 @@ def extract_custom_properties(prog):
if rating_elem.get('system'):
custom_props['rating_system'] = rating_elem.get('system')
+ # Extract star ratings (new)
+ star_ratings = []
+ for star_rating in prog.findall('star-rating'):
+ value_elem = star_rating.find('value')
+ if value_elem is not None and value_elem.text:
+ rating_data = {'value': value_elem.text.strip()}
+ if star_rating.get('system'):
+ rating_data['system'] = star_rating.get('system')
+ star_ratings.append(rating_data)
+ if star_ratings:
+ custom_props['star_ratings'] = star_ratings
+
# Extract credits more efficiently
credits_elem = prog.find('credits')
if credits_elem is not None:
credits = {}
- for credit_type in ['director', 'actor', 'writer', 'presenter', 'producer']:
- names = [e.text.strip() for e in credits_elem.findall(credit_type) if e.text and e.text.strip()]
- if names:
- credits[credit_type] = names
+ for credit_type in ['director', 'actor', 'writer', 'adapter', 'producer', 'composer', 'editor', 'presenter', 'commentator', 'guest']:
+ if credit_type == 'actor':
+ # Handle actors with roles and guest status
+ actors = []
+ for actor_elem in credits_elem.findall('actor'):
+ if actor_elem.text and actor_elem.text.strip():
+ actor_data = {'name': actor_elem.text.strip()}
+ if actor_elem.get('role'):
+ actor_data['role'] = actor_elem.get('role')
+ if actor_elem.get('guest') == 'yes':
+ actor_data['guest'] = True
+ actors.append(actor_data)
+ if actors:
+ credits['actor'] = actors
+ else:
+ names = [e.text.strip() for e in credits_elem.findall(credit_type) if e.text and e.text.strip()]
+ if names:
+ credits[credit_type] = names
if credits:
custom_props['credits'] = credits
# Extract other common program metadata
date_elem = prog.find('date')
if date_elem is not None and date_elem.text:
- custom_props['year'] = date_elem.text.strip()[:4] # Just the year part
+ custom_props['date'] = date_elem.text.strip()
country_elem = prog.find('country')
if country_elem is not None and country_elem.text:
custom_props['country'] = country_elem.text.strip()
+ # Extract language information (new)
+ language_elem = prog.find('language')
+ if language_elem is not None and language_elem.text:
+ custom_props['language'] = language_elem.text.strip()
+
+ orig_language_elem = prog.find('orig-language')
+ if orig_language_elem is not None and orig_language_elem.text:
+ custom_props['original_language'] = orig_language_elem.text.strip()
+
+ # Extract length (new)
+ length_elem = prog.find('length')
+ if length_elem is not None and length_elem.text:
+ try:
+ length_value = int(length_elem.text.strip())
+ length_units = length_elem.get('units', 'minutes')
+ custom_props['length'] = {'value': length_value, 'units': length_units}
+ except ValueError:
+ pass
+
+ # Extract video information (new)
+ video_elem = prog.find('video')
+ if video_elem is not None:
+ video_info = {}
+ for video_attr in ['present', 'colour', 'aspect', 'quality']:
+ attr_elem = video_elem.find(video_attr)
+ if attr_elem is not None and attr_elem.text:
+ video_info[video_attr] = attr_elem.text.strip()
+ if video_info:
+ custom_props['video'] = video_info
+
+ # Extract audio information (new)
+ audio_elem = prog.find('audio')
+ if audio_elem is not None:
+ audio_info = {}
+ for audio_attr in ['present', 'stereo']:
+ attr_elem = audio_elem.find(audio_attr)
+ if attr_elem is not None and attr_elem.text:
+ audio_info[audio_attr] = attr_elem.text.strip()
+ if audio_info:
+ custom_props['audio'] = audio_info
+
+ # Extract subtitles information (new)
+ subtitles = []
+ for subtitle_elem in prog.findall('subtitles'):
+ subtitle_data = {}
+ if subtitle_elem.get('type'):
+ subtitle_data['type'] = subtitle_elem.get('type')
+ lang_elem = subtitle_elem.find('language')
+ if lang_elem is not None and lang_elem.text:
+ subtitle_data['language'] = lang_elem.text.strip()
+ if subtitle_data:
+ subtitles.append(subtitle_data)
+
+ if subtitles:
+ custom_props['subtitles'] = subtitles
+
+ # Extract reviews (new)
+ reviews = []
+ for review_elem in prog.findall('review'):
+ if review_elem.text and review_elem.text.strip():
+ review_data = {'content': review_elem.text.strip()}
+ if review_elem.get('type'):
+ review_data['type'] = review_elem.get('type')
+ if review_elem.get('source'):
+ review_data['source'] = review_elem.get('source')
+ if review_elem.get('reviewer'):
+ review_data['reviewer'] = review_elem.get('reviewer')
+ reviews.append(review_data)
+ if reviews:
+ custom_props['reviews'] = reviews
+
+ # Extract images (new)
+ images = []
+ for image_elem in prog.findall('image'):
+ if image_elem.text and image_elem.text.strip():
+ image_data = {'url': image_elem.text.strip()}
+ for attr in ['type', 'size', 'orient', 'system']:
+ if image_elem.get(attr):
+ image_data[attr] = image_elem.get(attr)
+ images.append(image_data)
+ if images:
+ custom_props['images'] = images
+
icon_elem = prog.find('icon')
if icon_elem is not None and icon_elem.get('src'):
custom_props['icon'] = icon_elem.get('src')
- # Simpler approach for boolean flags
- for kw in ['previously-shown', 'premiere', 'new', 'live']:
+ # Simpler approach for boolean flags - expanded list
+ for kw in ['previously-shown', 'premiere', 'new', 'live', 'last-chance']:
if prog.find(kw) is not None:
custom_props[kw.replace('-', '_')] = True
+ # Extract premiere and last-chance text content if available
+ premiere_elem = prog.find('premiere')
+ if premiere_elem is not None:
+ custom_props['premiere'] = True
+ if premiere_elem.text and premiere_elem.text.strip():
+ custom_props['premiere_text'] = premiere_elem.text.strip()
+
+ last_chance_elem = prog.find('last-chance')
+ if last_chance_elem is not None:
+ custom_props['last_chance'] = True
+ if last_chance_elem.text and last_chance_elem.text.strip():
+ custom_props['last_chance_text'] = last_chance_elem.text.strip()
+
+ # Extract previously-shown details
+ prev_shown_elem = prog.find('previously-shown')
+ if prev_shown_elem is not None:
+ custom_props['previously_shown'] = True
+ prev_shown_data = {}
+ if prev_shown_elem.get('start'):
+ prev_shown_data['start'] = prev_shown_elem.get('start')
+ if prev_shown_elem.get('channel'):
+ prev_shown_data['channel'] = prev_shown_elem.get('channel')
+ if prev_shown_data:
+ custom_props['previously_shown_details'] = prev_shown_data
+
return custom_props
+
def clear_element(elem):
"""Clear an XML element and its parent to free memory."""
try:
diff --git a/apps/output/views.py b/apps/output/views.py
index 4ef9f4f2..67d72bd2 100644
--- a/apps/output/views.py
+++ b/apps/output/views.py
@@ -467,19 +467,27 @@ def generate_epg(request, profile_name=None, user=None):
for category in custom_data["categories"]:
program_xml.append(f" {html.escape(category)}")
- # Handle episode numbering - multiple formats supported
- # Standard episode number if available
- if "episode" in custom_data:
- program_xml.append(f' E{custom_data["episode"]}')
+ # Add keywords if available
+ if "keywords" in custom_data and custom_data["keywords"]:
+ for keyword in custom_data["keywords"]:
+ program_xml.append(f" {html.escape(keyword)}")
- # Handle onscreen episode format (like S06E128)
+ # Handle episode numbering - multiple formats supported
+ # Prioritize onscreen_episode over standalone episode for onscreen system
if "onscreen_episode" in custom_data:
program_xml.append(f' {html.escape(custom_data["onscreen_episode"])}')
+ elif "episode" in custom_data:
+ program_xml.append(f' E{custom_data["episode"]}')
# Handle dd_progid format
if 'dd_progid' in custom_data:
program_xml.append(f' {html.escape(custom_data["dd_progid"])}')
+ # Handle external database IDs
+ for system in ['thetvdb.com', 'themoviedb.org', 'imdb.com']:
+ if f'{system}_id' in custom_data:
+ program_xml.append(f' {html.escape(custom_data[f"{system}_id"])}')
+
# Add season and episode numbers in xmltv_ns format if available
if "season" in custom_data and "episode" in custom_data:
season = (
@@ -494,6 +502,46 @@ def generate_epg(request, profile_name=None, user=None):
)
program_xml.append(f' {season}.{episode}.')
+ # Add language information
+ if "language" in custom_data:
+ program_xml.append(f' {html.escape(custom_data["language"])}')
+
+ if "original_language" in custom_data:
+ program_xml.append(f' {html.escape(custom_data["original_language"])}')
+
+ # Add length information
+ if "length" in custom_data and isinstance(custom_data["length"], dict):
+ length_value = custom_data["length"].get("value", "")
+ length_units = custom_data["length"].get("units", "minutes")
+ program_xml.append(f' {html.escape(str(length_value))}')
+
+ # Add video information
+ if "video" in custom_data and isinstance(custom_data["video"], dict):
+ program_xml.append(" ")
+
+ # Add audio information
+ if "audio" in custom_data and isinstance(custom_data["audio"], dict):
+ program_xml.append(" ")
+
+ # Add subtitles information
+ if "subtitles" in custom_data and isinstance(custom_data["subtitles"], list):
+ for subtitle in custom_data["subtitles"]:
+ if isinstance(subtitle, dict):
+ subtitle_type = subtitle.get("type", "")
+ type_attr = f' type="{html.escape(subtitle_type)}"' if subtitle_type else ""
+ program_xml.append(f" ")
+ if "language" in subtitle:
+ program_xml.append(f" {html.escape(subtitle['language'])}")
+ program_xml.append(" ")
+
# Add rating if available
if "rating" in custom_data:
rating_system = custom_data.get("rating_system", "TV Parental Guidelines")
@@ -501,20 +549,74 @@ def generate_epg(request, profile_name=None, user=None):
program_xml.append(f' {html.escape(custom_data["rating"])}')
program_xml.append(f" ")
- # Add actors/directors/writers if available
- if "credits" in custom_data:
- program_xml.append(f" ")
- for role, people in custom_data["credits"].items():
- if isinstance(people, list):
- for person in people:
- program_xml.append(f" <{role}>{html.escape(person)}{role}>")
- else:
- program_xml.append(f" <{role}>{html.escape(people)}{role}>")
- program_xml.append(f" ")
+ # Add star ratings
+ if "star_ratings" in custom_data and isinstance(custom_data["star_ratings"], list):
+ for star_rating in custom_data["star_ratings"]:
+ if isinstance(star_rating, dict) and "value" in star_rating:
+ system_attr = f' system="{html.escape(star_rating["system"])}"' if "system" in star_rating else ""
+ program_xml.append(f" ")
+ program_xml.append(f" {html.escape(star_rating['value'])}")
+ program_xml.append(" ")
- # Add program date/year if available
- if "year" in custom_data:
- program_xml.append(f' {html.escape(custom_data["year"])}')
+ # Add reviews
+ if "reviews" in custom_data and isinstance(custom_data["reviews"], list):
+ for review in custom_data["reviews"]:
+ if isinstance(review, dict) and "content" in review:
+ review_type = review.get("type", "text")
+ attrs = [f'type="{html.escape(review_type)}"']
+ if "source" in review:
+ attrs.append(f'source="{html.escape(review["source"])}"')
+ if "reviewer" in review:
+ attrs.append(f'reviewer="{html.escape(review["reviewer"])}"')
+ attr_str = " ".join(attrs)
+ program_xml.append(f' {html.escape(review["content"])}')
+
+ # Add images
+ if "images" in custom_data and isinstance(custom_data["images"], list):
+ for image in custom_data["images"]:
+ if isinstance(image, dict) and "url" in image:
+ attrs = []
+ for attr in ['type', 'size', 'orient', 'system']:
+ if attr in image:
+ attrs.append(f'{attr}="{html.escape(image[attr])}"')
+ attr_str = " " + " ".join(attrs) if attrs else ""
+ program_xml.append(f' {html.escape(image["url"])}')
+
+ # Add enhanced credits handling
+ if "credits" in custom_data:
+ program_xml.append(" ")
+ credits = custom_data["credits"]
+
+ # Handle different credit types
+ for role in ['director', 'writer', 'adapter', 'producer', 'composer', 'editor', 'presenter', 'commentator', 'guest']:
+ if role in credits:
+ people = credits[role]
+ if isinstance(people, list):
+ for person in people:
+ program_xml.append(f" <{role}>{html.escape(person)}{role}>")
+ else:
+ program_xml.append(f" <{role}>{html.escape(people)}{role}>")
+
+ # Handle actors separately to include role and guest attributes
+ if "actor" in credits:
+ actors = credits["actor"]
+ if isinstance(actors, list):
+ for actor in actors:
+ if isinstance(actor, dict):
+ name = actor.get("name", "")
+ role_attr = f' role="{html.escape(actor["role"])}"' if "role" in actor else ""
+ guest_attr = ' guest="yes"' if actor.get("guest") else ""
+ program_xml.append(f" {html.escape(name)}")
+ else:
+ program_xml.append(f" {html.escape(actor)}")
+ else:
+ program_xml.append(f" {html.escape(actors)}")
+
+ program_xml.append(" ")
+
+ # Add program date if available (full date, not just year)
+ if "date" in custom_data:
+ program_xml.append(f' {html.escape(custom_data["date"])}')
# Add country if available
if "country" in custom_data:
@@ -524,18 +626,36 @@ def generate_epg(request, profile_name=None, user=None):
if "icon" in custom_data:
program_xml.append(f' ')
- # Add special flags as proper tags
+ # Add special flags as proper tags with enhanced handling
if custom_data.get("previously_shown", False):
- program_xml.append(f" ")
+ prev_shown_details = custom_data.get("previously_shown_details", {})
+ attrs = []
+ if "start" in prev_shown_details:
+ attrs.append(f'start="{html.escape(prev_shown_details["start"])}"')
+ if "channel" in prev_shown_details:
+ attrs.append(f'channel="{html.escape(prev_shown_details["channel"])}"')
+ attr_str = " " + " ".join(attrs) if attrs else ""
+ program_xml.append(f" ")
if custom_data.get("premiere", False):
- program_xml.append(f" ")
+ premiere_text = custom_data.get("premiere_text", "")
+ if premiere_text:
+ program_xml.append(f" {html.escape(premiere_text)}")
+ else:
+ program_xml.append(" ")
+
+ if custom_data.get("last_chance", False):
+ last_chance_text = custom_data.get("last_chance_text", "")
+ if last_chance_text:
+ program_xml.append(f" {html.escape(last_chance_text)}")
+ else:
+ program_xml.append(" ")
if custom_data.get("new", False):
- program_xml.append(f" ")
+ program_xml.append(" ")
if custom_data.get('live', False):
- program_xml.append(f' ')
+ program_xml.append(' ')
except Exception as e:
program_xml.append(f" ")