1
0
mirror of https://github.com/fHDHR/fHDHR_NextPVR.git synced 2025-12-06 07:46:58 -05:00

Better Loading process and Code Cleanup

This commit is contained in:
deathbybandaid 2020-10-07 09:43:55 -04:00
parent eeb13ea977
commit a831a07a59
28 changed files with 1085 additions and 979 deletions

View File

@ -9,4 +9,4 @@ COPY ./ /app/
WORKDIR /app WORKDIR /app
RUN pip3 install -r requirements.txt RUN pip3 install -r requirements.txt
ENTRYPOINT ["python3", "/app/main.py", "--config_file", "/app/config/config.ini"] ENTRYPOINT ["python3", "/app/main.py", "--c", "/app/config/config.ini"]

View File

@ -1,11 +1,12 @@
# FakeHDHR_NextPVR # fHDHR_NextPVR
Welcome to the world of streaming to Plex! We use some fancy python here to achieve a system of: Welcome to the world of streaming to Plex! We use some fancy python here to achieve a system of:
**f**un
**H**ome **H**ome
**D**istribution **D**istribution
**H**iatus **H**iatus &
**R**ecreation **R**ecreation

View File

@ -13,14 +13,14 @@
# weight = 300 # weight = 300
# epg_update_frequency = 43200 # epg_update_frequency = 43200
[fakehdhr] [fhdhr]
# address = 0.0.0.0 # address = 0.0.0.0
# port = 5004 # port = 5004
# discovery_address = 0.0.0.0 # discovery_address = 0.0.0.0
# tuner_count = 3 # tuner_count = 3
# friendlyname = fHDHR-Locast # friendlyname = fHDHR-Locast
# stream_type = ffmpeg # stream_type = ffmpeg
# epg_method = proxy # epg_method = origin
[zap2it] [zap2it]
# delay = 5 # delay = 5

View File

@ -1,9 +1,9 @@
[nextpvr] [nextpvr]
address = localhost address = localhost
port = 8866 port = 8866
pin = pin =
[fakehdhr] [fhdhr]
# address = 0.0.0.0 # address = 0.0.0.0
# port = 5004 # port = 5004
# discovery_address = 0.0.0.0 # discovery_address = 0.0.0.0

View File

@ -1,11 +1,11 @@
[main] [main]
uuid = fHDHR_None uuid =
cache_dir = fHDHR_None cache_dir =
[empty] [blocks]
epg_update_frequency = 43200 epg_update_frequency = 43200
[fakehdhr] [fhdhr]
address = 0.0.0.0 address = 0.0.0.0
port = 5004 port = 5004
discovery_address = 0.0.0.0 discovery_address = 0.0.0.0
@ -18,7 +18,7 @@ bytes_per_read = 1152000
chunksize = 1048576 chunksize = 1048576
[dev] [dev]
reporting_model = HDHR4-2DT reporting_manufacturer = BoronDust
reporting_firmware_name = hdhomerun4_dvbt reporting_model = fHDHR
reporting_firmware_ver = 20150826 reporting_firmware_ver = 20201001
reporting_tuner_type = Antenna reporting_tuner_type = Antenna

View File

@ -1,27 +1,31 @@
[dev]
reporting_firmware_name = fHDHR_NextPVR
[main] [main]
servicename = NextPVR servicename = NextPVR
dictpopname = nextpvr dictpopname = nextpvr
credentials = pin reponame = fHDHR_NextPVR
reponame = FakeHDHR_NextPVR required = nextpvr/pin
valid_epg_methods = None,blocks,origin,zap2it
[fakehdhr] [fhdhr]
friendlyname = fHDHR-NextPVR friendlyname = fHDHR-NextPVR
stream_type = direct stream_type = direct
epg_method = proxy epg_method = origin
tuner_count = 4 tuner_count = 4
[nextpvr] [nextpvr]
address = localhost address = localhost
port = 8866 port = 8866
ssl = fHDHR_False ssl = False
pin = fHDHR_None pin =
weight = 300 weight = 300
sidfile = fHDHR_None
epg_update_frequency = 43200 epg_update_frequency = 43200
sid =
[zap2it] [zap2it]
delay = 5 delay = 5
postalcode = fHDHR_None postalcode =
affiliate_id = gapzap affiliate_id = gapzap
country = USA country = USA
device = - device = -

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,139 +0,0 @@
import os
import sys
import time
from io import BytesIO
import xml.etree.ElementTree as ET
from . import epgtypes
def sub_el(parent, name, text=None, **kwargs):
el = ET.SubElement(parent, name, **kwargs)
if text:
el.text = text
return el
def clean_exit():
sys.stderr.flush()
sys.stdout.flush()
os._exit(0)
class EPGhandler():
def __init__(self, config, serviceproxy):
self.config = config.copy()
self.epgtypes = epgtypes.EPGTypes(config, serviceproxy)
def get_xmltv(self, base_url):
epgdict = self.epgtypes.get_epg()
if not epgdict:
return self.dummyxml()
epg_method = self.config["fakehdhr"]["epg_method"]
out = ET.Element('tv')
out.set('source-info-url', self.config["fakehdhr"]["friendlyname"])
out.set('source-info-name', self.config["main"]["servicename"])
out.set('generator-info-name', 'FAKEHDHR')
out.set('generator-info-url', 'FAKEHDHR/' + self.config["main"]["reponame"])
for c in list(epgdict.keys()):
c_out = sub_el(out, 'channel', id=str(epgdict[c]['number']))
sub_el(c_out, 'display-name',
text='%s %s' % (epgdict[c]['number'], epgdict[c]['callsign']))
sub_el(c_out, 'display-name',
text='%s %s %s' % (epgdict[c]['number'], epgdict[c]['callsign'], str(epgdict[c]['id'])))
sub_el(c_out, 'display-name', text=epgdict[c]['number'])
sub_el(c_out, 'display-name',
text='%s %s fcc' % (epgdict[c]['number'], epgdict[c]['callsign']))
sub_el(c_out, 'display-name', text=epgdict[c]['callsign'])
sub_el(c_out, 'display-name', text=epgdict[c]['callsign'])
sub_el(c_out, 'display-name', text=epgdict[c]['name'])
if epgdict[c]["thumbnail"] is not None:
channel_thumbnail = self.epgtypes.thumb_url(epg_method, "channel", base_url, str(epgdict[c]['thumbnail']))
sub_el(c_out, 'icon', src=(str(channel_thumbnail)))
else:
sub_el(c_out, 'icon', src=("http://" + str(base_url) + "/images?source=empty&type=channel&id=" + c['number']))
for progitem in list(epgdict.keys()):
channel_listing = epgdict[progitem]['listing']
for program in channel_listing:
prog_out = sub_el(out, 'programme',
start=program['time_start'],
stop=program['time_end'],
channel=str(progitem))
sub_el(prog_out, 'title', lang='en', text=program['title'])
sub_el(prog_out, 'desc', lang='en', text=program['description'])
sub_el(prog_out, 'sub-title', lang='en', text='Movie: ' + program['sub-title'])
sub_el(prog_out, 'length', units='minutes', text=str(int(program['duration_minutes'])))
for f in program['genres']:
sub_el(prog_out, 'category', lang='en', text=f)
sub_el(prog_out, 'genre', lang='en', text=f)
if program['seasonnumber'] and program['episodenumber']:
s_ = int(str(program['seasonnumber']), 10)
e_ = int(str(program['episodenumber']), 10)
sub_el(prog_out, 'episode-num', system='dd_progid',
text=str(program['id']))
sub_el(prog_out, 'episode-num', system='common',
text='S%02dE%02d' % (s_, e_))
sub_el(prog_out, 'episode-num', system='xmltv_ns',
text='%d.%d.' % (int(s_)-1, int(e_)-1))
sub_el(prog_out, 'episode-num', system='SxxExx">S',
text='S%02dE%02d' % (s_, e_))
if program["thumbnail"] is not None:
content_thumbnail = self.epgtypes.thumb_url(epg_method, "content", base_url, str(epgdict[c]['thumbnail']))
sub_el(prog_out, 'icon', src=(str(content_thumbnail)))
else:
sub_el(prog_out, 'icon', src=("http://" + str(base_url) + "/images?source=empty&type=content&id=" + program['title']))
if program['rating']:
rating_out = sub_el(prog_out, 'rating', system="MPAA")
sub_el(rating_out, 'value', text=program['rating'])
if program['isnew']:
sub_el(prog_out, 'new')
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(ET.tostring(out, encoding='UTF-8'))
return fakefile.getvalue()
def dummyxml(self):
out = ET.Element('tv')
out.set('source-info-url', self.config["fakehdhr"]["friendlyname"])
out.set('source-info-name', self.config["main"]["servicename"])
out.set('generator-info-name', 'FAKEHDHR')
out.set('generator-info-url', 'FAKEHDHR/' + self.config["main"]["reponame"])
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(ET.tostring(out, encoding='UTF-8'))
return fakefile.getvalue()
def epgServerProcess(config, epghandling):
sleeptime = int(config[config["fakehdhr"]["epg_method"]]["epg_update_frequency"])
try:
while True:
epghandling.epgtypes.update()
time.sleep(sleeptime)
except KeyboardInterrupt:
clean_exit()

View File

@ -1,28 +0,0 @@
from . import zap2it
from . import empty
class EPGTypes():
def __init__(self, config, serviceproxy):
self.config = config.copy()
self.proxy = serviceproxy
self.zap2it = zap2it.ZapEPG(config, serviceproxy)
self.empty = empty.EmptyEPG(config, serviceproxy)
def get_epg(self):
method_to_call = getattr(self, self.config["fakehdhr"]["epg_method"])
func_to_call = getattr(method_to_call, 'epg_cache_open')
epgdict = func_to_call()
return epgdict
def thumb_url(self, epg_method, thumb_type, base_url, thumbnail):
method_to_call = getattr(self, self.config["fakehdhr"]["epg_method"])
func_to_call = getattr(method_to_call, 'thumb_url')
thumbnail = func_to_call(thumb_type, base_url, thumbnail)
return thumbnail
def update(self):
method_to_call = getattr(self, self.config["fakehdhr"]["epg_method"])
func_to_call = getattr(method_to_call, 'update_epg')
func_to_call()

2
fHDHR/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# coding=utf-8
fHDHR_VERSION = "v0.2.0-beta"

0
fHDHR/cli/__init__.py Normal file
View File

96
fHDHR/cli/run.py Normal file
View File

@ -0,0 +1,96 @@
import os
import sys
import time
import argparse
from multiprocessing import Process
from fHDHR import fHDHR_VERSION, config, originservice, ssdpserver, epghandler, fHDHRerrors, fHDHRweb
ERR_CODE = 1
ERR_CODE_NO_RESTART = 2
if sys.version_info.major == 2 or sys.version_info < (3, 3):
print('Error: fHDHR requires python 3.3+.')
sys.exit(1)
def build_args_parser():
"""Build argument parser for fHDHR"""
print("Validating CLI Argument")
parser = argparse.ArgumentParser(description='fHDHR')
parser.add_argument('-c', '--config', dest='cfg', type=str, required=True, help='configuration file to load.')
return parser.parse_args()
def get_configuration(args, script_dir):
if not os.path.isfile(args.cfg):
raise config.ConfigurationNotFound(filename=args.cfg)
return config.Config(args.cfg, script_dir)
def get_originservice(settings):
return originservice.OriginService(settings)
def run(settings, origserv, epghandling):
if settings.dict["fhdhr"]["discovery_address"]:
ssdpServer = Process(target=ssdpserver.ssdpServerProcess, args=(settings,))
ssdpServer.start()
if settings.dict["fhdhr"]["epg_method"]:
epgServer = Process(target=epghandler.epgServerProcess, args=(settings, epghandling))
epgServer.start()
fhdhrweb = Process(target=fHDHRweb.interface_start, args=(settings, origserv, epghandling))
fhdhrweb.start()
print(settings.dict["fhdhr"]["friendlyname"] + " is now running!")
# wait forever
while True:
time.sleep(3600)
return ERR_CODE
def start(args, script_dir):
"""Get Configuration for fHDHR and start"""
try:
settings = get_configuration(args, script_dir)
except fHDHRerrors.ConfigurationError as e:
print(e)
return ERR_CODE_NO_RESTART
try:
origserv = get_originservice(settings)
except fHDHRerrors.LoginError as e:
print(e)
return ERR_CODE_NO_RESTART
try:
epghandling = epghandler.EPGhandler(settings, origserv)
except fHDHRerrors.EPGSetupError as e:
print(e)
return ERR_CODE_NO_RESTART
return run(settings, origserv, epghandling)
def main(script_dir):
"""fHDHR run script entry point"""
print("Loading fHDHR " + fHDHR_VERSION)
try:
args = build_args_parser()
return start(args, script_dir)
except KeyboardInterrupt:
print("\n\nInterrupted")
return ERR_CODE
if __name__ == '__main__':
main()

158
fHDHR/config/__init__.py Normal file
View File

@ -0,0 +1,158 @@
import os
import random
import configparser
import pathlib
from fHDHR import fHDHRerrors
from fHDHR.tools import isint, isfloat, is_arithmetic
class Config():
def __init__(self, filename, script_dir):
self.dict = {}
self.config_file = filename
self.parser = configparser.RawConfigParser(allow_no_value=True)
self.load_defaults(script_dir)
print("Loading Configuration File: " + str(self.config_file))
self.read_config(self.config_file)
print("Verifying Configuration settings.")
self.config_verification()
print("Server is set to run on " +
str(self.dict["fhdhr"]["address"]) + ":" +
str(self.dict["fhdhr"]["port"]))
def load_defaults(self, script_dir):
data_dir = pathlib.Path(script_dir).joinpath('data')
www_dir = pathlib.Path(data_dir).joinpath('www')
self.dict["filedir"] = {
"script_dir": script_dir,
"data_dir": data_dir,
"cache_dir": pathlib.Path(data_dir).joinpath('cache'),
"internal_config": pathlib.Path(data_dir).joinpath('internal_config'),
"www_dir": www_dir,
"font": pathlib.Path(data_dir).joinpath('garamond.ttf'),
"favicon": pathlib.Path(data_dir).joinpath('favicon.ico'),
"epg_cache": {},
}
for conffile in os.listdir(self.dict["filedir"]["internal_config"]):
conffilepath = os.path.join(self.dict["filedir"]["internal_config"], conffile)
self.read_config(conffilepath)
def read_config(self, conffilepath):
config_handler = configparser.ConfigParser()
config_handler.read(conffilepath)
for each_section in config_handler.sections():
if each_section not in list(self.dict.keys()):
self.dict[each_section] = {}
for (each_key, each_val) in config_handler.items(each_section):
if not each_val:
each_val = None
elif each_val.lower() in ["none", "false"]:
each_val = False
elif each_val.lower() in ["true"]:
each_val = True
elif isint(each_val):
each_val = int(each_val)
elif isfloat(each_val):
each_val = float(each_val)
elif is_arithmetic(each_val):
each_val = eval(each_val)
elif "," in each_val:
each_val = each_val.split(",")
self.dict[each_section.lower()][each_key.lower()] = each_val
def write(self, section, key, value):
if section == self.dict["main"]["dictpopname"]:
self.dict["origin"][key] = value
else:
self.dict[section][key] = value
config_handler = configparser.ConfigParser()
config_handler.read(self.config_file)
if not config_handler.has_section(section):
config_handler.add_section(section)
config_handler.set(section, key, value)
with open(self.config_file, 'w') as config_file:
config_handler.write(config_file)
def config_verification(self):
if self.dict["main"]["required"]:
required_missing = []
if isinstance(self.dict["main"]["required"], str):
self.dict["main"]["required"] = [self.dict["main"]["required"]]
if len(self.dict["main"]["required"]):
for req_item in self.dict["main"]["required"]:
req_section = req_item.split("/")[0]
req_key = req_item.split("/")[1]
if not self.dict[req_section][req_key]:
required_missing.append(req_item)
if len(required_missing):
raise fHDHRerrors.ConfigurationError("Required configuration options missing: " + ", ".join(required_missing))
self.dict["origin"] = self.dict.pop(self.dict["main"]["dictpopname"])
if isinstance(self.dict["main"]["valid_epg_methods"], str):
self.dict["main"]["valid_epg_methods"] = [self.dict["main"]["valid_epg_methods"]]
if self.dict["fhdhr"]["epg_method"] and self.dict["fhdhr"]["epg_method"] not in ["None"]:
if self.dict["fhdhr"]["epg_method"] == self.dict["main"]["dictpopname"]:
self.dict["fhdhr"]["epg_method"] = "origin"
elif self.dict["fhdhr"]["epg_method"] not in self.dict["main"]["valid_epg_methods"]:
raise fHDHRerrors.ConfigurationError("Invalid EPG Method. Exiting...")
else:
print("EPG Method not set, will not create EPG/xmltv")
# generate UUID here for when we are not using docker
if not self.dict["main"]["uuid"]:
print("No UUID found. Generating one now...")
# from https://pynative.com/python-generate-random-string/
# create a string that wouldn't be a real device uuid for
self.dict["main"]["uuid"] = ''.join(random.choice("hijklmnopqrstuvwxyz") for i in range(8))
self.write('main', 'uuid', self.dict["main"]["uuid"])
print("UUID set to: " + str(self.dict["main"]["uuid"]) + "...")
else:
print("UUID read as: " + str(self.dict["main"]["uuid"]) + "...")
if self.dict["main"]["cache_dir"]:
print("Verifying cache directory...")
if not pathlib.Path(self.dict["main"]["cache_dir"]).is_dir():
raise fHDHRerrors.ConfigurationError("Invalid Cache Directory. Exiting...")
self.dict["filedir"]["cache_dir"] = pathlib.Path(self.dict["main"]["cache_dir"])
print("Cache set to " + str(self.dict["filedir"]["cache_dir"]))
cache_dir = self.dict["filedir"]["cache_dir"]
for epg_method in self.dict["main"]["valid_epg_methods"]:
if epg_method and epg_method != "None":
epg_cache_dir = pathlib.Path(cache_dir).joinpath(epg_method)
if not epg_cache_dir.is_dir():
epg_cache_dir.mkdir()
if epg_method not in list(self.dict["filedir"]["epg_cache"].keys()):
self.dict["filedir"]["epg_cache"][epg_method] = {}
self.dict["filedir"]["epg_cache"][epg_method]["top"] = epg_cache_dir
epg_web_cache_dir = pathlib.Path(epg_cache_dir).joinpath("web_cache")
if not epg_web_cache_dir.is_dir():
epg_web_cache_dir.mkdir()
self.dict["filedir"]["epg_cache"][epg_method]["web_cache"] = epg_web_cache_dir
self.dict["filedir"]["epg_cache"][epg_method]["epg_json"] = pathlib.Path(epg_cache_dir).joinpath('epg.json')
if self.dict["fhdhr"]["stream_type"] not in ["direct", "ffmpeg"]:
raise fHDHRerrors.ConfigurationError("Invalid stream type. Exiting...")
if not self.dict["fhdhr"]["discovery_address"] and self.dict["fhdhr"]["address"] != "0.0.0.0":
self.dict["fhdhr"]["discovery_address"] = self.dict["fhdhr"]["address"]
if not self.dict["fhdhr"]["discovery_address"] or self.dict["fhdhr"]["discovery_address"] == "0.0.0.0":
self.dict["fhdhr"]["discovery_address"] = None
print("SSDP Server disabled.")

View File

@ -0,0 +1,35 @@
import time
from fHDHR.epghandler import epgtypes, xmltv
class EPGhandler():
def __init__(self, settings, origserv):
self.config = settings
self.epg_method = self.config.dict["fhdhr"]["epg_method"]
if self.epg_method:
self.sleeptime = self.config.dict[self.epg_method]["epg_update_frequency"]
self.epgtypes = epgtypes.EPGTypes(settings, origserv)
self.xmltv = xmltv.xmlTV(settings)
def get_xmltv(self, base_url):
epgdict = self.epgtypes.get_epg()
return self.xmltv.create_xmltv(base_url, epgdict)
def get_thumbnail(self, itemtype, itemid):
return self.epgtypes.get_thumbnail(itemtype, itemid)
def epgServerProcess(settings, epghandling):
print("Starting EPG thread...")
try:
while True:
epghandling.epgtypes.update()
time.sleep(epghandling.sleeptime)
except KeyboardInterrupt:
pass

View File

@ -0,0 +1,58 @@
import os
import json
from collections import OrderedDict
from fHDHR.epghandler.epgtypes import blocks, zap2it
class EPGTypes():
def __init__(self, settings, origserv):
self.config = settings
self.origin = origserv
self.blocks = blocks.BlocksEPG(settings, origserv)
self.zap2it = zap2it.ZapEPG(settings, origserv)
self.epg_method = self.config.dict["fhdhr"]["epg_method"]
if self.epg_method:
self.epg_cache_file = self.config.dict["filedir"]["epg_cache"][self.epg_method]["epg_json"]
self.epgtypename = self.epg_method
if self.epg_method == self.config.dict["main"]["dictpopname"] or self.epg_method == "origin":
self.epgtypename = self.config.dict["main"]["dictpopname"]
def get_epg(self):
epgdict = None
if os.path.isfile(self.epg_cache_file):
with open(self.epg_cache_file, 'r') as epgfile:
epgdict = json.load(epgfile)
return epgdict
def get_thumbnail(self, itemtype, itemid):
epgdict = self.get_epg()
if itemtype == "channel":
for channel in list(epgdict.keys()):
if epgdict[channel]["id"] == itemid:
return epgdict[channel]["thumbnail"]
elif itemtype == "content":
for channel in list(epgdict.keys()):
for progitem in epgdict[channel]["listing"]:
if progitem["id"] == itemid:
return progitem["thumbnail"]
return None
def update(self):
print("Updating " + self.epgtypename + " EPG cache file.")
method_to_call = getattr(self, self.epg_method)
func_to_call = getattr(method_to_call, 'update_epg')
programguide = func_to_call()
programguide = OrderedDict(sorted(programguide.items()))
for cnum in programguide:
programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start'])
with open(self.epg_cache_file, 'w') as epgfile:
epgfile.write(json.dumps(programguide, indent=4))
print("Wrote " + self.epgtypename + " EPG cache file.")

View File

@ -1,38 +1,13 @@
import os
import json
import datetime import datetime
class EmptyEPG(): class BlocksEPG():
def __init__(self, config, serviceproxy): def __init__(self, settings, origserv):
self.config = settings
self.config = config.copy() self.origserv = origserv
self.serviceproxy = serviceproxy
self.postalcode = None
self.epg_cache = None
self.cache_dir = self.config["empty"]["empty_cache"]
self.epg_cache_file = self.config["empty"]["empty_cache_file"]
self.epg_cache = self.epg_cache_open()
def epg_cache_open(self):
epg_cache = None
if os.path.isfile(self.epg_cache_file):
with open(self.epg_cache_file, 'r') as epgfile:
epg_cache = json.load(epgfile)
return epg_cache
def thumb_url(self, thumb_type, base_url, thumbnail):
if thumb_type == "channel":
return "http://" + str(base_url) + str(thumbnail)
elif thumb_type == "content":
return "http://" + str(base_url) + str(thumbnail)
def update_epg(self): def update_epg(self):
print('Updating Empty EPG cache file.')
programguide = {} programguide = {}
timestamps = [] timestamps = []
@ -53,7 +28,7 @@ class EmptyEPG():
} }
timestamps.append(timestampdict) timestamps.append(timestampdict)
for c in self.serviceproxy.get_channels(): for c in self.origserv.get_channels():
if str(c["number"]) not in list(programguide.keys()): if str(c["number"]) not in list(programguide.keys()):
programguide[str(c["number"])] = { programguide[str(c["number"])] = {
"callsign": c["callsign"], "callsign": c["callsign"],
@ -85,11 +60,4 @@ class EmptyEPG():
programguide[str(c["number"])]["listing"].append(clean_prog_dict) programguide[str(c["number"])]["listing"].append(clean_prog_dict)
for cnum in programguide:
programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start'])
self.epg_cache = programguide
with open(self.epg_cache_file, 'w') as epgfile:
epgfile.write(json.dumps(programguide, indent=4))
print('Wrote updated Empty EPG cache file.')
return programguide return programguide

View File

@ -1,141 +1,62 @@
import os
import json import json
import time import time
import datetime import datetime
import urllib.error
import urllib.parse import urllib.parse
import urllib.request
from fHDHR.tools import xmldictmaker, WebReq
def xmltimestamp_zap(inputtime): from fHDHR.fHDHRerrors import EPGSetupError
xmltime = inputtime.replace('Z', '+00:00')
xmltime = datetime.datetime.fromisoformat(xmltime)
xmltime = xmltime.strftime('%Y%m%d%H%M%S %z')
return xmltime
def xmldictmaker(inputdict, req_items, list_items=[], str_items=[]):
xml_dict = {}
for origitem in list(inputdict.keys()):
xml_dict[origitem] = inputdict[origitem]
for req_item in req_items:
if req_item not in list(inputdict.keys()):
xml_dict[req_item] = None
if not xml_dict[req_item]:
if req_item in list_items:
xml_dict[req_item] = []
elif req_item in str_items:
xml_dict[req_item] = ""
return xml_dict
class ZapEPG(): class ZapEPG():
def __init__(self, config, serviceproxy): def __init__(self, settings, origserv):
self.config = settings
self.origserv = origserv
self.web = WebReq()
self.config = config.copy() self.postalcode = self.config.dict["zap2it"]["postalcode"]
self.serviceproxy = serviceproxy
self.postalcode = None self.web_cache_dir = self.config.dict["filedir"]["epg_cache"]["zap2it"]["web_cache"]
self.epg_cache = None
self.cache_dir = self.config["main"]["zap_web_cache"]
self.epg_cache_file = self.config["zap2it"]["epg_cache"]
self.epg_cache = self.epg_cache_open()
def get_location(self): def get_location(self):
self.postalcode = self.config["zap2it"]["postalcode"] print("Zap2it postalcode not set, attempting to retrieve.")
if self.postalcode: if not self.postalcode:
url = 'http://ipinfo.io/json'
response = urllib.request.urlopen(url)
data = json.load(response)
return data["postal"]
def epg_cache_open(self):
epg_cache = None
if os.path.isfile(self.epg_cache_file):
with open(self.epg_cache_file, 'r') as epgfile:
epg_cache = json.load(epgfile)
return epg_cache
def thumb_url(self, thumb_type, base_url, thumbnail):
if thumb_type == "channel":
return thumbnail
elif thumb_type == "content":
return thumbnail
def get_cached(self, cache_key, delay, url):
cache_path = self.cache_dir.joinpath(cache_key)
if cache_path.is_file():
print('FROM CACHE:', str(cache_path))
with open(cache_path, 'rb') as f:
return f.read()
else:
print('Fetching: ', url)
try: try:
resp = urllib.request.urlopen(url) postalcode_url = 'http://ipinfo.io/json'
result = resp.read() postalcode_req = self.web.session.get(postalcode_url)
except urllib.error.HTTPError as e: data = postalcode_req.json()
if e.code == 400: self.postalcode = data["postal"]
print('Got a 400 error! Ignoring it.')
result = (
b'{'
b'"note": "Got a 400 error at this time, skipping.",'
b'"channels": []'
b'}')
else:
raise
with open(cache_path, 'wb') as f:
f.write(result)
time.sleep(int(delay))
return result
def remove_stale_cache(self, todaydate):
for p in self.cache_dir.glob('*'):
try:
cachedate = datetime.datetime.strptime(str(p.name), "%Y-%m-%d")
todaysdate = datetime.datetime.strptime(str(todaydate), "%Y-%m-%d")
if cachedate >= todaysdate:
continue
except Exception as e: except Exception as e:
print(e) raise EPGSetupError("Unable to automatically optain zap2it postalcode: " + str(e))
pass return self.postalcode
print('Removing stale cache file:', p.name)
p.unlink()
def update_epg(self): def update_epg(self):
print('Updating Zap2it EPG cache file.')
programguide = {} programguide = {}
self.get_location()
# Start time parameter is now rounded down to nearest `zap_timespan`, in s. # Start time parameter is now rounded down to nearest `zap_timespan`, in s.
zap_time = time.mktime(time.localtime()) zap_time = time.mktime(time.localtime())
zap_time_window = int(self.config["zap2it"]["timespan"]) * 3600 zap_time_window = int(self.config.dict["zap2it"]["timespan"]) * 3600
zap_time = int(zap_time - (zap_time % zap_time_window)) zap_time = int(zap_time - (zap_time % zap_time_window))
self.remove_stale_cache(zap_time) self.remove_stale_cache(zap_time)
# Fetch data in `zap_timespan` chunks. # Fetch data in `zap_timespan` chunks.
for i in range(int(7 * 24 / int(self.config["zap2it"]["timespan"]))): for i in range(int(7 * 24 / int(self.config.dict["zap2it"]["timespan"]))):
i_time = zap_time + (i * zap_time_window) i_time = zap_time + (i * zap_time_window)
parameters = { parameters = {
'aid': self.config["zap2it"]['affiliate_id'], 'aid': self.config.dict["zap2it"]['affiliate_id'],
'country': self.config["zap2it"]['country'], 'country': self.config.dict["zap2it"]['country'],
'device': self.config["zap2it"]['device'], 'device': self.config.dict["zap2it"]['device'],
'headendId': self.config["zap2it"]['headendid'], 'headendId': self.config.dict["zap2it"]['headendid'],
'isoverride': "true", 'isoverride': "true",
'languagecode': self.config["zap2it"]['languagecode'], 'languagecode': self.config.dict["zap2it"]['languagecode'],
'pref': 'm,p', 'pref': 'm,p',
'timespan': self.config["zap2it"]['timespan'], 'timespan': self.config.dict["zap2it"]['timespan'],
'timezone': self.config["zap2it"]['timezone'], 'timezone': self.config.dict["zap2it"]['timezone'],
'userId': self.config["zap2it"]['userid'], 'userId': self.config.dict["zap2it"]['userid'],
'postalCode': self.postalcode, 'postalCode': str(self.postalcode or self.get_location()),
'lineupId': '%s-%s-DEFAULT' % (self.config["zap2it"]['country'], self.config["zap2it"]['device']), 'lineupId': '%s-%s-DEFAULT' % (self.config.dict["zap2it"]['country'], self.config.dict["zap2it"]['device']),
'time': i_time, 'time': i_time,
'Activity_ID': 1, 'Activity_ID': 1,
'FromPage': "TV%20Guide", 'FromPage': "TV%20Guide",
@ -144,7 +65,7 @@ class ZapEPG():
url = 'https://tvlistings.zap2it.com/api/grid?' url = 'https://tvlistings.zap2it.com/api/grid?'
url += urllib.parse.urlencode(parameters) url += urllib.parse.urlencode(parameters)
result = self.get_cached(str(i_time), self.config["zap2it"]['delay'], url) result = self.get_cached(str(i_time), self.config.dict["zap2it"]['delay'], url)
d = json.loads(result) d = json.loads(result)
for c in d['channels']: for c in d['channels']:
@ -168,8 +89,8 @@ class ZapEPG():
progdict = xmldictmaker(event['program'], ["title", "sub-title", "releaseYear", "episodeTitle", "shortDesc", "season", "episode", "id"]) progdict = xmldictmaker(event['program'], ["title", "sub-title", "releaseYear", "episodeTitle", "shortDesc", "season", "episode", "id"])
clean_prog_dict = { clean_prog_dict = {
"time_start": xmltimestamp_zap(eventdict['startTime']), "time_start": self.xmltimestamp_zap(eventdict['startTime']),
"time_end": xmltimestamp_zap(eventdict['endTime']), "time_end": self.xmltimestamp_zap(eventdict['endTime']),
"duration_minutes": eventdict['duration'], "duration_minutes": eventdict['duration'],
"thumbnail": str("https://zap2it.tmsimg.com/assets/" + str(eventdict['thumbnail']) + ".jpg"), "thumbnail": str("https://zap2it.tmsimg.com/assets/" + str(eventdict['thumbnail']) + ".jpg"),
"title": progdict['title'] or "Unavailable", "title": progdict['title'] or "Unavailable",
@ -182,7 +103,7 @@ class ZapEPG():
"seasonnumber": progdict['season'], "seasonnumber": progdict['season'],
"episodenumber": progdict['episode'], "episodenumber": progdict['episode'],
"isnew": False, "isnew": False,
"id": progdict['id'] or xmltimestamp_zap(eventdict['startTime']), "id": progdict['id'] or self.xmltimestamp_zap(eventdict['startTime']),
} }
for f in eventdict['filter']: for f in eventdict['filter']:
@ -198,11 +119,38 @@ class ZapEPG():
programguide[str(cdict["channelNo"])]["listing"].append(clean_prog_dict) programguide[str(cdict["channelNo"])]["listing"].append(clean_prog_dict)
for cnum in programguide:
programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start'])
self.epg_cache = programguide
with open(self.epg_cache_file, 'w') as epgfile:
epgfile.write(json.dumps(programguide, indent=4))
print('Wrote updated Zap2it EPG cache file.')
return programguide return programguide
def xmltimestamp_zap(self, inputtime):
xmltime = inputtime.replace('Z', '+00:00')
xmltime = datetime.datetime.fromisoformat(xmltime)
xmltime = xmltime.strftime('%Y%m%d%H%M%S %z')
return xmltime
def get_cached(self, cache_key, delay, url):
cache_path = self.web_cache_dir.joinpath(cache_key)
if cache_path.is_file():
print('FROM CACHE:', str(cache_path))
with open(cache_path, 'rb') as f:
return f.read()
else:
print('Fetching: ', url)
resp = self.web.session.get(url)
result = resp.content
with open(cache_path, 'wb') as f:
f.write(result)
time.sleep(int(delay))
return result
def remove_stale_cache(self, todaydate):
for p in self.web_cache_dir.glob('*'):
try:
cachedate = datetime.datetime.strptime(str(p.name), "%Y-%m-%d")
todaysdate = datetime.datetime.strptime(str(todaydate), "%Y-%m-%d")
if cachedate >= todaysdate:
continue
except Exception as e:
print(e)
pass
print('Removing stale cache file:', p.name)
p.unlink()

110
fHDHR/epghandler/xmltv.py Normal file
View File

@ -0,0 +1,110 @@
import xml.etree.ElementTree
from io import BytesIO
class xmlTV():
"""Methods to create xmltv.xml"""
def __init__(self, settings):
self.config = settings
self.epg_method = self.config.dict["fhdhr"]["epg_method"]
def sub_el(self, parent, name, text=None, **kwargs):
el = xml.etree.ElementTree.SubElement(parent, name, **kwargs)
if text:
el.text = text
return el
def xmltv_headers(self):
"""This method creates the XML headers for our xmltv"""
xmltvgen = xml.etree.ElementTree.Element('tv')
xmltvgen.set('source-info-url', self.config.dict["fhdhr"]["friendlyname"])
xmltvgen.set('source-info-name', self.config.dict["main"]["servicename"])
xmltvgen.set('generator-info-name', 'fHDHR')
xmltvgen.set('generator-info-url', 'fHDHR/' + self.config.dict["main"]["reponame"])
return xmltvgen
def xmltv_file(self, xmltvgen):
"""This method is used to close out the xml file"""
xmltvfile = BytesIO()
xmltvfile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
xmltvfile.write(xml.etree.ElementTree.tostring(xmltvgen, encoding='UTF-8'))
return xmltvfile.getvalue()
def xmltv_empty(self):
"""This method is called when creation of a full xmltv is not possible"""
return self.xmltv_file(self.xmltv_headers())
def create_xmltv(self, base_url, epgdict):
if not epgdict:
return self.xmltv_empty()
out = self.xmltv_headers()
for c in list(epgdict.keys()):
c_out = self.sub_el(out, 'channel', id=str(epgdict[c]['number']))
self.sub_el(c_out, 'display-name',
text='%s %s' % (epgdict[c]['number'], epgdict[c]['callsign']))
self.sub_el(c_out, 'display-name',
text='%s %s %s' % (epgdict[c]['number'], epgdict[c]['callsign'], str(epgdict[c]['id'])))
self.sub_el(c_out, 'display-name', text=epgdict[c]['number'])
self.sub_el(c_out, 'display-name',
text='%s %s fcc' % (epgdict[c]['number'], epgdict[c]['callsign']))
self.sub_el(c_out, 'display-name', text=epgdict[c]['callsign'])
self.sub_el(c_out, 'display-name', text=epgdict[c]['callsign'])
self.sub_el(c_out, 'display-name', text=epgdict[c]['name'])
if epgdict[c]["thumbnail"] is not None:
self.sub_el(c_out, 'icon', src=("http://" + str(base_url) + "/images?source=epg&type=channel&id=" + epgdict[c]['id']))
else:
self.sub_el(c_out, 'icon', src=("http://" + str(base_url) + "/images?source=generate&message=" + epgdict[c]['number']))
for channelnum in list(epgdict.keys()):
channel_listing = epgdict[channelnum]['listing']
for program in channel_listing:
prog_out = self.sub_el(out, 'programme',
start=program['time_start'],
stop=program['time_end'],
channel=str(channelnum))
self.sub_el(prog_out, 'title', lang='en', text=program['title'])
self.sub_el(prog_out, 'desc', lang='en', text=program['description'])
self.sub_el(prog_out, 'sub-title', lang='en', text='Movie: ' + program['sub-title'])
self.sub_el(prog_out, 'length', units='minutes', text=str(int(program['duration_minutes'])))
for f in program['genres']:
self.sub_el(prog_out, 'category', lang='en', text=f)
self.sub_el(prog_out, 'genre', lang='en', text=f)
if program['seasonnumber'] and program['episodenumber']:
s_ = int(str(program['seasonnumber']), 10)
e_ = int(str(program['episodenumber']), 10)
self.sub_el(prog_out, 'episode-num', system='dd_progid',
text=str(program['id']))
self.sub_el(prog_out, 'episode-num', system='common',
text='S%02dE%02d' % (s_, e_))
self.sub_el(prog_out, 'episode-num', system='xmltv_ns',
text='%d.%d.' % (int(s_)-1, int(e_)-1))
self.sub_el(prog_out, 'episode-num', system='SxxExx">S',
text='S%02dE%02d' % (s_, e_))
if program["thumbnail"]:
self.sub_el(prog_out, 'icon', src=("http://" + str(base_url) + "/images?source=epg&type=content&id=" + program['id']))
else:
self.sub_el(prog_out, 'icon', src=("http://" + str(base_url) + "/images?source=generate&message=" + program['title'].replace(" ", "")))
if program['rating']:
rating_out = self.sub_el(prog_out, 'rating', system="MPAA")
self.sub_el(rating_out, 'value', text=program['rating'])
if program['isnew']:
self.sub_el(prog_out, 'new')
return self.xmltv_file(out)

View File

@ -0,0 +1,32 @@
class LoginError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return 'LoginError: %s' % self.value
class EPGSetupError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return 'EPGSetupError: %s' % self.value
class ConfigurationError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return 'ConfigurationError: %s' % self.value
class ConfigurationNotFound(ConfigurationError):
def __init__(self, filename):
super(ConfigurationNotFound, self).__init__(None)
self.filename = filename
def __str__(self):
return 'Unable to find the configuration file %s' % self.filename

View File

@ -4,7 +4,6 @@ from flask import (Flask, send_from_directory, request, Response,
from io import BytesIO from io import BytesIO
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import json import json
import time
import requests import requests
import subprocess import subprocess
import threading import threading
@ -28,7 +27,7 @@ def getSize(txt, font):
class HDHR_Hub(): class HDHR_Hub():
config = None config = None
serviceproxy = None origserv = None
epghandling = None epghandling = None
station_scan = False station_scan = False
station_list = [] station_list = []
@ -38,11 +37,11 @@ class HDHR_Hub():
self.tuner_lock = threading.Lock() self.tuner_lock = threading.Lock()
self.tuners = 0 self.tuners = 0
def hubprep(self, config, serviceproxy, epghandling): def hubprep(self, config, origserv, epghandling):
self.config = config self.config = config
self.max_tuners = int(self.config["fakehdhr"]["tuner_count"]) self.max_tuners = int(self.config.dict["fhdhr"]["tuner_count"])
self.station_scan = False self.station_scan = False
self.serviceproxy = serviceproxy self.origserv = origserv
self.epghandling = epghandling self.epghandling = epghandling
def tuner_usage(self, number): def tuner_usage(self, number):
@ -62,45 +61,55 @@ class HDHR_Hub():
def get_xmltv(self, base_url): def get_xmltv(self, base_url):
return self.epghandling.get_xmltv(base_url) return self.epghandling.get_xmltv(base_url)
def generate_image(self, messagetype, message):
if messagetype == "channel":
width = 360
height = 270
fontsize = 72
elif messagetype == "content":
width = 1080
height = 1440
fontsize = 100
colorBackground = "#228822"
colorText = "#717D7E"
colorOutline = "#717D7E"
fontname = str(self.config.dict["filedir"]["font"])
font = PIL.ImageFont.truetype(fontname, fontsize)
text_width, text_height = getSize(message, font)
img = PIL.Image.new('RGBA', (width+4, height+4), colorBackground)
d = PIL.ImageDraw.Draw(img)
d.text(((width-text_width)/2, (height-text_height)/2), message, fill=colorText, font=font)
d.rectangle((0, 0, width+3, height+3), outline=colorOutline)
s = BytesIO()
img.save(s, 'png')
return s.getvalue()
def get_image(self, req_args): def get_image(self, req_args):
imageid = req_args["id"] imageUri = self.epghandling.get_thumbnail(req_args["type"], req_args["id"])
if not imageUri:
return self.generate_image(req_args["type"], req_args["id"])
if req_args["source"] == "proxy": try:
if req_args["type"] == "channel":
imageUri = self.serviceproxy.get_channel_thumbnail(imageid)
elif req_args["type"] == "content":
imageUri = self.serviceproxy.get_content_thumbnail(imageid)
req = requests.get(imageUri) req = requests.get(imageUri)
return req.content return req.content
except Exception as e:
print(e)
return self.generate_image(req_args["type"], req_args["id"])
elif req_args["source"] == "empty": def get_image_type(self, image_data):
if req_args["type"] == "channel": header_byte = image_data[0:3].hex().lower()
width = 360 if header_byte == '474946':
height = 270 return "image/gif"
text = req_args["id"] elif header_byte == '89504e':
fontsize = 72 return "image/png"
elif req_args["type"] == "content": elif header_byte == 'ffd8ff':
width = 1080 return "image/jpeg"
height = 1440 else:
fontsize = 100 return "image/jpeg"
text = req_args["id"]
colorBackground = "#228822"
colorText = "#717D7E"
colorOutline = "#717D7E"
fontname = str(self.config["fakehdhr"]["font"])
font = PIL.ImageFont.truetype(fontname, fontsize)
text_width, text_height = getSize(text, font)
img = PIL.Image.new('RGBA', (width+4, height+4), colorBackground)
d = PIL.ImageDraw.Draw(img)
d.text(((width-text_width)/2, (height-text_height)/2), text, fill=colorText, font=font)
d.rectangle((0, 0, width+3, height+3), outline=colorOutline)
s = BytesIO()
img.save(s, 'png')
return s.getvalue()
def get_xmldiscover(self, base_url): def get_xmldiscover(self, base_url):
out = ET.Element('root') out = ET.Element('root')
@ -114,12 +123,12 @@ class HDHR_Hub():
device_out = sub_el(out, 'device') device_out = sub_el(out, 'device')
sub_el(device_out, 'deviceType', "urn:schemas-upnp-org:device:MediaServer:1") sub_el(device_out, 'deviceType', "urn:schemas-upnp-org:device:MediaServer:1")
sub_el(device_out, 'friendlyName', self.config["fakehdhr"]["friendlyname"]) sub_el(device_out, 'friendlyName', self.config.dict["fhdhr"]["friendlyname"])
sub_el(device_out, 'manufacturer', "Silicondust") sub_el(device_out, 'manufacturer', self.config.dict["dev"]["reporting_manufacturer"])
sub_el(device_out, 'modelName', self.config["dev"]["reporting_model"]) sub_el(device_out, 'modelName', self.config.dict["dev"]["reporting_model"])
sub_el(device_out, 'modelNumber', self.config["dev"]["reporting_model"]) sub_el(device_out, 'modelNumber', self.config.dict["dev"]["reporting_model"])
sub_el(device_out, 'serialNumber') sub_el(device_out, 'serialNumber')
sub_el(device_out, 'UDN', "uuid:" + self.config["main"]["uuid"]) sub_el(device_out, 'UDN', "uuid:" + self.config.dict["main"]["uuid"])
fakefile = BytesIO() fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n') fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
@ -128,13 +137,13 @@ class HDHR_Hub():
def get_discover_json(self, base_url): def get_discover_json(self, base_url):
jsondiscover = { jsondiscover = {
"FriendlyName": self.config["fakehdhr"]["friendlyname"], "FriendlyName": self.config.dict["fhdhr"]["friendlyname"],
"Manufacturer": "Silicondust", "Manufacturer": "Borondust",
"ModelNumber": self.config["dev"]["reporting_model"], "ModelNumber": self.config.dict["dev"]["reporting_model"],
"FirmwareName": self.config["dev"]["reporting_firmware_name"], "FirmwareName": self.config.dict["dev"]["reporting_firmware_name"],
"TunerCount": self.config["fakehdhr"]["tuner_count"], "TunerCount": self.config.dict["fhdhr"]["tuner_count"],
"FirmwareVersion": self.config["dev"]["reporting_firmware_ver"], "FirmwareVersion": self.config.dict["dev"]["reporting_firmware_ver"],
"DeviceID": self.config["main"]["uuid"], "DeviceID": self.config.dict["main"]["uuid"],
"DeviceAuth": "fHDHR", "DeviceAuth": "fHDHR",
"BaseURL": "http://" + base_url, "BaseURL": "http://" + base_url,
"LineupURL": "http://" + base_url + "/lineup.json" "LineupURL": "http://" + base_url + "/lineup.json"
@ -143,7 +152,7 @@ class HDHR_Hub():
def get_lineup_status(self): def get_lineup_status(self):
if self.station_scan: if self.station_scan:
channel_count = self.serviceproxy.get_station_total() channel_count = self.origserv.get_station_total()
jsonlineup = { jsonlineup = {
"ScanInProgress": "true", "ScanInProgress": "true",
"Progress": 99, "Progress": 99,
@ -153,14 +162,14 @@ class HDHR_Hub():
jsonlineup = { jsonlineup = {
"ScanInProgress": "false", "ScanInProgress": "false",
"ScanPossible": "true", "ScanPossible": "true",
"Source": self.config["dev"]["reporting_tuner_type"], "Source": self.config.dict["dev"]["reporting_tuner_type"],
"SourceList": [self.config["dev"]["reporting_tuner_type"]], "SourceList": [self.config.dict["dev"]["reporting_tuner_type"]],
} }
return jsonlineup return jsonlineup
def get_lineup_xml(self, base_url): def get_lineup_xml(self, base_url):
out = ET.Element('Lineup') out = ET.Element('Lineup')
station_list = self.serviceproxy.get_station_list(base_url) station_list = self.origserv.get_station_list(base_url)
for station_item in station_list: for station_item in station_list:
program_out = sub_el(out, 'Program') program_out = sub_el(out, 'Program')
sub_el(program_out, 'GuideNumber', station_item['GuideNumber']) sub_el(program_out, 'GuideNumber', station_item['GuideNumber'])
@ -199,11 +208,11 @@ class HDHR_HTTP_Server():
@app.route('/') @app.route('/')
def root_path(): def root_path():
return hdhr.config["fakehdhr"]["friendlyname"] return hdhr.config.dict["fhdhr"]["friendlyname"]
@app.route('/favicon.ico', methods=['GET']) @app.route('/favicon.ico', methods=['GET'])
def favicon(): def favicon():
return send_from_directory(hdhr.config["main"]["www_dir"], return send_from_directory(hdhr.config.dict["filedir"]["www_dir"],
'favicon.ico', 'favicon.ico',
mimetype='image/vnd.microsoft.icon') mimetype='image/vnd.microsoft.icon')
@ -241,7 +250,7 @@ class HDHR_HTTP_Server():
@app.route('/lineup.json', methods=['GET']) @app.route('/lineup.json', methods=['GET'])
def lineup_json(): def lineup_json():
base_url = request.headers["host"] base_url = request.headers["host"]
station_list = hdhr.serviceproxy.get_station_list(base_url) station_list = hdhr.origserv.get_station_list(base_url)
return Response(status=200, return Response(status=200,
response=json.dumps(station_list, indent=4), response=json.dumps(station_list, indent=4),
mimetype='application/json') mimetype='application/json')
@ -263,12 +272,36 @@ class HDHR_HTTP_Server():
mimetype='application/json') mimetype='application/json')
@app.route('/images', methods=['GET']) @app.route('/images', methods=['GET'])
def images_nothing(): def images():
if ('source' not in list(request.args.keys()) or 'id' not in list(request.args.keys()) or 'type' not in list(request.args.keys())):
abort(404)
image = hdhr.get_image(request.args) if 'source' not in list(request.args.keys()):
return Response(image, content_type='image/png', direct_passthrough=True) image = hdhr.generate_image("content", "Unknown Request")
else:
itemtype = 'content'
if 'type' in list(request.args.keys()):
itemtype = request.args["type"]
if request.args['source'] == 'epg':
if 'id' in list(request.args.keys()):
req_dict = {
"source": request.args["source"],
"type": request.args["type"],
"id": request.args["id"],
}
image = hdhr.get_image(req_dict)
else:
itemmessage = "Unknown Request"
image = hdhr.generate_image(itemtype, itemmessage)
elif request.args['source'] == 'generate':
itemmessage = "Unknown Request"
if 'message' in list(request.args.keys()):
itemmessage = request.args["message"]
image = hdhr.generate_image(itemtype, itemmessage)
else:
itemmessage = "Unknown Request"
image = hdhr.generate_image(itemtype, itemmessage)
return Response(image, content_type=hdhr.get_image_type(image), direct_passthrough=True)
@app.route('/watch', methods=['GET']) @app.route('/watch', methods=['GET'])
def watch(): def watch():
@ -280,27 +313,24 @@ class HDHR_HTTP_Server():
tuner = hdhr.get_tuner() tuner = hdhr.get_tuner()
if not tuner: if not tuner:
print("A " + method + " stream request for channel " +
str(channel_id) + " was rejected do to a lack of available tuners.")
abort(503) abort(503)
channelUri = hdhr.serviceproxy.get_channel_stream(channel_id) print("Attempting a " + method + " stream request for channel " + str(channel_id))
hdhr.tuner_usage(1)
channelUri = hdhr.origserv.get_channel_stream(channel_id)
# print("Proxy URL determined as " + str(channelUri))
if method == "direct": if method == "direct":
duration = request.args.get('duration', default=0, type=int) chunksize = int(hdhr.config.dict["direct_stream"]['chunksize'])
if not duration == 0:
duration += time.time()
req = requests.get(channelUri, stream=True) req = requests.get(channelUri, stream=True)
hdhr.tuner_usage(1)
def generate(): def generate():
try: try:
yield '' for chunk in req.iter_content(chunk_size=chunksize):
for chunk in req.iter_content(chunk_size=int(hdhr.config["direct_stream"]['chunksize'])):
if not duration == 0 and not time.time() < duration:
req.close()
hdhr.tuner_usage(-1)
break
yield chunk yield chunk
except GeneratorExit: except GeneratorExit:
req.close() req.close()
@ -311,7 +341,9 @@ class HDHR_HTTP_Server():
elif method == "ffmpeg": elif method == "ffmpeg":
ffmpeg_command = [hdhr.config["ffmpeg"]["ffmpeg_path"], bytes_per_read = int(hdhr.config.dict["ffmpeg"]["bytes_per_read"])
ffmpeg_command = [hdhr.config.dict["ffmpeg"]["ffmpeg_path"],
"-i", channelUri, "-i", channelUri,
"-c", "copy", "-c", "copy",
"-f", "mpegts", "-f", "mpegts",
@ -321,12 +353,11 @@ class HDHR_HTTP_Server():
] ]
ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE) ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE)
hdhr.tuner_usage(1)
def generate(): def generate():
try: try:
while True: while True:
videoData = ffmpeg_proc.stdout.read(int(hdhr.config["ffmpeg"]["bytes_per_read"])) videoData = ffmpeg_proc.stdout.read(bytes_per_read)
if not videoData: if not videoData:
break break
try: try:
@ -366,12 +397,12 @@ class HDHR_HTTP_Server():
return Response(status=200, response=currenthtmlerror, mimetype='text/html') return Response(status=200, response=currenthtmlerror, mimetype='text/html')
def __init__(self, config): def __init__(self, config):
self.config = config.copy() self.config = config
def run(self): def run(self):
self.http = WSGIServer(( self.http = WSGIServer((
self.config["fakehdhr"]["address"], self.config.dict["fhdhr"]["address"],
int(self.config["fakehdhr"]["port"]) int(self.config.dict["fhdhr"]["port"])
), self.app.wsgi_app) ), self.app.wsgi_app)
try: try:
self.http.serve_forever() self.http.serve_forever()
@ -379,7 +410,8 @@ class HDHR_HTTP_Server():
self.http.stop() self.http.stop()
def interface_start(config, serviceproxy, epghandling): def interface_start(config, origserv, epghandling):
hdhr.hubprep(config, serviceproxy, epghandling) print("Starting fHDHR Web Interface")
hdhr.hubprep(config, origserv, epghandling)
fakhdhrserver = HDHR_HTTP_Server(config) fakhdhrserver = HDHR_HTTP_Server(config)
fakhdhrserver.run() fakhdhrserver.run()

View File

@ -0,0 +1,89 @@
import datetime
import fHDHR.originservice.nextpvr as serviceorigin
from fHDHR.tools import hours_between_datetime
from fHDHR.fHDHRerrors import LoginError
class OriginService():
def __init__(self, settings):
self.config = settings
self.serviceorigin = serviceorigin.fHDHRservice(settings)
if not self.serviceorigin.login():
raise LoginError(self.config.dict["main"]["servicename"] + " Login Failed.")
self.streamtype = self.config.dict["fhdhr"]["stream_type"]
self.channels = {
"list": {},
"list_updated": None,
}
def append_channel_info(self, chanlist):
for chan in chanlist:
if chan["number"] not in list(self.channels["list"].keys()):
self.channels["list"][chan["number"]] = {}
for chankey in list(chan.keys()):
self.channels["list"][chan["number"]][chankey] = chan[chankey]
def get_channels(self):
updatelist = False
if not self.channels["list_updated"]:
updatelist = True
elif hours_between_datetime(self.channels["list_updated"], datetime.datetime.now()) > 12:
updatelist = True
if updatelist:
chanlist = self.serviceorigin.get_channels()
self.append_channel_info(chanlist)
self.channels["list_updated"] = datetime.datetime.now()
channel_list = []
for chandict in list(self.channels["list"].keys()):
channel_list.append(self.channels["list"][chandict])
return channel_list
def get_fhdhr_stream_url(self, base_url, channel):
return ('%s%s/watch?method=%s&channel=%s' %
("http://",
base_url,
self.streamtype,
channel['number']))
def get_station_list(self, base_url):
station_list = []
for c in self.get_channels():
station_list.append({
'GuideNumber': c['number'],
'GuideName': c['name'],
'URL': self.get_fhdhr_stream_url(base_url, c),
})
return station_list
def get_channel_stream(self, channel_number):
if channel_number not in list(self.channels["list"].keys()):
self.get_channels()
if channel_number not in list(self.channels["list"].keys()):
return None
if "stream_url" not in list(self.channels["list"][channel_number].keys()):
chandict = self.get_channel_dict("number", channel_number)
streamlist, caching = self.serviceorigin.get_channel_stream(chandict, self.channels["list"])
if caching:
self.append_channel_info(streamlist)
return self.channels["list"][channel_number]["stream_url"]
else:
chanstreamdict = next(item for item in streamlist if item["number"] == channel_number)
return chanstreamdict["stream_url"]
def get_station_total(self):
chanlist = self.get_channels()
return len(chanlist)
def get_channel_dict(self, keyfind, valfind):
chanlist = self.get_channels()
return next(item for item in chanlist if item[keyfind] == valfind)
def update_epg(self):
return self.serviceorigin.update_epg()

View File

@ -0,0 +1,198 @@
import xmltodict
import json
import hashlib
import datetime
import fHDHR.tools
class fHDHRservice():
def __init__(self, settings):
self.config = settings
self.web = fHDHR.tools.WebReq()
def login(self):
print("Logging into NextPVR")
self.sid = self.get_sid()
if not self.sid:
return False
else:
print("NextPVR Login Success")
self.config.write(self.config.dict["main"]["dictpopname"], 'sid', self.sid)
return True
def get_sid(self):
if self.config.dict["origin"]["sid"]:
return self.config.dict["origin"]["sid"]
initiate_url = ('%s%s:%s/service?method=session.initiate&ver=1.0&device=fhdhr' %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
))
initiate_req = self.web.session.get(initiate_url)
initiate_dict = xmltodict.parse(initiate_req.content)
sid = initiate_dict['rsp']['sid']
salt = initiate_dict['rsp']['salt']
md5PIN = hashlib.md5(str(self.config.dict["origin"]['pin']).encode('utf-8')).hexdigest()
string = ':%s:%s' % (md5PIN, salt)
clientKey = hashlib.md5(string.encode('utf-8')).hexdigest()
login_url = ('%s%s:%s/service?method=session.login&sid=%s&md5=%s' %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
sid,
clientKey
))
login_req = self.web.session.get(login_url)
login_dict = xmltodict.parse(login_req.content)
loginsuccess = None
if login_dict['rsp']['@stat'] == "ok":
if login_dict['rsp']['allow_watch'] == "true":
loginsuccess = sid
return loginsuccess
def get_channels(self):
data_url = ('%s%s:%s/service?method=channel.list&sid=%s' %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
self.sid
))
data_req = self.web.session.get(data_url)
data_dict = xmltodict.parse(data_req.content)
if 'channels' not in list(data_dict['rsp'].keys()):
print("Could not retrieve channel list")
return []
channel_o_list = data_dict['rsp']['channels']['channel']
channel_list = []
for c in channel_o_list:
dString = json.dumps(c)
channel_dict = eval(dString)
clean_station_item = {
"name": channel_dict["name"],
"callsign": channel_dict["name"],
"number": channel_dict["formatted-number"],
"id": channel_dict["id"],
}
channel_list.append(clean_station_item)
return channel_list
def get_channel_stream(self, chandict, allchandict):
caching = True
streamlist = []
streamdict = {}
streamurl = ('%s%s:%s/live?channel=%s&client=%s' %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
str(chandict["number"]),
str(chandict["number"]),
))
streamdict = {"number": chandict["number"], "stream_url": streamurl}
streamlist.append(streamdict)
return streamlist, caching
def get_channel_thumbnail(self, channel_id):
channel_thumb_url = ("%s%s:%s/service?method=channel.icon&channel_id=%s" %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
str(channel_id)
))
return channel_thumb_url
def get_content_thumbnail(self, content_id):
item_thumb_url = ("%s%s:%s/service?method=channel.show.artwork&sid=%s&event_id=%s" %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
self.config.dict["origin"]["sid"],
str(content_id)
))
return item_thumb_url
def update_epg(self):
programguide = {}
for c in self.get_channels():
cdict = fHDHR.tools.xmldictmaker(c, ["callsign", "name", "number", "id"])
if str(cdict['number']) not in list(programguide.keys()):
programguide[str(cdict['number'])] = {
"callsign": cdict["callsign"],
"name": cdict["name"] or cdict["callsign"],
"number": cdict["number"],
"id": cdict["id"],
"thumbnail": self.get_channel_thumbnail(cdict['id']),
"listing": [],
}
epg_url = ('%s%s:%s/service?method=channel.listings&channel_id=%s' %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
str(cdict["id"]),
))
epg_req = self.web.session.get(epg_url)
epg_dict = xmltodict.parse(epg_req.content)
for program_listing in epg_dict["rsp"]["listings"]:
for program_item in epg_dict["rsp"]["listings"][program_listing]:
if not isinstance(program_item, str):
progdict = fHDHR.tools.xmldictmaker(program_item, ["start", "end", "title", "name", "subtitle", "rating", "description", "season", "episode", "id", "episodeTitle"])
clean_prog_dict = {
"time_start": self.xmltimestamp_nextpvr(progdict["start"]),
"time_end": self.xmltimestamp_nextpvr(progdict["end"]),
"duration_minutes": self.duration_nextpvr_minutes(progdict["start"], progdict["end"]),
"thumbnail": self.get_content_thumbnail(progdict['id']),
"title": progdict['name'] or "Unavailable",
"sub-title": progdict['subtitle'] or "Unavailable",
"description": progdict['description'] or "Unavailable",
"rating": progdict['rating'] or "N/A",
"episodetitle": progdict['episodeTitle'],
"releaseyear": None,
"genres": [],
"seasonnumber": progdict['season'],
"episodenumber": progdict['episode'],
"isnew": False,
"id": progdict['id'] or self.xmltimestamp_nextpvr(progdict["start"]),
}
if 'genre' in list(progdict.keys()):
clean_prog_dict["genres"] = progdict['genre'].split(",")
if clean_prog_dict['sub-title'].startswith("Movie:"):
clean_prog_dict['releaseyear'] = clean_prog_dict['sub-title'].split("Movie: ")[-1]
clean_prog_dict['sub-title'] = "Unavailable"
clean_prog_dict["genres"].append("Movie")
# TODO isNEW
programguide[str(cdict["number"])]["listing"].append(clean_prog_dict)
return programguide
def xmltimestamp_nextpvr(self, epochtime):
xmltime = datetime.datetime.fromtimestamp(int(epochtime)/1000)
xmltime = str(xmltime.strftime('%Y%m%d%H%M%S')) + " +0000"
return xmltime
def duration_nextpvr_minutes(self, starttime, endtime):
return ((int(endtime) - int(starttime))/1000/60)

View File

@ -24,14 +24,17 @@ logger = logging.getLogger()
# mostly from https://github.com/ZeWaren/python-upnp-ssdp-example # mostly from https://github.com/ZeWaren/python-upnp-ssdp-example
def ssdpServerProcess(config): def ssdpServerProcess(settings):
ssdp = SSDPServer() ssdp = SSDPServer()
ssdp.ssdp_port = 1900 ssdp.ssdp_port = 1900
ssdp.register('local', ssdp.register('local',
'uuid:' + config["main"]["uuid"] + '::upnp:rootdevice', 'uuid:' + settings.dict["main"]["uuid"] + '::upnp:rootdevice',
'upnp:rootdevice', 'upnp:rootdevice',
'http://' + config["fakehdhr"]["discovery_address"] + ':' + 'http://' + settings.dict["fhdhr"]["discovery_address"] + ':' +
config["fakehdhr"]["port"] + '/device.xml') str(settings.dict["fhdhr"]["port"]) + '/device.xml')
print("SSDP server Started on port " + str(ssdp.ssdp_port) +
" and broadcasting the availability of " + settings.dict["fhdhr"]["friendlyname"] +
" at " 'http://' + settings.dict["fhdhr"]["discovery_address"] + ':' + str(settings.dict["fhdhr"]["port"]))
try: try:
ssdp.run() ssdp.run()
except KeyboardInterrupt: except KeyboardInterrupt:

81
fHDHR/tools/__init__.py Normal file
View File

@ -0,0 +1,81 @@
import os
import sys
import ast
import requests
UNARY_OPS = (ast.UAdd, ast.USub)
BINARY_OPS = (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod)
def clean_exit():
sys.stderr.flush()
sys.stdout.flush()
os._exit(0)
def xmldictmaker(inputdict, req_items, list_items=[], str_items=[]):
xml_dict = {}
for origitem in list(inputdict.keys()):
xml_dict[origitem] = inputdict[origitem]
for req_item in req_items:
if req_item not in list(inputdict.keys()):
xml_dict[req_item] = None
if not xml_dict[req_item]:
if req_item in list_items:
xml_dict[req_item] = []
elif req_item in str_items:
xml_dict[req_item] = ""
return xml_dict
def is_arithmetic(s):
def _is_arithmetic(node):
if isinstance(node, ast.Num):
return True
elif isinstance(node, ast.Expression):
return _is_arithmetic(node.body)
elif isinstance(node, ast.UnaryOp):
valid_op = isinstance(node.op, UNARY_OPS)
return valid_op and _is_arithmetic(node.operand)
elif isinstance(node, ast.BinOp):
valid_op = isinstance(node.op, BINARY_OPS)
return valid_op and _is_arithmetic(node.left) and _is_arithmetic(node.right)
else:
raise ValueError('Unsupported type {}'.format(node))
try:
return _is_arithmetic(ast.parse(s, mode='eval'))
except (SyntaxError, ValueError):
return False
def isint(x):
try:
a = float(x)
b = int(a)
except ValueError:
return False
else:
return a == b
def isfloat(x):
try:
float(x)
except ValueError:
return False
else:
return True
def hours_between_datetime(first_time, later_time):
timebetween = first_time - later_time
return (timebetween.total_seconds() / 60 / 60)
class WebReq():
def __init__(self):
self.session = requests.Session()

View File

@ -1,178 +0,0 @@
import os
import sys
import random
import configparser
import pathlib
def clean_exit():
sys.stderr.flush()
sys.stdout.flush()
os._exit(0)
class HDHRConfig():
config_file = None
config_handler = configparser.ConfigParser()
script_dir = None
config = {}
def __init__(self, script_dir, args):
self.get_config_path(script_dir, args)
self.import_default_config(script_dir)
self.import_service_config(script_dir)
self.import_config()
self.critical_config(script_dir)
self.config_adjustments_this()
self.config_adjustments()
def get_config_path(self, script_dir, args):
if args.cfg:
self.config_file = pathlib.Path(str(args.cfg))
if not self.config_file or not os.path.exists(self.config_file):
print("Config file missing, Exiting...")
clean_exit()
print("Loading Configuration File: " + str(self.config_file))
def import_config(self):
self.config_handler.read(self.config_file)
for each_section in self.config_handler.sections():
if each_section not in list(self.config.keys()):
self.config[each_section] = {}
for (each_key, each_val) in self.config_handler.items(each_section):
self.config[each_section.lower()][each_key.lower()] = each_val
def import_default_config(self, script_dir):
config_handler = configparser.ConfigParser()
data_dir = pathlib.Path(script_dir).joinpath('data')
internal_config_dir = pathlib.Path(data_dir).joinpath('internal_config')
serviceconf = pathlib.Path(internal_config_dir).joinpath('fakehdhr.ini')
config_handler.read(serviceconf)
for each_section in config_handler.sections():
if each_section not in list(self.config.keys()):
self.config[each_section] = {}
for (each_key, each_val) in config_handler.items(each_section):
if each_val == "fHDHR_None":
each_val = None
elif each_val == "fHDHR_True":
each_val = True
elif each_val == "fHDHR_False":
each_val = False
self.config[each_section.lower()][each_key.lower()] = each_val
def import_service_config(self, script_dir):
config_handler = configparser.ConfigParser()
data_dir = pathlib.Path(script_dir).joinpath('data')
internal_config_dir = pathlib.Path(data_dir).joinpath('internal_config')
serviceconf = pathlib.Path(internal_config_dir).joinpath('serviceconf.ini')
config_handler.read(serviceconf)
for each_section in config_handler.sections():
if each_section not in list(self.config.keys()):
self.config[each_section] = {}
for (each_key, each_val) in config_handler.items(each_section):
if each_val == "fHDHR_None":
each_val = None
elif each_val == "fHDHR_True":
each_val = True
elif each_val == "fHDHR_False":
each_val = False
self.config[each_section.lower()][each_key.lower()] = each_val
def write(self, section, key, value):
self.config[section][key] = value
self.config_handler.set(section, key, value)
with open(self.config_file, 'w') as config_file:
self.config_handler.write(config_file)
def critical_config(self, script_dir):
self.config["main"]["script_dir"] = script_dir
data_dir = pathlib.Path(script_dir).joinpath('data')
self.config["main"]["data_dir"] = data_dir
self.config["fakehdhr"]["font"] = pathlib.Path(data_dir).joinpath('garamond.ttf')
if not self.config["main"]["cache_dir"]:
self.config["main"]["cache_dir"] = pathlib.Path(data_dir).joinpath('cache')
else:
self.config["main"]["cache_dir"] = pathlib.Path(self.config["main"]["cache_dir"])
if not self.config["main"]["cache_dir"].is_dir():
print("Invalid Cache Directory. Exiting...")
clean_exit()
cache_dir = self.config["main"]["cache_dir"]
empty_cache = pathlib.Path(cache_dir).joinpath('empty_cache')
self.config["empty"]["empty_cache"] = empty_cache
if not empty_cache.is_dir():
empty_cache.mkdir()
self.config["empty"]["empty_cache_file"] = pathlib.Path(empty_cache).joinpath('epg.json')
www_dir = pathlib.Path(data_dir).joinpath('www')
self.config["main"]["www_dir"] = www_dir
self.config["main"]["favicon"] = pathlib.Path(www_dir).joinpath('favicon.ico')
def config_adjustments(self):
# generate UUID here for when we are not using docker
if self.config["main"]["uuid"] is None:
print("No UUID found. Generating one now...")
# from https://pynative.com/python-generate-random-string/
# create a string that wouldn't be a real device uuid for
self.config["main"]["uuid"] = ''.join(random.choice("hijklmnopqrstuvwxyz") for i in range(8))
self.write('main', 'uuid', self.config["main"]["uuid"])
print("UUID set to: " + self.config["main"]["uuid"] + "...")
if not self.config["fakehdhr"]["discovery_address"]:
if self.config["fakehdhr"]["address"] != "0.0.0.0":
self.config["fakehdhr"]["discovery_address"] = self.config["fakehdhr"]["address"]
print("Server is set to run on " +
str(self.config["fakehdhr"]["address"]) + ":" +
str(self.config["fakehdhr"]["port"]))
def config_adjustments_this(self):
self.config["proxy"] = self.config.pop(self.config["main"]["dictpopname"])
self.config_adjustments_proxy()
self.config_adjustments_zap2it()
def config_adjustments_proxy(self):
cache_dir = self.config["main"]["cache_dir"]
if self.config["main"]["credentials"]:
credentials_list = self.config["main"]["credentials"].split(",")
creds_missing = False
if len(credentials_list):
for cred_item in credentials_list:
if not self.config["proxy"][cred_item]:
creds_missing = True
if creds_missing:
print(self.config["main"]["servicename"] + " Login Credentials Missing. Exiting...")
clean_exit()
proxy_cache = pathlib.Path(cache_dir).joinpath('proxy')
self.config["main"]["proxy_cache"] = proxy_cache
if not proxy_cache.is_dir():
proxy_cache.mkdir()
self.config["proxy"]["sidfile"] = pathlib.Path(proxy_cache).joinpath('sid.txt')
self.config["proxy"]["epg_cache"] = pathlib.Path(proxy_cache).joinpath('epg.json')
proxy_web_cache = pathlib.Path(proxy_cache).joinpath('proxy_web_cache')
self.config["main"]["proxy_web_cache"] = proxy_web_cache
if not proxy_web_cache.is_dir():
proxy_web_cache.mkdir()
def config_adjustments_zap2it(self):
cache_dir = self.config["main"]["cache_dir"]
zap_cache = pathlib.Path(cache_dir).joinpath('zap2it')
self.config["main"]["zap_cache"] = zap_cache
if not zap_cache.is_dir():
zap_cache.mkdir()
self.config["zap2it"]["epg_cache"] = pathlib.Path(zap_cache).joinpath('epg.json')
zap_web_cache = pathlib.Path(zap_cache).joinpath('zap_web_cache')
self.config["main"]["zap_web_cache"] = zap_web_cache
if not zap_web_cache.is_dir():
zap_web_cache.mkdir()

74
main.py
View File

@ -1,74 +1,14 @@
#!/usr/bin/env python3
# coding=utf-8
import os import os
import sys import sys
import pathlib import pathlib
import argparse from multiprocessing import freeze_support
from multiprocessing import Process
import fhdhrconfig from fHDHR.cli import run
import proxyservice
import fakehdhr
import epghandler
import ssdpserver
if sys.version_info.major == 2 or sys.version_info < (3, 3):
print('Error: FakeHDHR requires python 3.3+.')
sys.exit(1)
def get_args():
parser = argparse.ArgumentParser(description='FakeHDHR.', epilog='')
parser.add_argument('--config_file', dest='cfg', type=str, default=None, help='')
return parser.parse_args()
def clean_exit():
sys.stderr.flush()
sys.stdout.flush()
os._exit(0)
SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
if __name__ == '__main__': if __name__ == '__main__':
freeze_support()
# Gather args sys.exit(run.main(SCRIPT_DIR))
args = get_args()
# set to directory of script
script_dir = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
# Open Configuration File
print("Opening and Verifying Configuration File.")
config = fhdhrconfig.HDHRConfig(script_dir, args)
# Open proxyservice
serviceproxy = proxyservice.proxyserviceFetcher(config.config)
# Open EPG Handler
epghandling = epghandler.EPGhandler(config.config, serviceproxy)
try:
print("Starting EPG thread...")
epgServer = Process(target=epghandler.epgServerProcess, args=(config.config.copy(), epghandling))
epgServer.start()
print("Starting fHDHR Interface")
fhdhrServer = Process(target=fakehdhr.interface_start, args=(config.config.copy(), serviceproxy, epghandling))
fhdhrServer.start()
if (config.config["fakehdhr"]["discovery_address"] and
config.config["fakehdhr"]["discovery_address"] != "0.0.0.0"):
print("Starting SSDP server...")
ssdpServer = Process(target=ssdpserver.ssdpServerProcess, args=(config.config.copy(),))
ssdpServer.daemon = True
ssdpServer.start()
else:
ssdpServer = None
print("Not Starting SSDP server...")
except KeyboardInterrupt:
print('^C received, shutting down the server')
epgServer.terminate()
fhdhrServer.terminate()
if ssdpServer:
ssdpServer.terminate()
clean_exit()

View File

@ -1,304 +0,0 @@
import os
import xmltodict
import json
import hashlib
import datetime
import urllib.error
import urllib.parse
import urllib.request
def xmldictmaker(inputdict, req_items, list_items=[], str_items=[]):
xml_dict = {}
for origitem in list(inputdict.keys()):
xml_dict[origitem] = inputdict[origitem]
for req_item in req_items:
if req_item not in list(inputdict.keys()):
xml_dict[req_item] = None
if not xml_dict[req_item]:
if req_item in list_items:
xml_dict[req_item] = []
elif req_item in str_items:
xml_dict[req_item] = ""
return xml_dict
class NextPVR_Auth():
config = {
'npvrURL': '',
'npvrSID': '',
'npvrPIN': '',
}
sidfile = None
def __init__(self, config):
self.sidfile = config["proxy"]["sidfile"]
self.config["npvrPIN"] = config["proxy"]["pin"]
self.config["npvrURL"] = ('%s%s:%s' %
("https://" if config["proxy"]["ssl"] else "http://",
config["proxy"]["address"],
str(config["proxy"]["port"]),
))
def _check_sid(self):
if 'sid' not in self.config:
if os.path.isfile(self.sidfile):
with open(self.sidfile, 'r') as text_file:
self.config['sid'] = text_file.read()
print('Read SID from file.')
else:
self._get_sid()
return True
def _get_sid(self):
sid = ''
salt = ''
clientKey = ''
initiate_url = "%s/service?method=session.initiate&ver=1.0&device=fhdhr" % self.config['npvrURL']
initiate_req = urllib.request.urlopen(initiate_url)
initiate_dict = xmltodict.parse(initiate_req)
sid = initiate_dict['rsp']['sid']
salt = initiate_dict['rsp']['salt']
md5PIN = hashlib.md5(self.config['npvrPIN'].encode('utf-8')).hexdigest()
string = ':%s:%s' % (md5PIN, salt)
clientKey = hashlib.md5(string.encode('utf-8')).hexdigest()
login_url = '%s/service?method=session.login&sid=%s&md5=%s' % (self.config['npvrURL'], sid, clientKey)
login_req = urllib.request.urlopen(login_url)
login_dict = xmltodict.parse(login_req)
if login_dict['rsp']['allow_watch'] == "true":
self.config['sid'] = sid
with open(self.sidfile, 'w') as text_file:
text_file.write(self.config['sid'])
print('Wrote SID to file.')
else:
print("NextPVR Login Failed")
self.config['sid'] = ''
def xmltimestamp_nextpvr(epochtime):
xmltime = datetime.datetime.fromtimestamp(int(epochtime)/1000)
xmltime = str(xmltime.strftime('%Y%m%d%H%M%S')) + " +0000"
return xmltime
def duration_nextpvr_minutes(starttime, endtime):
return ((int(endtime) - int(starttime))/1000/60)
class proxyserviceFetcher():
def __init__(self, config):
self.config = config.copy()
self.epg_cache = None
self.epg_cache_file = self.config["proxy"]["epg_cache"]
self.urls = {}
self.url_assembler()
self.auth = NextPVR_Auth(config)
self.epg_cache = self.epg_cache_open()
def epg_cache_open(self):
epg_cache = None
if os.path.isfile(self.epg_cache_file):
with open(self.epg_cache_file, 'r') as epgfile:
epg_cache = json.load(epgfile)
return epg_cache
def thumb_url(self, thumb_type, base_url, thumbnail):
if thumb_type == "channel":
return "http://" + str(base_url) + str(thumbnail)
elif thumb_type == "content":
return "http://" + str(base_url) + str(thumbnail)
def url_assembler(self):
pass
def get_channels(self):
self.auth._check_sid()
url = ('%s%s:%s/service?method=channel.list&sid=%s' %
("https://" if self.config["proxy"]["ssl"] else "http://",
self.config["proxy"]["address"],
str(self.config["proxy"]["port"]),
self.auth.config['sid']
))
r = urllib.request.urlopen(url)
data_dict = xmltodict.parse(r)
if 'channels' not in list(data_dict['rsp'].keys()):
print("could not retrieve channel list")
return []
channel_o_list = data_dict['rsp']['channels']['channel']
channel_list = []
for c in channel_o_list:
dString = json.dumps(c)
channel_dict = eval(dString)
clean_station_item = {
"name": channel_dict["name"],
"callsign": channel_dict["name"],
"number": channel_dict["formatted-number"],
"id": channel_dict["id"],
}
channel_list.append(clean_station_item)
return channel_list
def get_station_list(self, base_url):
station_list = []
for c in self.get_channels():
if self.config["fakehdhr"]["stream_type"] == "ffmpeg":
watchtype = "ffmpeg"
else:
watchtype = "direct"
url = ('%s%s/watch?method=%s&channel=%s' %
("http://",
base_url,
watchtype,
c['number']
))
station_list.append(
{
'GuideNumber': str(c['number']),
'GuideName': c['name'],
'URL': url
})
return station_list
def get_station_total(self):
total_channels = 0
for c in self.get_channels():
total_channels += 1
return total_channels
def get_channel_stream(self, id):
url = ('%s%s:%s/live?channel=%s&client=%s' %
("https://" if self.config["proxy"]["ssl"] else "http://",
self.config["proxy"]["address"],
str(self.config["proxy"]["port"]),
str(id),
str(id),
))
return url
def get_channel_streams(self):
streamdict = {}
for c in self.get_channels():
url = ('%s%s:%s/live?channel=%s&client=%s' %
("https://" if self.config["proxy"]["ssl"] else "http://",
self.config["proxy"]["address"],
str(self.config["proxy"]["port"]),
str(c["number"]),
str(c["number"]),
))
streamdict[str(c["number"])] = url
return streamdict
def get_channel_thumbnail(self, channel_id):
channel_thumb_url = ("%s%s:%s/service?method=channel.icon&channel_id=%s" %
("https://" if self.config["proxy"]["ssl"] else "http://",
self.config["proxy"]["address"],
str(self.config["proxy"]["port"]),
str(channel_id)
))
return channel_thumb_url
def get_content_thumbnail(self, content_id):
self.auth._check_sid()
item_thumb_url = ("%s%s:%s/service?method=channel.show.artwork&sid=%s&event_id=%s" %
("https://" if self.config["proxy"]["ssl"] else "http://",
self.config["proxy"]["address"],
str(self.config["proxy"]["port"]),
self.auth.config['sid'],
str(content_id)
))
return item_thumb_url
def update_epg(self):
print('Updating NextPVR EPG cache file.')
self.auth._check_sid()
programguide = {}
for c in self.get_channels():
cdict = xmldictmaker(c, ["callsign", "name", "number", "id"])
if str(cdict['number']) not in list(programguide.keys()):
programguide[str(cdict['number'])] = {
"callsign": cdict["callsign"],
"name": cdict["name"] or cdict["callsign"],
"number": cdict["number"],
"id": cdict["id"],
"thumbnail": ("/images?source=proxy&type=channel&id=%s" % (str(cdict['id']))),
"listing": [],
}
epg_url = ('%s%s:%s/service?method=channel.listings&channel_id=%s' %
("https://" if self.config["proxy"]["ssl"] else "http://",
self.config["proxy"]["address"],
str(self.config["proxy"]["port"]),
str(cdict["id"]),
))
epg_req = urllib.request.urlopen(epg_url)
epg_dict = xmltodict.parse(epg_req)
for program_listing in epg_dict["rsp"]["listings"]:
for program_item in epg_dict["rsp"]["listings"][program_listing]:
if not isinstance(program_item, str):
progdict = xmldictmaker(program_item, ["start", "end", "title", "name", "subtitle", "rating", "description", "season", "episode", "id", "episodeTitle"])
clean_prog_dict = {
"time_start": xmltimestamp_nextpvr(progdict["start"]),
"time_end": xmltimestamp_nextpvr(progdict["end"]),
"duration_minutes": duration_nextpvr_minutes(progdict["start"], progdict["end"]),
"thumbnail": ("/images?source=proxy&type=content&id=%s" % (str(progdict['id']))),
"title": progdict['name'] or "Unavailable",
"sub-title": progdict['subtitle'] or "Unavailable",
"description": progdict['description'] or "Unavailable",
"rating": progdict['rating'] or "N/A",
"episodetitle": progdict['episodeTitle'],
"releaseyear": None,
"genres": [],
"seasonnumber": progdict['season'],
"episodenumber": progdict['episode'],
"isnew": False,
"id": progdict['id'] or xmltimestamp_nextpvr(progdict["start"]),
}
if 'genre' in list(progdict.keys()):
clean_prog_dict["genres"] = progdict['genre'].split(",")
if clean_prog_dict['sub-title'].startswith("Movie:"):
clean_prog_dict['releaseyear'] = clean_prog_dict['sub-title'].split("Movie: ")[-1]
clean_prog_dict['sub-title'] = "Unavailable"
clean_prog_dict["genres"].append("Movie")
# TODO isNEW
programguide[str(cdict["number"])]["listing"].append(clean_prog_dict)
for cnum in programguide:
programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start'])
self.epg_cache = programguide
with open(self.epg_cache_file, 'w') as epgfile:
epgfile.write(json.dumps(programguide, indent=4))
print('Wrote updated NextPVR EPG cache file.')
return programguide