Implement epg

This commit is contained in:
Nirvana
2025-12-04 12:33:25 +01:00
parent 75c2f5d95c
commit fd8491bc39
2 changed files with 424 additions and 40 deletions
+142 -8
View File
@@ -16,6 +16,17 @@ class EPGMapping:
Manages mapping between provider channel IDs and EPG channel IDs.
Supports user-editable mapping with default fallback.
Mapping format (human-editable with channel names):
{
"provider_name": {
"_provider_name": "Display Name",
"channel_id": {
"epg_id": "tkmde_404",
"name": "Channel Display Name"
}
}
}
Uses VFS for all file operations. Automatically detects addon resources
path for default mapping (only used on first run to copy defaults).
"""
@@ -31,7 +42,12 @@ class EPGMapping:
# VFS for user data directory (where editable mapping lives)
self.user_vfs = VFS(addon_subdir="") # Root of addon data
# Store the raw mapping from file
self.raw_mapping: Dict = {}
# Store the flattened provider->channel->epg_id mapping for fast lookups
self.mapping: Dict[str, Dict[str, str]] = {}
# Store channel names for reference
self.channel_names: Dict[str, Dict[str, str]] = {}
logger.info(f"EPGMapping: User mapping path: {self.user_vfs.base_path}")
@@ -104,11 +120,23 @@ class EPGMapping:
logger.error(f"EPGMapping: Failed to read/copy default mapping: {e}")
# Fall through to create empty mapping
# No default found or copy failed - create empty mapping
# No default found or copy failed - create empty mapping with format info
logger.warning("EPGMapping: No default mapping found, creating empty mapping")
empty_mapping = {}
empty_mapping = {
"_format_version": "1.0",
"_description": "EPG Channel Mapping - Maps provider channel IDs to XMLTV EPG channel IDs",
"_example": {
"provider_key": {
"_provider_name": "Provider Display Name (optional)",
"channel_id": {
"epg_id": "xmltv_channel_id",
"name": "Channel Display Name"
}
}
}
}
if self.user_vfs.write_json(self.MAPPING_FILE, empty_mapping):
logger.info("EPGMapping: Created empty user mapping file")
logger.info("EPGMapping: Created empty user mapping file with examples")
return True
else:
logger.error("EPGMapping: Failed to create empty user mapping")
@@ -130,6 +158,85 @@ class EPGMapping:
logger.info("EPGMapping: User mapping not found, initializing...")
return self._copy_default_to_user()
@staticmethod
def _flatten_mapping(self, raw_mapping: Dict) -> tuple:
"""
Flatten mapping format to simple provider->channel->epg_id format for lookups.
Also extracts channel names for reference.
Input format:
{
"provider_name": {
"_provider_name": "Display Name",
"channel_id": {
"epg_id": "tkmde_404",
"name": "Channel Name"
}
}
}
Output format:
(
{ # mapping
"provider_name": {
"channel_id": "tkmde_404"
}
},
{ # channel_names
"provider_name": {
"channel_id": "Channel Name"
}
}
)
Args:
raw_mapping: Raw mapping from JSON file
Returns:
Tuple of (flattened mapping dict, channel names dict)
"""
flattened = {}
names = {}
for provider_name, provider_data in raw_mapping.items():
# Skip metadata entries (those starting with underscore)
if provider_name.startswith('_'):
continue
# Skip if not a dict (shouldn't happen, but be safe)
if not isinstance(provider_data, dict):
continue
# Initialize provider dicts if needed
if provider_name not in flattened:
flattened[provider_name] = {}
names[provider_name] = {}
# Process each channel in this provider
for channel_id, channel_data in provider_data.items():
# Skip metadata entries
if channel_id.startswith('_'):
continue
# Handle both dict format and simple string format
if isinstance(channel_data, dict):
# New format: { "epg_id": "...", "name": "..." }
epg_id = channel_data.get('epg_id')
name = channel_data.get('name', channel_id)
elif isinstance(channel_data, str):
# Legacy simple format: just the epg_id string
epg_id = channel_data
name = channel_id
else:
logger.warning(f"EPGMapping: Invalid format for {provider_name}/{channel_id}")
continue
if epg_id:
flattened[provider_name][channel_id] = epg_id
names[provider_name][channel_id] = name
return flattened, names
def _load_mapping(self) -> bool:
"""
Load mapping from user file.
@@ -141,15 +248,25 @@ class EPGMapping:
self._ensure_user_mapping_exists()
# Load user mapping
user_mapping = self.user_vfs.read_json(self.MAPPING_FILE)
if user_mapping is not None:
self.mapping = user_mapping
logger.info(f"EPGMapping: Loaded mapping with {len(self.mapping)} providers")
raw_mapping = self.user_vfs.read_json(self.MAPPING_FILE)
if raw_mapping is not None:
self.raw_mapping = raw_mapping
# Flatten the mapping for easy lookups
self.mapping, self.channel_names = self._flatten_mapping(raw_mapping)
total_channels = sum(len(channels) for channels in self.mapping.values())
logger.info(
f"EPGMapping: Loaded mapping with {len(self.mapping)} providers "
f"and {total_channels} channels"
)
return True
# Failed to load mapping
logger.warning("EPGMapping: Failed to load mapping, using empty mapping")
self.raw_mapping = {}
self.mapping = {}
self.channel_names = {}
return False
def get_epg_channel_id(self, provider_name: str, channel_id: str) -> Optional[str]:
@@ -161,7 +278,7 @@ class EPGMapping:
channel_id: Channel ID within provider
Returns:
EPG channel ID (e.g., "de.rtl"), or None if not mapped
EPG channel ID (e.g., "tkmde_404"), or None if not mapped
"""
# Get provider's mapping
provider_mapping = self.mapping.get(provider_name)
@@ -178,6 +295,23 @@ class EPGMapping:
logger.debug(f"EPGMapping: Mapped '{provider_name}/{channel_id}' -> '{epg_id}'")
return epg_id
def get_channel_name(self, provider_name: str, channel_id: str) -> Optional[str]:
"""
Get display name for a channel.
Args:
provider_name: Name of provider
channel_id: Channel ID within provider
Returns:
Channel display name, or None if not found
"""
provider_names = self.channel_names.get(provider_name)
if not provider_names:
return None
return provider_names.get(channel_id)
def get_provider_mapping(self, provider_name: str) -> Dict[str, str]:
"""
Get all channel mappings for a specific provider.
+282 -32
View File
@@ -1,42 +1,292 @@
{
"_comment": "EPG Channel Mapping - Maps provider/channel IDs to XMLTV EPG channel IDs",
"_format": "{ 'provider_name': { 'channel_id': 'epg_channel_id' } }",
"_note": "Provider names include country suffix for multi-country providers (e.g., joyn_de, joyn_ch)",
"_format_version": "1.0",
"_description": "EPG Channel Mapping - Maps provider channel IDs to XMLTV EPG channel IDs",
"rtlplus": {
"rtl": "de.rtl",
"vox": "de.vox",
"rtl2": "de.rtl2",
"nitro": "de.nitro",
"super_rtl": "de.superrtl"
"_provider_name": "RTL+",
"rtl": {
"epg_id": "tkmde_404",
"name": "RTL"
}
},
"joyn_de": {
"prosieben": "de.prosieben",
"sat1": "de.sat1",
"kabel_eins": "de.kabeleins",
"sixx": "de.sixx",
"pro7_maxx": "de.pro7maxx",
"sat1_gold": "de.sat1gold"
"_provider_name": "Joyn Germany",
"prosieben": {
"epg_id": "tkmde_374",
"name": "ProSieben"
},
"sat1": {
"epg_id": "tkmde_381",
"name": "SAT.1"
},
"kabel_eins": {
"epg_id": "tkmde_405",
"name": "Kabel Eins"
},
"sat1_gold": {
"epg_id": "tkmde_338",
"name": "SAT.1 GOLD"
},
"pro7_maxx": {
"epg_id": "tkmde_396",
"name": "ProSieben MAXX"
}
},
"joyn_ch": {
"prosieben_schweiz": "ch.prosieben",
"sat1_schweiz": "ch.sat1",
"kabel_eins_schweiz": "ch.kabeleins"
},
"zattoo_de": {
"ard": "de.ard",
"zdf": "de.zdf",
"rtl": "de.rtl",
"sat1": "de.sat1",
"prosieben": "de.prosieben"
},
"example_provider": {
"channel1": "epg.channel.one",
"channel2": "epg.channel.two",
"channel3": "epg.channel.three"
"magentaeu_at": {
"_provider_name": "Magenta TV Austria",
"206965288143": {
"epg_id": "tkmde_404",
"name": "RTL HD"
},
"206964776235": {
"epg_id": "tkmde_374",
"name": "ProSieben HD"
},
"206963752404": {
"epg_id": "tkmde_381",
"name": "SAT.1 HD"
},
"206965800213": {
"epg_id": "tkmde_405",
"name": "Kabel Eins HD"
},
"206963752441": {
"epg_id": "tkmde_338",
"name": "SAT.1 Gold HD"
},
"206966311964": {
"epg_id": "tkmde_396",
"name": "ProSieben Maxx HD"
},
"206963752417": {
"epg_id": "tkmde_382",
"name": "RTLZWEI HD"
},
"206966311961": {
"epg_id": "tkmde_542",
"name": "RTLup HD"
},
"207381032332": {
"epg_id": "tkmde_216",
"name": "RTL Crime HD"
},
"207381032331": {
"epg_id": "tkmde_227",
"name": "RTL Living HD"
},
"207382568087": {
"epg_id": "tkmde_209",
"name": "RTL Passion HD"
},
"207382568112": {
"epg_id": "tkmde_224",
"name": "SAT.1 emotions HD"
},
"206966824407": {
"epg_id": "tkmde_259",
"name": "ProSieben FUN HD"
},
"206965288240": {
"epg_id": "tkmde_573",
"name": "Kabel Eins Doku HD"
},
"207382568111": {
"epg_id": "tkmde_157",
"name": "kabel eins CLASSICS HD"
},
"206964776277": {
"epg_id": "tkmde_373",
"name": "Das Erste HD"
},
"206964776230": {
"epg_id": "tkmde_408",
"name": "ZDF HD"
},
"206966312080": {
"epg_id": "tkmde_378",
"name": "ZDFinfo HD"
},
"206965800291": {
"epg_id": "tkmde_391",
"name": "ZDFneo HD"
},
"206966312188": {
"epg_id": "tkmde_387",
"name": "KiKA HD"
},
"206965800302": {
"epg_id": "tkmde_402",
"name": "One HD"
},
"206966312077": {
"epg_id": "tkmde_371",
"name": "phoenix HD"
},
"207382568115": {
"epg_id": "tkmde_192",
"name": "Sky Sport 1"
},
"207382568116": {
"epg_id": "tkmde_212",
"name": "Sky Sport 2"
},
"207382568117": {
"epg_id": "tkmde_189",
"name": "Sky Sport 3"
},
"207382568118": {
"epg_id": "tkmde_111",
"name": "Sky Sport 4"
},
"207382568119": {
"epg_id": "tkmde_114",
"name": "Sky Sport 5"
},
"207382568108": {
"epg_id": "tkmde_113",
"name": "Sky Sport 6"
},
"207382568110": {
"epg_id": "tkmde_127",
"name": "Sky Sport 7"
},
"207382568113": {
"epg_id": "tkmde_261",
"name": "Sky Sport 8"
},
"207382568114": {
"epg_id": "tkmde_187",
"name": "Sky Sport 9"
},
"301712936310": {
"epg_id": "tkmde_5554",
"name": "Sky Sport 10"
},
"207382568125": {
"epg_id": "tkmde_42",
"name": "Sky Bundesliga"
},
"207382568030": {
"epg_id": "tkmde_206",
"name": "Sky Bundesliga 1"
},
"207381032273": {
"epg_id": "tkmde_211",
"name": "Sky Bundesliga 2"
},
"207382568028": {
"epg_id": "tkmde_199",
"name": "Sky Bundesliga 3"
},
"207381032275": {
"epg_id": "tkmde_232",
"name": "Sky Bundesliga 4"
},
"207381032276": {
"epg_id": "tkmde_234",
"name": "Sky Bundesliga 5"
},
"207381032277": {
"epg_id": "tkmde_231",
"name": "Sky Bundesliga 6"
},
"207381032278": {
"epg_id": "tkmde_258",
"name": "Sky Bundesliga 7"
},
"207381032279": {
"epg_id": "tkmde_219",
"name": "Sky Bundesliga 8"
},
"207381032274": {
"epg_id": "tkmde_237",
"name": "Sky Bundesliga 9"
},
"301713448071": {
"epg_id": "tkmde_5557",
"name": "Sky Bundesliga 10"
},
"207382568163": {
"epg_id": "tkmde_4889",
"name": "Sky Sport F1"
},
"301709352380": {
"epg_id": "tkmde_5558",
"name": "Sky Sport Golf"
},
"207381032347": {
"epg_id": "tkmde_5556",
"name": "Sky Sport Mix"
},
"206967848002": {
"epg_id": "tkmde_190",
"name": "Sky Sport News"
},
"301712936303": {
"epg_id": "tkmde_5555",
"name": "Sky Sport Premier League"
},
"207381032358": {
"epg_id": "tkmde_198",
"name": "Sky Sport Tennis"
},
"306844200112": {
"epg_id": "tkmde_5577",
"name": "Sky Showcase"
},
"207382568156": {
"epg_id": "tkmde_3756",
"name": "Sky Replay"
},
"207381032386": {
"epg_id": "tkmde_576",
"name": "Sky One"
},
"229429288190": {
"epg_id": "tkmde_5142",
"name": "Sky Nature"
},
"207381032342": {
"epg_id": "tkmde_201",
"name": "Sky Krimi HD"
},
"229429799984": {
"epg_id": "tkmde_5143",
"name": "Sky Documentaries"
},
"207382568161": {
"epg_id": "tkmde_4924",
"name": "Sky Crime"
},
"207381032340": {
"epg_id": "tkmde_195",
"name": "Sky Cinema Premiere"
},
"207381032388": {
"epg_id": "tkmde_6210",
"name": "Sky Cinema Highlights"
},
"207381032385": {
"epg_id": "tkmde_572",
"name": "Sky Cinema Family"
},
"207382568093": {
"epg_id": "tkmde_200",
"name": "Sky Cinema Classics"
},
"207381032334": {
"epg_id": "tkmde_40",
"name": "Sky Cinema Action"
},
"207381032335": {
"epg_id": "tkmde_264",
"name": "Sky Atlantic"
},
"207381032291": {
"epg_id": "tkmde_31",
"name": "Sky Sport Austria 1"
}
}
}