Implement epg

This commit is contained in:
Nirvana
2025-12-05 11:00:44 +01:00
parent addb9f9028
commit a8a511bd29
3 changed files with 491 additions and 497 deletions
+236 -205
View File
@@ -3,10 +3,15 @@
"""
EPG Channel Mapping Manager
Maps provider/channel IDs to EPG channel IDs from XMLTV
New provider-per-file format:
- Each provider has its own file: {provider}_epg_mapping.json
- File contains direct channel mappings without provider wrapper
"""
import os
from typing import Optional, Dict
import glob
from typing import List, Optional, Dict, Any
from ..utils.logger import logger
from ..utils.vfs import VFS
@@ -14,55 +19,48 @@ from ..utils.vfs import VFS
class EPGMapping:
"""
Manages mapping between provider channel IDs and EPG channel IDs.
Supports user-editable mapping with default fallback.
Uses separate JSON files per provider for better organization.
Mapping format (human-editable with channel names):
File format (per provider):
{
"provider_name": {
"_provider_name": "Display Name",
"channel_id": {
"epg_id": "tkmde_404",
"name": "Channel Display Name"
}
"_provider_name": "Display Name (optional)",
"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).
Fallback: If no mapping file exists or is corrupted, uses channel_id as epg_id.
"""
# Mapping filename
MAPPING_FILE = "epg_mapping.json"
# Mapping filename pattern
MAPPING_FILE_PATTERN = "*_epg_mapping.json"
MAPPING_FILE_SUFFIX = "_epg_mapping.json"
def __init__(self):
"""
Initialize EPG mapping manager.
Automatically detects environment and sets up paths.
Sets up VFS and copies default mapping files from addon resources.
"""
# VFS for user data directory (where editable mapping lives)
# VFS for user data directory
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]] = {}
# Cache for loaded provider mappings
# Structure: {provider_name: {"mapping": {...}, "names": {...}}}
self._cache: Dict[str, Dict[str, Any]] = {}
logger.info(f"EPGMapping: User mapping path: {self.user_vfs.base_path}")
logger.info(f"EPGMapping: Initialized with user path: {self.user_vfs.base_path}")
# Load mapping on initialization
self._load_mapping()
# Copy default mapping files from addon resources
self._copy_default_mapping_files()
def _get_default_mapping_path(self) -> Optional[str]:
@staticmethod
def _get_default_mapping_files_dir() -> Optional[str]:
"""
Get path to default mapping in addon resources.
Only used on first run to copy defaults to user directory.
Uses settings manager to detect paths if available.
Get path to default mapping files directory in addon resources.
Returns:
Path to default mapping file, or None if not found
Path to default mapping directory, or None if not found
"""
try:
# Try to use settings manager's kodi bridge to get addon path
@@ -73,148 +71,147 @@ class EPGMapping:
addon_info = bridge.get_addon_info()
addon_path = addon_info.get('path')
if addon_path:
default_path = os.path.join(addon_path, 'resources', 'config', self.MAPPING_FILE)
logger.debug(f"EPGMapping: Default mapping path (Kodi): {default_path}")
return default_path
default_dir = os.path.join(addon_path, 'resources', 'config', 'epg_mappings')
logger.debug(f"EPGMapping: Default mapping dir (Kodi): {default_dir}")
return default_dir
# Fallback to standard filesystem
addon_path = os.getcwd()
default_path = os.path.join(addon_path, 'resources', 'config', self.MAPPING_FILE)
logger.debug(f"EPGMapping: Default mapping path (standard): {default_path}")
return default_path
default_dir = os.path.join(addon_path, 'resources', 'config', 'epg_mappings')
logger.debug(f"EPGMapping: Default mapping dir (standard): {default_dir}")
return default_dir
except Exception as e:
logger.warning(f"EPGMapping: Could not determine default mapping path: {e}")
logger.warning(f"EPGMapping: Could not determine default mapping dir: {e}")
# Last resort fallback
addon_path = os.getcwd()
default_path = os.path.join(addon_path, 'resources', 'config', self.MAPPING_FILE)
return default_path
default_dir = os.path.join(addon_path, 'resources', 'config', 'epg_mappings')
return default_dir
def _copy_default_to_user(self) -> bool:
def _copy_default_mapping_files(self) -> bool:
"""
Copy default mapping from addon resources to user directory.
Only called on first run when user mapping doesn't exist.
Copy all default mapping files from addon resources to user directory.
Only called on first run.
Returns:
True if copied successfully or created empty mapping
True if at least one file was copied or user files already exist
"""
default_path = self._get_default_mapping_path()
default_dir = self._get_default_mapping_files_dir()
if default_path and os.path.exists(default_path):
# Read default mapping using standard file operations
# (it's in addon resources, not in VFS user data)
try:
import json
with open(default_path, 'r', encoding='utf-8') as f:
default_mapping = json.load(f)
if not default_dir or not os.path.exists(default_dir):
logger.warning("EPGMapping: No default mapping directory found")
# Check if any user mapping files already exist
user_files = self.user_vfs.list_files(pattern=self.MAPPING_FILE_PATTERN)
if user_files:
logger.info(f"EPGMapping: Found {len(user_files)} existing user mapping files")
return True
else:
logger.warning("EPGMapping: No default files and no user files found")
return False
# Write to user VFS
if self.user_vfs.write_json(self.MAPPING_FILE, default_mapping):
logger.info(f"EPGMapping: Copied default mapping to user directory")
return True
else:
logger.error(f"EPGMapping: Failed to write default mapping to user directory")
return False
try:
# Get list of default mapping files
default_files = glob.glob(os.path.join(default_dir, self.MAPPING_FILE_PATTERN))
except Exception as e:
logger.error(f"EPGMapping: Failed to read/copy default mapping: {e}")
# Fall through to create empty mapping
if not default_files:
logger.warning(f"EPGMapping: No default mapping files found in {default_dir}")
return False
# No default found or copy failed - create empty mapping with format info
logger.warning("EPGMapping: No default mapping found, creating 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 with examples")
return True
else:
logger.error("EPGMapping: Failed to create empty user mapping")
copied_count = 0
for default_file in default_files:
filename = os.path.basename(default_file)
# Skip if user file already exists
if self.user_vfs.exists(filename):
logger.debug(f"EPGMapping: User file already exists: {filename}")
continue
# Read default file using standard file operations
try:
with open(default_file, 'r', encoding='utf-8') as f:
content = f.read()
# Write to user VFS
if self.user_vfs.write_text(filename, content):
logger.info(f"EPGMapping: Copied default mapping: {filename}")
copied_count += 1
else:
logger.error(f"EPGMapping: Failed to write user mapping: {filename}")
except Exception as e:
logger.error(f"EPGMapping: Failed to read/copy {filename}: {e}")
continue
logger.info(f"EPGMapping: Copied {copied_count} default mapping files")
return copied_count > 0
except Exception as e:
logger.error(f"EPGMapping: Failed to copy default mapping files: {e}")
return False
def _ensure_user_mapping_exists(self) -> bool:
def _extract_provider_from_filename(self, filename: str) -> str:
"""
Ensure user mapping file exists. Copy from default if needed.
Returns:
True if user mapping exists or was created successfully
"""
# Check if user mapping already exists
if self.user_vfs.exists(self.MAPPING_FILE):
logger.debug("EPGMapping: User mapping file exists")
return True
# User mapping doesn't exist - copy from default or create empty
logger.info("EPGMapping: User mapping not found, initializing...")
return self._copy_default_to_user()
@staticmethod
def _flatten_mapping(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"
}
}
)
Extract provider name from mapping filename.
Args:
raw_mapping: Raw mapping from JSON file
filename: e.g., "magentaeu_at_epg_mapping.json"
Returns:
Tuple of (flattened mapping dict, channel names dict)
Provider name, e.g., "magentaeu_at"
"""
flattened = {}
names = {}
if filename.endswith(self.MAPPING_FILE_SUFFIX):
return filename[:-len(self.MAPPING_FILE_SUFFIX)]
return filename
for provider_name, provider_data in raw_mapping.items():
# Skip metadata entries (those starting with underscore)
if provider_name.startswith('_'):
continue
def _get_mapping_filename(self, provider_name: str) -> str:
"""
Get mapping filename for a provider.
# Skip if not a dict (shouldn't happen, but be safe)
if not isinstance(provider_data, dict):
continue
Args:
provider_name: Provider name
# Initialize provider dicts if needed
if provider_name not in flattened:
flattened[provider_name] = {}
names[provider_name] = {}
Returns:
Filename, e.g., "magentaeu_at_epg_mapping.json"
"""
return f"{provider_name}{self.MAPPING_FILE_SUFFIX}"
# Process each channel in this provider
for channel_id, channel_data in provider_data.items():
# Skip metadata entries
def _load_provider_mapping(self, provider_name: str) -> Optional[Dict[str, Any]]:
"""
Load mapping for a specific provider from file.
Uses cache if already loaded.
Args:
provider_name: Provider name to load mapping for
Returns:
Dictionary with "mapping" and "names", or None if failed
"""
# Check cache first
if provider_name in self._cache:
logger.debug(f"EPGMapping: Using cached mapping for '{provider_name}'")
return self._cache[provider_name]
filename = self._get_mapping_filename(provider_name)
# Check if file exists
if not self.user_vfs.exists(filename):
logger.debug(f"EPGMapping: No mapping file found for provider '{provider_name}'")
return None
try:
# Load mapping from file
raw_mapping = self.user_vfs.read_json(filename)
if raw_mapping is None:
logger.warning(f"EPGMapping: Failed to parse mapping file for '{provider_name}'")
return None
# Flatten the mapping
mapping = {}
names = {}
for channel_id, channel_data in raw_mapping.items():
# Skip metadata entries (those starting with underscore)
if channel_id.startswith('_'):
continue
@@ -232,68 +229,64 @@ class EPGMapping:
continue
if epg_id:
flattened[provider_name][channel_id] = epg_id
names[provider_name][channel_id] = name
mapping[channel_id] = epg_id
names[channel_id] = name
return flattened, names
# Store in cache
cached_data = {
"mapping": mapping,
"names": names,
"provider_name": raw_mapping.get('_provider_name', provider_name)
}
self._cache[provider_name] = cached_data
def _load_mapping(self) -> bool:
"""
Load mapping from user file.
Returns:
True if mapping loaded successfully
"""
# Ensure user mapping exists (copy from default on first run)
self._ensure_user_mapping_exists()
# Load user mapping
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"
f"EPGMapping: Loaded mapping for '{provider_name}' "
f"with {len(mapping)} 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
return cached_data
except Exception as e:
logger.error(f"EPGMapping: Failed to load mapping for '{provider_name}': {e}")
return None
def get_epg_channel_id(self, provider_name: str, channel_id: str) -> Optional[str]:
"""
Get EPG channel ID for a provider/channel combination.
Falls back to channel_id if no mapping found.
Args:
provider_name: Name of provider (e.g., "rtlplus", "joyn_de")
channel_id: Channel ID within provider
Returns:
EPG channel ID (e.g., "tkmde_404"), or None if not mapped
EPG channel ID, or channel_id if no mapping found
"""
# Get provider's mapping
provider_mapping = self.mapping.get(provider_name)
if not provider_mapping:
logger.debug(f"EPGMapping: No mapping found for provider '{provider_name}'")
return None
# Load provider mapping
provider_data = self._load_provider_mapping(provider_name)
if not provider_data:
# No mapping file exists - fall back to using channel_id as epg_id
logger.debug(
f"EPGMapping: No mapping file for '{provider_name}', "
f"using channel_id as epg_id: '{channel_id}'"
)
return channel_id
# Get channel's EPG ID
epg_id = provider_mapping.get(channel_id)
if not epg_id:
logger.debug(f"EPGMapping: No EPG ID found for '{provider_name}/{channel_id}'")
return None
epg_id = provider_data["mapping"].get(channel_id)
logger.debug(f"EPGMapping: Mapped '{provider_name}/{channel_id}' -> '{epg_id}'")
return epg_id
if epg_id:
logger.debug(f"EPGMapping: Mapped '{provider_name}/{channel_id}' -> '{epg_id}'")
return epg_id
else:
# Channel not in mapping - fall back to channel_id
logger.debug(
f"EPGMapping: No mapping for '{provider_name}/{channel_id}', "
f"using channel_id as epg_id"
)
return channel_id
def get_channel_name(self, provider_name: str, channel_id: str) -> Optional[str]:
"""
@@ -306,11 +299,12 @@ class EPGMapping:
Returns:
Channel display name, or None if not found
"""
provider_names = self.channel_names.get(provider_name)
if not provider_names:
provider_data = self._load_provider_mapping(provider_name)
if not provider_data:
return None
return provider_names.get(channel_id)
return provider_data["names"].get(channel_id)
def get_provider_mapping(self, provider_name: str) -> Dict[str, str]:
"""
@@ -322,7 +316,12 @@ class EPGMapping:
Returns:
Dictionary mapping channel IDs to EPG IDs
"""
return self.mapping.get(provider_name, {})
provider_data = self._load_provider_mapping(provider_name)
if not provider_data:
return {}
return provider_data["mapping"].copy()
def has_mapping(self, provider_name: str, channel_id: Optional[str] = None) -> bool:
"""
@@ -335,23 +334,35 @@ class EPGMapping:
Returns:
True if mapping exists
"""
if provider_name not in self.mapping:
provider_data = self._load_provider_mapping(provider_name)
if not provider_data:
return False
if channel_id is None:
return True
return channel_id in self.mapping[provider_name]
return channel_id in provider_data["mapping"]
def reload_mapping(self) -> bool:
def reload_mapping(self, provider_name: Optional[str] = None) -> bool:
"""
Reload mapping from file (useful after user edits).
Reload mapping from file. If provider_name is None, reload all.
Args:
provider_name: Provider name to reload, or None for all
Returns:
True if reloaded successfully
"""
logger.info("EPGMapping: Reloading mapping from file")
return self._load_mapping()
if provider_name is None:
logger.info("EPGMapping: Reloading all mappings from files")
self._cache.clear()
return True
else:
logger.info(f"EPGMapping: Reloading mapping for '{provider_name}'")
if provider_name in self._cache:
del self._cache[provider_name]
return self._load_provider_mapping(provider_name) is not None
def get_mapping_stats(self) -> Dict:
"""
@@ -360,15 +371,35 @@ class EPGMapping:
Returns:
Dictionary with mapping statistics
"""
total_providers = len(self.mapping)
total_channels = sum(len(channels) for channels in self.mapping.values())
# List all provider mapping files in user directory
user_files = self.user_vfs.list_files(pattern=self.MAPPING_FILE_PATTERN)
user_mapping_path = self.user_vfs.join_path(self.MAPPING_FILE)
# Extract provider names from filenames
providers = [self._extract_provider_from_filename(f) for f in user_files]
# Get cached providers
cached_providers = list(self._cache.keys())
total_channels = sum(
len(data["mapping"])
for data in self._cache.values()
)
return {
'total_providers': total_providers,
'total_providers': len(providers),
'cached_providers': len(cached_providers),
'total_channels': total_channels,
'providers': list(self.mapping.keys()),
'user_mapping_path': user_mapping_path,
'user_mapping_exists': self.user_vfs.exists(self.MAPPING_FILE)
}
'providers': providers,
'cached_providers_list': cached_providers,
'user_mapping_dir': self.user_vfs.base_path
}
def list_providers_with_mappings(self) -> List[str]:
"""
List all providers that have mapping files.
Returns:
List of provider names
"""
user_files = self.user_vfs.list_files(pattern=self.MAPPING_FILE_PATTERN)
return [self._extract_provider_from_filename(f) for f in user_files]
-292
View File
@@ -1,292 +0,0 @@
{
"_format_version": "1.0",
"_description": "EPG Channel Mapping - Maps provider channel IDs to XMLTV EPG channel IDs",
"rtlplus": {
"_provider_name": "RTL+",
"rtl": {
"epg_id": "tkmde_404",
"name": "RTL"
}
},
"joyn_de": {
"_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"
}
},
"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"
}
}
}
@@ -0,0 +1,255 @@
{
"_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"
}
}