From 11bee5a7c061c11c35314ca4643c21e62f275e4a Mon Sep 17 00:00:00 2001 From: Nirvana Date: Sat, 6 Dec 2025 14:25:57 +0100 Subject: [PATCH] Improve EPG --- .../base/epg/epg_manager.py | 84 +++- .../base/epg/epg_parser.py | 157 ++++--- .../base/models/epg_models.py | 410 ++++++++++++++++++ 3 files changed, 578 insertions(+), 73 deletions(-) create mode 100644 lib/streaming_providers/base/models/epg_models.py diff --git a/lib/streaming_providers/base/epg/epg_manager.py b/lib/streaming_providers/base/epg/epg_manager.py index 63c6a06..9eb8d04 100644 --- a/lib/streaming_providers/base/epg/epg_manager.py +++ b/lib/streaming_providers/base/epg/epg_manager.py @@ -1,8 +1,15 @@ -#!/usr/bin/env python3 +# ============================================================================ +# EPG MANAGER - Refactored to work with EPGEntry objects internally +# ============================================================================ + +# !/usr/bin/env python3 # streaming_providers/base/epg/epg_manager.py """ EPG Manager - Central coordinator for EPG operations Orchestrates cache, mapping, and parsing components + +Refactored to use EPGEntry objects internally while maintaining +backward-compatible dictionary output for external consumers. """ from datetime import datetime, timedelta @@ -10,6 +17,7 @@ from typing import List, Dict, Optional from .epg_cache import EPGCache from .epg_mapping import EPGMapping from .epg_parser import EPGParser +from ..models.epg_models import EPGEntry from ..utils.logger import logger @@ -18,6 +26,9 @@ class EPGManager: Central manager for EPG operations. Coordinates cache management, channel mapping, and EPG parsing. + Internal: Works with EPGEntry objects for type safety and validation + External: Returns dictionaries for backward compatibility with C++ frontend + All file operations are handled transparently by VFS - works in both Kodi and standard Python environments. """ @@ -66,7 +77,8 @@ class EPGManager: return None - def _get_default_time_range(self) -> tuple: + @staticmethod + def _get_default_time_range() -> tuple: """ Get default time range for EPG queries (now to +12 hours). @@ -92,6 +104,8 @@ class EPGManager: This is the main entry point for EPG queries. + EXTERNAL INTERFACE: Returns list of dictionaries for C++ compatibility. + Args: provider_name: Name of provider (e.g., "rtlplus", "joyn_de") channel_id: Channel ID within provider @@ -131,8 +145,8 @@ class EPGManager: logger.debug(f"EPGManager: Time range: {start_ts} to {end_ts}") - # Step 4: Parse EPG for channel and time range - epg_entries = self.parser.parse_epg_for_channel( + # Step 4: Parse EPG for channel and time range (returns EPGEntry objects) + epg_entries: List[EPGEntry] = self.parser.parse_epg_for_channel( xml_path, epg_channel_id, start_ts, @@ -144,12 +158,68 @@ class EPGManager: f"{provider_name}/{channel_id}" ) - return epg_entries + # Step 5: Convert EPGEntry objects to dictionaries for external consumers + return [entry.to_dict() for entry in epg_entries] except Exception as e: logger.error(f"EPGManager: Error getting EPG: {e}", exc_info=True) return [] + def get_epg_entries( + self, + provider_name: str, + channel_id: str, + start_time: Optional[datetime] = None, + end_time: Optional[datetime] = None + ) -> List[EPGEntry]: + """ + Get EPG data as EPGEntry objects (for internal use). + + INTERNAL INTERFACE: Returns EPGEntry objects for type safety. + + Args: + provider_name: Name of provider + channel_id: Channel ID within provider + start_time: Start of time range (datetime), None for now + end_time: End of time range (datetime), None for now+12h + + Returns: + List of EPGEntry objects (empty on any error) + """ + logger.info(f"EPGManager: Getting EPG entries for {provider_name}/{channel_id}") + + try: + epg_channel_id = self.mapping.get_epg_channel_id(provider_name, channel_id) + if not epg_channel_id: + logger.warning( + f"EPGManager: No EPG mapping found for {provider_name}/{channel_id}" + ) + return [] + + xml_path = self.cache.get_or_download(self.epg_url) + if not xml_path: + logger.error("EPGManager: Failed to get EPG XML file") + return [] + + if start_time is None or end_time is None: + default_start, default_end = self._get_default_time_range() + start_ts = int(start_time.timestamp()) if start_time else default_start + end_ts = int(end_time.timestamp()) if end_time else default_end + else: + start_ts = int(start_time.timestamp()) + end_ts = int(end_time.timestamp()) + + return self.parser.parse_epg_for_channel( + xml_path, + epg_channel_id, + start_ts, + end_ts + ) + + except Exception as e: + logger.error(f"EPGManager: Error getting EPG entries: {e}", exc_info=True) + return [] + def get_epg_for_provider( self, provider_name: str, @@ -159,13 +229,15 @@ class EPGManager: """ Get EPG data for all channels of a provider. + EXTERNAL INTERFACE: Returns dictionaries for C++ compatibility. + Args: provider_name: Name of provider start_time: Start of time range (datetime) end_time: End of time range (datetime) Returns: - Dictionary mapping channel IDs to their EPG entries + Dictionary mapping channel IDs to their EPG entries (as dicts) """ logger.info(f"EPGManager: Getting EPG for all channels of provider '{provider_name}'") diff --git a/lib/streaming_providers/base/epg/epg_parser.py b/lib/streaming_providers/base/epg/epg_parser.py index f8dca58..7303ca1 100644 --- a/lib/streaming_providers/base/epg/epg_parser.py +++ b/lib/streaming_providers/base/epg/epg_parser.py @@ -9,14 +9,22 @@ import xml.etree.ElementTree as ET import gzip import hashlib from datetime import datetime -from typing import List, Dict, Optional, Tuple, Any +from typing import List, Optional, Tuple, Any, Dict +from ..models.epg_models import EPGEntry +from ..models import epg_models from ..utils.logger import logger +# Import constants with proper naming +EPGGenre = epg_models.EPGGenre +EPGFlags = epg_models.EPGFlags + class EPGParser: """ Parses XMLTV EPG data and converts to Kodi PVR format. Uses streaming parse for performance with large EPG files. + + Now returns EPGEntry objects internally for validation and type safety. """ @staticmethod @@ -181,16 +189,57 @@ class EPGParser: return None @staticmethod - def parse_programme(programme_elem: ET.Element, epg_channel_id: str) -> Optional[Dict]: + def _map_genre_to_type(genre_str: str) -> int: """ - Parse a single XMLTV programme element to Kodi EPG format. + Map genre string to numeric genre type based on DVB-SI standard. + + Args: + genre_str: Genre string from XMLTV (lowercase) + + Returns: + Numeric genre type from EPGGenre (DVB-SI ETSI EN 300 468) + """ + if 'movie' in genre_str or 'film' in genre_str or 'drama' in genre_str: + return EPGGenre.MOVIEDRAMA + elif 'news' in genre_str: + return EPGGenre.NEWSCURRENTAFFAIRS + elif 'show' in genre_str or 'series' in genre_str or 'serie' in genre_str: + return EPGGenre.SHOW + elif 'sports' in genre_str or 'sport' in genre_str: + return EPGGenre.SPORTS + elif 'children' in genre_str or 'kids' in genre_str or 'cartoon' in genre_str or 'youth' in genre_str: + return EPGGenre.CHILDRENYOUTH + elif 'documentary' in genre_str or 'dokumentation' in genre_str: + # Documentary can be either News/Current Affairs or Educational/Science + # We'll use News/Current Affairs as it's more common + return EPGGenre.NEWSCURRENTAFFAIRS + elif 'music' in genre_str or 'ballet' in genre_str or 'dance' in genre_str: + return EPGGenre.MUSICBALLETDANCE + elif 'arts' in genre_str or 'culture' in genre_str or 'art' in genre_str: + return EPGGenre.ARTSCULTURE + elif 'educational' in genre_str or 'education' in genre_str or 'science' in genre_str: + return EPGGenre.EDUCATIONALSCIENCE + elif 'social' in genre_str or 'political' in genre_str or 'politics' in genre_str or 'economic' in genre_str: + return EPGGenre.SOCIALPOLITICALECONOMICS + elif 'leisure' in genre_str or 'hobbies' in genre_str or 'hobby' in genre_str: + return EPGGenre.LEISUREHOBBIES + elif 'comedy' in genre_str: + # Comedy is a subtype of Movie/Drama + return EPGGenre.MOVIEDRAMA + else: + return EPGGenre.UNDEFINED + + @staticmethod + def parse_programme(programme_elem: ET.Element, epg_channel_id: str) -> Optional[EPGEntry]: + """ + Parse a single XMLTV programme element to EPGEntry object. Args: programme_elem: XML programme element epg_channel_id: EPG channel ID for validation Returns: - Dictionary with EPG data, or None if required fields missing + EPGEntry object, or None if required fields missing or validation fails """ # Verify channel matches channel = programme_elem.get('channel', '') @@ -215,12 +264,12 @@ class EPGParser: # Generate broadcast ID broadcast_id = EPGParser.generate_broadcast_id(epg_channel_id, start_time) - # Build base EPG entry with required fields for C++ frontend - epg_entry: Dict[str, Any] = { + # Build EPGEntry with required fields + entry_kwargs: Dict[str, Any] = { 'broadcast_id': broadcast_id, 'title': title.strip(), - 'start': start_time, # C++ expects 'start' key - 'end': end_time, # C++ expects 'end' key + 'start': start_time, + 'end': end_time, } # Parse optional fields @@ -228,86 +277,59 @@ class EPGParser: # Sub-title (episode name) subtitle_elem = programme_elem.find('sub-title') if subtitle_elem is not None and subtitle_elem.text: - epg_entry['episode_name'] = subtitle_elem.text.strip() + entry_kwargs['episode_name'] = subtitle_elem.text.strip() # Description (plot) desc_elem = programme_elem.find('desc') if desc_elem is not None and desc_elem.text: plot = desc_elem.text.strip() - epg_entry['description'] = plot # C++ expects 'description' for full plot + entry_kwargs['description'] = plot # Use first sentence or first 100 chars as plot_outline outline = plot.split('.')[0] if '.' in plot else plot[:100] - epg_entry['plot_outline'] = outline.strip() + entry_kwargs['plot_outline'] = outline.strip() # Credits credits_elem = programme_elem.find('credits') if credits_elem is not None: - # Director - C++ expects 'directors' as array + # Director director_elem = credits_elem.find('director') if director_elem is not None and director_elem.text: - director = director_elem.text.strip() - epg_entry['directors'] = [director] + entry_kwargs['directors'] = [director_elem.text.strip()] - # Actors - C++ expects 'cast' as array + # Actors actors = credits_elem.findall('actor') if actors: cast_list = [actor.text.strip() for actor in actors if actor.text] if cast_list: - epg_entry['cast'] = cast_list + entry_kwargs['cast'] = cast_list - # Writer - C++ expects 'writers' as array + # Writer writer_elem = credits_elem.find('writer') if writer_elem is not None and writer_elem.text: - writer = writer_elem.text.strip() - epg_entry['writers'] = [writer] + entry_kwargs['writers'] = [writer_elem.text.strip()] # Year/Date date_elem = programme_elem.find('date') if date_elem is not None and date_elem.text: try: year = int(date_elem.text.strip()[:4]) - epg_entry['year'] = year + entry_kwargs['year'] = year except ValueError: pass - # Category (genre) - Convert to numeric genre type for C++ + # Category (genre) category_elem = programme_elem.find('category') if category_elem is not None and category_elem.text: genre_str = category_elem.text.strip().lower() + entry_kwargs['genre'] = EPGParser._map_genre_to_type(genre_str) - # Convert to numeric genre type (Kodi expects 0-...) - genre_type = 0 # Unknown - - if 'movie' in genre_str or 'film' in genre_str: - genre_type = 1 # Movie - elif 'news' in genre_str: - genre_type = 2 # News - elif 'show' in genre_str or 'series' in genre_str or 'serie' in genre_str: - genre_type = 3 # TV Show - elif 'sports' in genre_str or 'sport' in genre_str: - genre_type = 4 # Sports - elif 'children' in genre_str or 'kids' in genre_str or 'cartoon' in genre_str: - genre_type = 5 # Children's - elif 'documentary' in genre_str or 'dokumentation' in genre_str: - genre_type = 6 # Documentary - elif 'music' in genre_str: - genre_type = 7 # Music - elif 'comedy' in genre_str: - genre_type = 8 # Comedy - elif 'drama' in genre_str: - genre_type = 9 # Drama - elif 'educational' in genre_str or 'education' in genre_str: - genre_type = 10 # Educational - - epg_entry['genre'] = genre_type - - # Icon - C++ expects 'icon' + # Icon icon_elem = programme_elem.find('icon') if icon_elem is not None: icon_src = icon_elem.get('src', '') if icon_src: - epg_entry['icon'] = icon_src + entry_kwargs['icon'] = icon_src # Episode numbering episode_nums = programme_elem.findall('episode-num') @@ -327,18 +349,18 @@ class EPGParser: break if series_num is not None: - epg_entry['season_number'] = series_num # C++ expects 'season_number' + entry_kwargs['season_number'] = series_num if episode_num is not None: - epg_entry['episode_number'] = episode_num + entry_kwargs['episode_number'] = episode_num if part_num is not None: - epg_entry['episode_part_number'] = part_num + entry_kwargs['episode_part_number'] = part_num # Star rating star_rating_elem = programme_elem.find('star-rating') if star_rating_elem is not None: rating = EPGParser.parse_star_rating(star_rating_elem) if rating is not None: - epg_entry['star_rating'] = rating + entry_kwargs['star_rating'] = rating # Parental rating rating_elems = programme_elem.findall('rating') @@ -350,7 +372,7 @@ class EPGParser: import re match = re.search(r'\d+', value_elem.text) if match: - epg_entry['parental_rating'] = int(match.group()) + entry_kwargs['parental_rating'] = int(match.group()) break except (ValueError, AttributeError): pass @@ -362,9 +384,14 @@ class EPGParser: if first_aired_str: first_aired = EPGParser.parse_xmltv_time(first_aired_str) if first_aired: - epg_entry['first_aired'] = first_aired + entry_kwargs['first_aired'] = first_aired - return epg_entry + # Create EPGEntry object with validation + try: + return EPGEntry(**entry_kwargs) + except ValueError as e: + logger.warning(f"Invalid EPG entry: {e}") + return None @staticmethod def open_xml_file(file_path: str): @@ -390,7 +417,7 @@ class EPGParser: epg_channel_id: str, start_time: Optional[int] = None, end_time: Optional[int] = None - ) -> List[Dict]: + ) -> List[EPGEntry]: """ Parse EPG data for a specific channel within a time range. Uses streaming parse for performance. @@ -402,11 +429,11 @@ class EPGParser: end_time: End of time range (Unix timestamp), None for no upper bound Returns: - List of EPG entries as dictionaries + List of EPGEntry objects """ logger.info(f"Parsing EPG for channel '{epg_channel_id}' from {xml_path}") - programmes = [] + programmes: List[EPGEntry] = [] try: with EPGParser.open_xml_file(xml_path) as xml_file: @@ -421,17 +448,13 @@ class EPGParser: epg_entry = EPGParser.parse_programme(elem, epg_channel_id) if epg_entry: - # Filter by time range - prog_start = epg_entry['start'] # Changed from 'start_time' - prog_end = epg_entry['end'] # Changed from 'end_time' - - # Include if programme overlaps with requested range - if start_time is not None and prog_end < start_time: + # Filter by time range using EPGEntry methods + if start_time is not None and epg_entry.end < start_time: # Programme ends before requested range elem.clear() continue - if end_time is not None and prog_start > end_time: + if end_time is not None and epg_entry.start > end_time: # Programme starts after requested range # Since XMLTV is chronological, we can stop here elem.clear() @@ -450,4 +473,4 @@ class EPGParser: return [] except Exception as e: logger.error(f"Error parsing EPG file: {e}") - return [] \ No newline at end of file + return [] diff --git a/lib/streaming_providers/base/models/epg_models.py b/lib/streaming_providers/base/models/epg_models.py new file mode 100644 index 0000000..e1b4176 --- /dev/null +++ b/lib/streaming_providers/base/models/epg_models.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +# streaming_providers/base/models/epg_models.py +""" +EPG Models - Data classes for Electronic Program Guide entries +Based on Kodi PVR EPG Tag specification +""" + +from dataclasses import dataclass +from typing import Optional, List +from datetime import datetime + + +@dataclass +class EPGEntry: + """ + EPG Entry model for Kodi PVR Backend. + + Represents a single program/event in the Electronic Program Guide. + Based on Kodi's PVREPGTag C++ class specification. + + This class matches the dictionary format used by epg_parser.py and + expected by the C++ PVR frontend. + """ + + # Required fields + broadcast_id: int + """Unique identifier for this broadcast event. Must be unique per channel.""" + + title: str + """Program title.""" + + start: int + """Start time as Unix timestamp (seconds since epoch).""" + + end: int + """End time as Unix timestamp (seconds since epoch).""" + + # Optional fields - Program Information + description: Optional[str] = None + """Full program description/plot. C++ expects 'description' key.""" + + plot_outline: Optional[str] = None + """Short plot outline (first sentence or ~100 chars of description).""" + + episode_name: Optional[str] = None + """Episode title/name (XMLTV sub-title).""" + + original_title: Optional[str] = None + """Original title if different from main title.""" + + # Optional fields - Media Metadata + year: Optional[int] = None + """Production year.""" + + icon: Optional[str] = None + """URL to program icon/poster image. C++ expects 'icon' key.""" + + # Optional fields - People (C++ expects arrays) + cast: Optional[List[str]] = None + """List of actor names. C++ expects array format.""" + + directors: Optional[List[str]] = None + """List of director names. C++ expects 'directors' array.""" + + writers: Optional[List[str]] = None + """List of writer names. C++ expects 'writers' array.""" + + # Optional fields - Genre/Category + genre: Optional[int] = None + """ + Numeric genre type based on DVB-SI standard (ETSI EN 300 468). + Use EPGGenre constants (e.g., EPGGenre.MOVIEDRAMA = 0x10). + Set to EPGGenre.USE_STRING (0xF0) to use genre_description instead. + """ + + genre_sub_type: Optional[int] = None + """ + Numeric genre subtype based on DVB-SI standard. + Use EPGGenreSubtype nested classes (e.g., EPGGenreSubtype.Sports.FOOTBALL_SOCCER). + Must be used in combination with appropriate genre type. + """ + + genre_description: Optional[str] = None + """ + Text description of genre. + Used when genre=EPGGenre.USE_STRING or for custom genres not in DVB-SI standard. + """ + + # Optional fields - Episode Information + season_number: Optional[int] = None + """Season/series number (1-based). C++ expects 'season_number' key.""" + + episode_number: Optional[int] = None + """Episode number within season (1-based).""" + + episode_part_number: Optional[int] = None + """Part number for multi-part episodes (1-based).""" + + # Optional fields - Ratings + star_rating: Optional[int] = None + """Star rating on 0-10 scale.""" + + parental_rating: Optional[int] = None + """Parental rating code/age restriction.""" + + parental_rating_code: Optional[str] = None + """Text parental rating code (e.g., 'TV-PG', 'FSK 12').""" + + # Optional fields - Additional Metadata + first_aired: Optional[int] = None + """Original air date as Unix timestamp.""" + + imdb_number: Optional[str] = None + """IMDB identifier.""" + + series_link: Optional[str] = None + """Link to series information.""" + + flags: Optional[int] = None + """ + Bit field flags for EPG entry properties. + Common flags: IS_SERIES=0x01, IS_NEW=0x02, IS_PREMIERE=0x04, + IS_FINALE=0x08, IS_LIVE=0x10 + """ + + def to_dict(self) -> dict: + """ + Convert EPGEntry to dictionary format expected by C++ frontend. + Only includes non-None values to minimize data transfer. + + Returns: + Dictionary with EPG data + """ + result = { + 'broadcast_id': self.broadcast_id, + 'title': self.title, + 'start': self.start, + 'end': self.end, + } + + # Add optional fields only if they have values + optional_fields = [ + 'description', 'plot_outline', 'episode_name', 'original_title', + 'year', 'icon', 'cast', 'directors', 'writers', 'genre', + 'genre_description', 'season_number', 'episode_number', + 'episode_part_number', 'star_rating', 'parental_rating', + 'parental_rating_code', 'first_aired', 'imdb_number', + 'series_link', 'flags' + ] + + for field_name in optional_fields: + value = getattr(self, field_name) + if value is not None: + result[field_name] = value + + return result + + @classmethod + def from_dict(cls, data: dict) -> 'EPGEntry': + """ + Create EPGEntry from dictionary (e.g., from epg_parser output). + + Args: + data: Dictionary with EPG data + + Returns: + EPGEntry instance + """ + from dataclasses import fields + + # Extract only fields that exist in EPGEntry + valid_fields = {f.name for f in fields(cls)} + + filtered_data = { + k: v for k, v in data.items() + if k in valid_fields + } + + return cls(**filtered_data) + + @property + def duration_seconds(self) -> int: + """Calculate program duration in seconds.""" + return self.end - self.start + + @property + def start_datetime(self) -> datetime: + """Get start time as datetime object.""" + return datetime.fromtimestamp(self.start) + + @property + def end_datetime(self) -> datetime: + """Get end time as datetime object.""" + return datetime.fromtimestamp(self.end) + + def is_currently_airing(self, reference_time: Optional[int] = None) -> bool: + """ + Check if program is currently airing. + + Args: + reference_time: Unix timestamp to check against (None = now) + + Returns: + True if program is airing at reference_time + """ + if reference_time is None: + reference_time = int(datetime.now().timestamp()) + + return self.start <= reference_time < self.end + + def overlaps_with(self, start_time: int, end_time: int) -> bool: + """ + Check if this program overlaps with a given time range. + + Args: + start_time: Start of time range (Unix timestamp) + end_time: End of time range (Unix timestamp) + + Returns: + True if there is any overlap + """ + # Program overlaps if it doesn't end before range starts + # and doesn't start after range ends + return self.end > start_time and self.start < end_time + + def __post_init__(self): + """Validate required fields after initialization.""" + if self.broadcast_id <= 0: + raise ValueError("broadcast_id must be positive") + + if not self.title or not self.title.strip(): + raise ValueError("title is required and cannot be empty") + + if self.start <= 0: + raise ValueError("start time must be a valid Unix timestamp") + + if self.end <= self.start: + raise ValueError("end time must be after start time") + + +# Constants matching C++ EPG_TAG_FLAG values +class EPGFlags: + """Bit flags for EPG entry properties.""" + IS_SERIES = 0x01 + IS_NEW = 0x02 + IS_PREMIERE = 0x04 + IS_FINALE = 0x08 + IS_LIVE = 0x10 + + +# Genre type constants based on ETSI EN 300 468 V1.14.1 (DVB-SI EIT content descriptor) +# These match Kodi's EPG_EVENT_CONTENTMASK values +class EPGGenre: + """ + EPG genre type codes based on DVB-SI standard (ETSI EN 300 468). + These are the main content masks - use with genre_type field. + """ + # Main genre types (content masks) + UNDEFINED = 0x00 + MOVIEDRAMA = 0x10 + NEWSCURRENTAFFAIRS = 0x20 + SHOW = 0x30 + SPORTS = 0x40 + CHILDRENYOUTH = 0x50 + MUSICBALLETDANCE = 0x60 + ARTSCULTURE = 0x70 + SOCIALPOLITICALECONOMICS = 0x80 + EDUCATIONALSCIENCE = 0x90 + LEISUREHOBBIES = 0xA0 + SPECIAL = 0xB0 + USERDEFINED = 0xF0 + + # Special Kodi value to indicate genre is provided as string + USE_STRING = 0xF0 # Same as USERDEFINED, signals use of genre_description + + +class EPGGenreSubtype: + """ + EPG genre subtype codes based on DVB-SI standard (ETSI EN 300 468). + These are used with genre_sub_type field in combination with main genre_type. + """ + + # Movie/Drama subtypes (use with EPGGenre.MOVIEDRAMA) + class MovieDrama: + GENERAL = 0x00 + DETECTIVE_THRILLER = 0x01 + ADVENTURE_WESTERN_WAR = 0x02 + SCIENCEFICTION_FANTASY_HORROR = 0x03 + COMEDY = 0x04 + SOAP_MELODRAMA_FOLKLORIC = 0x05 + ROMANCE = 0x06 + SERIOUS_CLASSICAL_RELIGIOUS_HISTORICAL = 0x07 + ADULT = 0x08 + USERDEFINED = 0x0F + + # News/Current Affairs subtypes (use with EPGGenre.NEWSCURRENTAFFAIRS) + class NewsCurrentAffairs: + GENERAL = 0x00 + WEATHER = 0x01 + MAGAZINE = 0x02 + DOCUMENTARY = 0x03 + DISCUSSION_INTERVIEW_DEBATE = 0x04 + USERDEFINED = 0x0F + + # Show/Game Show subtypes (use with EPGGenre.SHOW) + class Show: + GENERAL = 0x00 + GAMESHOW_QUIZ_CONTEST = 0x01 + VARIETY_SHOW = 0x02 + TALK_SHOW = 0x03 + USERDEFINED = 0x0F + + # Sports subtypes (use with EPGGenre.SPORTS) + class Sports: + GENERAL = 0x00 + OLYMPICGAMES_WORLDCUP = 0x01 + SPORTS_MAGAZINES = 0x02 + FOOTBALL_SOCCER = 0x03 + TENNIS_SQUASH = 0x04 + TEAMSPORTS = 0x05 + ATHLETICS = 0x06 + MOTORSPORT = 0x07 + WATERSPORT = 0x08 + WINTERSPORTS = 0x09 + EQUESTRIAN = 0x0A + MARTIALSPORTS = 0x0B + USERDEFINED = 0x0F + + # Children/Youth subtypes (use with EPGGenre.CHILDRENYOUTH) + class ChildrenYouth: + GENERAL = 0x00 + PRESCHOOL_CHILDREN = 0x01 + ENTERTAIN_6TO14 = 0x02 + ENTERTAIN_10TO16 = 0x03 + INFORMATIONAL_EDUCATIONAL_SCHOOL = 0x04 + CARTOONS_PUPPETS = 0x05 + USERDEFINED = 0x0F + + # Music/Ballet/Dance subtypes (use with EPGGenre.MUSICBALLETDANCE) + class MusicBalletDance: + GENERAL = 0x00 + ROCKPOP = 0x01 + SERIOUSMUSIC_CLASSICALMUSIC = 0x02 + FOLK_TRADITIONAL_MUSIC = 0x03 + JAZZ = 0x04 + MUSICAL_OPERA = 0x05 + BALLET = 0x06 + USERDEFINED = 0x0F + + # Arts/Culture subtypes (use with EPGGenre.ARTSCULTURE) + class ArtsCulture: + GENERAL = 0x00 + PERFORMINGARTS = 0x01 + FINEARTS = 0x02 + RELIGION = 0x03 + POPULARCULTURE_TRADITIONALARTS = 0x04 + LITERATURE = 0x05 + FILM_CINEMA = 0x06 + EXPERIMENTALFILM_VIDEO = 0x07 + BROADCASTING_PRESS = 0x08 + NEWMEDIA = 0x09 + ARTS_CULTUREMAGAZINES = 0x0A + FASHION = 0x0B + USERDEFINED = 0x0F + + # Social/Political/Economics subtypes (use with EPGGenre.SOCIALPOLITICALECONOMICS) + class SocialPoliticalEconomics: + GENERAL = 0x00 + MAGAZINES_REPORTS_DOCUMENTARY = 0x01 + ECONOMICS_SOCIALADVISORY = 0x02 + REMARKABLEPEOPLE = 0x03 + USERDEFINED = 0x0F + + # Educational/Science subtypes (use with EPGGenre.EDUCATIONALSCIENCE) + class EducationalScience: + GENERAL = 0x00 + NATURE_ANIMALS_ENVIRONMENT = 0x01 + TECHNOLOGY_NATURALSCIENCES = 0x02 + MEDICINE_PHYSIOLOGY_PSYCHOLOGY = 0x03 + FOREIGNCOUNTRIES_EXPEDITIONS = 0x04 + SOCIAL_SPIRITUALSCIENCES = 0x05 + FURTHEREDUCATION = 0x06 + LANGUAGES = 0x07 + USERDEFINED = 0x0F + + # Leisure/Hobbies subtypes (use with EPGGenre.LEISUREHOBBIES) + class LeisureHobbies: + GENERAL = 0x00 + TOURISM_TRAVEL = 0x01 + HANDICRAFT = 0x02 + MOTORING = 0x03 + FITNESSANDHEALTH = 0x04 + COOKING = 0x05 + ADVERTISEMENT_SHOPPING = 0x06 + GARDENING = 0x07 + USERDEFINED = 0x0F + + # Special Characteristics subtypes (use with EPGGenre.SPECIAL) + class Special: + GENERAL = 0x00 + BLACKANDWHITE = 0x01 + UNPUBLISHED = 0x02 + LIVEBROADCAST = 0x03 + PLANOSTEREOSCOPIC = 0x04 + LOCALORREGIONAL = 0x05 + USERDEFINED = 0x0F + + +# Legacy alias for backwards compatibility with existing code +PVREPGTag = EPGEntry \ No newline at end of file