Skip to content

Commit

Permalink
much richer epg handling (on tvheadend 4.3 this requires digest auth …
Browse files Browse the repository at this point in the history
…to be disabled)
  • Loading branch information
chkuendig committed Apr 19, 2020
1 parent a455014 commit 3ba13de
Showing 1 changed file with 140 additions and 25 deletions.
165 changes: 140 additions & 25 deletions tvhProxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
logger = logging.getLogger()

host_name = socket.gethostname()
host_ip = socket.gethostbyname(host_name)
host_ip = socket.gethostbyname(host_name)

# URL format: <protocol>://<username>:<password>@<hostname>:<port>, example: https://test:1234@localhost:9981
config = {
Expand All @@ -32,10 +32,10 @@
'tvhProxyURL': os.environ.get('TVH_PROXY_URL'), # only used if set (in case of forward-proxy), otherwise assembled from host + port bel
'tvhProxyHost': os.environ.get('TVH_PROXY_HOST') or host_ip,
'tvhProxyPort': os.environ.get('TVH_PROXY_PORT') or 5004,
'tunerCount': os.environ.get('TVH_TUNER_COUNT') or 6, # number of tuners in tvh
'tvhWeight': os.environ.get('TVH_WEIGHT') or 300, # subscription priority
'chunkSize': os.environ.get('TVH_CHUNK_SIZE') or 1024*1024, # usually you don't need to edit this
'streamProfile': os.environ.get('TVH_PROFILE') or 'pass' # specifiy a stream profile that you want to use for adhoc transcoding in tvh, e.g. mp4
'tunerCount': os.environ.get('TVH_TUNER_COUNT') or 6, # number of tuners in tvh
'tvhWeight': os.environ.get('TVH_WEIGHT') or 300, # subscription priority
'chunkSize': os.environ.get('TVH_CHUNK_SIZE') or 1024*1024, # usually you don't need to edit this
'streamProfile': os.environ.get('TVH_PROFILE') or 'pass' # specifiy a stream profile that you want to use for adhoc transcoding in tvh, e.g. mp4
}

discoverData = {
Expand All @@ -45,7 +45,7 @@
'FirmwareName': 'hdhomeruntc_atsc',
'TunerCount': int(config['tunerCount']),
'FirmwareVersion': '20150826',
'DeviceID': config['deviceID'],
'DeviceID': config['deviceID'],
'DeviceAuth': 'test1234',
'BaseURL': '%s' % (config['tvhProxyURL'] or "http://" + config['tvhProxyHost'] + ":" + str(config['tvhProxyPort'])),
'LineupURL': '%s/lineup.json' % (config['tvhProxyURL'] or "http://" + config['tvhProxyHost'] + ":" + str(config['tvhProxyPort']))
Expand All @@ -72,7 +72,8 @@ def lineup():

for c in _get_channels():
if c['enabled']:
url = '%s/stream/channel/%s?profile=%s&weight=%s' % (config['tvhURL'], c['uuid'], config['streamProfile'],int(config['tvhWeight']))
url = '%s/stream/channel/%s?profile=%s&weight=%s' % (
config['tvhURL'], c['uuid'], config['streamProfile'], int(config['tvhWeight']))

lineup.append({'GuideNumber': str(c['number']),
'GuideName': c['name'],
Expand All @@ -99,26 +100,87 @@ def epg():


def _get_channels():
url = '%s/api/channel/grid?start=0&limit=999999' % config['tvhURL']

url = '%s/api/channel/grid' % config['tvhURL']
params = {
'limit': 999999,
'start': 0
}
try:
r = requests.get(url)
r = requests.get(url, params)
return r.json()['entries']

except Exception as e:
logger.error('An error occured: %s' + repr(e))


def _get_genres():
def _findMainCategory(majorCategories, minorCategory):
prevKey, currentKey = None, None
for currentKey in sorted(majorCategories.keys()):
if(currentKey > minorCategory):
return majorCategories[prevKey]
prevKey = currentKey
return majorCategories[prevKey]
url = '%s/api/epg/content_type/list' % config['tvhURL']
params = {'full': 1}
try:
r = requests.get(url)
entries = r.json()['entries']
r = requests.get(url, params)
entries_full = r.json()['entries']
majorCategories = {}
genres = {}
for entry in entries:
majorCategories[entry['key']] = entry['val']
for entry in entries_full:
if not entry['key'] in majorCategories:
mainCategory = _findMainCategory(majorCategories, entry['key'])
if(mainCategory != entry['val']):
genres[entry['key']] = [mainCategory, entry['val']]
else:
genres[entry['key']] = [entry['val']]
else:
genres[entry['key']] = [entry['val']]
return genres
except Exception as e:
logger.error('An error occured: %s' + repr(e))


def _get_xmltv():
url = '%s/xmltv/channels' % config['tvhURL']
logger.info('downloading xmltv from %s', url)
try:
url = '%s/xmltv/channels' % config['tvhURL']
r = requests.get(url)
logger.info('downloading xmltv from %s', r.url)
tree = ElementTree.ElementTree(
ElementTree.fromstring(requests.get(url).content))
root = tree.getroot()
url = '%s/api/epg/events/grid' % config['tvhURL']
params = {
'limit': 999999,
'filter': json.dumps([
{
"field": "start",
"type": "numeric",
"value": int(round(datetime.timestamp(datetime.now() + timedelta(hours=48)))),
"comparison": "lt"
}
])
}
r = requests.get(url, params)
logger.info('downloading epg grid from %s', r.url)
epg_events_grid = r.json()['entries']
epg_events = {}
event_keys = {}
for epg_event in epg_events_grid:
if epg_event['channelUuid'] not in epg_events:
epg_events[epg_event['channelUuid']] = {}
epg_events[epg_event['channelUuid']
][epg_event['start']] = epg_event
for key in epg_event.keys():
event_keys[key] = True
channelNumberMapping = {}
channelsInEPG = {}
genres = _get_genres()
for child in root:
if child.tag == 'channel':
channelId = child.attrib['id']
Expand All @@ -128,12 +190,64 @@ def _get_xmltv():
logger.error("duplicate channelNo: %s", channelNo)
channelsInEPG[channelNo] = False
child.remove(child[1])
# FIXME: properly rewrite with TVH_URL or even proxy
child[1].attrib['src'] = child[1].attrib['src']+".png"
# check if icon exists (tvh always returns an URL even if there is no channel icon)
for icon in child.iter('icon'):
iconUrl = icon.attrib['src']+".png"
r = requests.head(iconUrl)
if r.status_code == requests.codes.ok:
child[1].attrib['src'] = iconUrl
else:
child.remove(icon)

child.attrib['id'] = channelNo
if child.tag == 'programme':
child.attrib['channel'] = channelNumberMapping[child.attrib['channel']]
channelsInEPG[child.attrib['channel']] = True
channelUuid = child.attrib['channel']
channelNumber = channelNumberMapping[channelUuid]
channelsInEPG[channelNumber] = True
child.attrib['channel'] = channelNumber
start_datetime = datetime.strptime(
child.attrib['start'], "%Y%m%d%H%M%S %z").replace(tzinfo=None)
stop_datetime = datetime.strptime(
child.attrib['stop'], "%Y%m%d%H%M%S %z").replace(tzinfo=None)
if datetime.now() < start_datetime - timedelta(hours=48):
# Plex doesn't like extremely large XML files, we'll remove the details from entries more than 48h in the future
for desc in child.iter('desc'):
child.remove(desc)
elif stop_datetime > datetime.now():
# add extra details for programs in the next 48hs
start_timestamp = int(
round(datetime.timestamp(start_datetime)))
epg_event = epg_events[channelUuid][start_timestamp]
if ('image' in epg_event):
programmeImage = ElementTree.SubElement(child, 'icon')
imageUrl = str(epg_event['image'])
if(imageUrl.startswith('imagecache')):
imageUrl = config['tvhURL'] + \
"/" + imageUrl + ".png"
programmeImage.attrib['src'] = imageUrl
if ('genre' in epg_event):
for genreId in epg_event['genre']:
for category in genres[genreId]:
programmeCategory = ElementTree.SubElement(
child, 'category')
programmeCategory.text = category
if ('episodeOnscreen' in epg_event):
episodeNum = ElementTree.SubElement(
child, 'episode-num')
episodeNum.attrib['system'] = 'onscreen'
episodeNum.text = epg_event['episodeOnscreen']
if('hd' in epg_event):
video = ElementTree.SubElement(child, 'video')
quality = ElementTree.SubElement(video, 'quality')
quality.text = "HDTV"
if('new' in epg_event):
ElementTree.SubElement(child, 'new')
else:
ElementTree.SubElement(child, 'previously-shown')
if('copyright_year' in epg_event):
date = ElementTree.SubElement(child, 'date')
date.text = str(epg_event['copyright_year'])
del epg_events[channelUuid][start_timestamp]
for key in sorted(channelsInEPG):
if channelsInEPG[key]:
logger.debug("Programmes found for channel %s", key)
Expand All @@ -142,28 +256,29 @@ def _get_xmltv():
'channel[@id="'+key+'"]/display-name').text
logger.error("No programme for channel %s: %s",
key, channelName)

# create 2h programmes for 72 hours
yesterday_midnight = datetime.combine(datetime.today(), time.min) - timedelta(days=1)
yesterday_midnight = datetime.combine(
datetime.today(), time.min) - timedelta(days=1)
date_format = '%Y%m%d%H%M%S'

for x in range(0, 36):
dummyProgramme = ElementTree.SubElement(root, 'programme')
dummyProgramme.attrib['channel'] = str(key)
dummyProgramme.attrib['start'] = (yesterday_midnight + timedelta(hours=x*2)).strftime(date_format)
dummyProgramme.attrib['stop'] = (yesterday_midnight + timedelta(hours=(x*2)+2)).strftime(date_format)
dummyTitle = ElementTree.SubElement(dummyProgramme, 'title')
dummyProgramme.attrib['start'] = (
yesterday_midnight + timedelta(hours=x*2)).strftime(date_format)
dummyProgramme.attrib['stop'] = (
yesterday_midnight + timedelta(hours=(x*2)+2)).strftime(date_format)
dummyTitle = ElementTree.SubElement(
dummyProgramme, 'title')
dummyTitle.attrib['lang'] = 'eng'
dummyTitle.text = channelName
dummyDesc = ElementTree.SubElement(dummyProgramme, 'desc')
dummyDesc.attrib['lang'] = 'eng'
dummyDesc.text = "No programming information"

logger.info("returning epg")
return ElementTree.tostring(root)
except requests.exceptions.RequestException as e: # This is the correct syntax
logger.error('An error occured: %s' + repr(e))


def _start_ssdp():
ssdp = SSDPServer()
Expand All @@ -179,7 +294,7 @@ def _start_ssdp():


if __name__ == '__main__':
http = WSGIServer((config['bindAddr'], config['tvhProxyPort']),
http = WSGIServer((config['bindAddr'], int(config['tvhProxyPort'])),
app.wsgi_app, log=logger, error_log=logger)
_start_ssdp()
http.serve_forever()

0 comments on commit 3ba13de

Please sign in to comment.