From a831a07a59e1db506478ef8db0b943128a757c5e Mon Sep 17 00:00:00 2001 From: deathbybandaid Date: Wed, 7 Oct 2020 09:43:55 -0400 Subject: [PATCH] Better Loading process and Code Cleanup --- Dockerfile | 2 +- README.md | 5 +- config.all.ini | 4 +- config.example.ini | 4 +- .../{fakehdhr.ini => fhdhr.ini} | 14 +- data/internal_config/serviceconf.ini | 20 +- data/www/images/default-channel-thumb.png | Bin 1413 -> 0 bytes data/www/images/default-content-thumb.png | Bin 1413 -> 0 bytes epghandler/__init__.py | 139 -------- epghandler/epgtypes/__init__.py | 28 -- fHDHR/__init__.py | 2 + fHDHR/cli/__init__.py | 0 fHDHR/cli/run.py | 96 ++++++ fHDHR/config/__init__.py | 158 +++++++++ fHDHR/epghandler/__init__.py | 35 ++ fHDHR/epghandler/epgtypes/__init__.py | 58 ++++ .../epghandler/epgtypes/blocks.py | 42 +-- .../epghandler}/epgtypes/zap2it.py | 184 ++++------- fHDHR/epghandler/xmltv.py | 110 +++++++ fHDHR/fHDHRerrors/__init__.py | 32 ++ {fakehdhr => fHDHR/fHDHRweb}/__init__.py | 196 ++++++----- fHDHR/originservice/__init__.py | 89 +++++ fHDHR/originservice/nextpvr.py | 198 ++++++++++++ {ssdpserver => fHDHR/ssdpserver}/__init__.py | 11 +- fHDHR/tools/__init__.py | 81 +++++ fhdhrconfig/__init__.py | 178 ---------- main.py | 74 +---- proxyservice/__init__.py | 304 ------------------ 28 files changed, 1085 insertions(+), 979 deletions(-) rename data/internal_config/{fakehdhr.ini => fhdhr.ini} (58%) delete mode 100644 data/www/images/default-channel-thumb.png delete mode 100644 data/www/images/default-content-thumb.png delete mode 100644 epghandler/__init__.py delete mode 100644 epghandler/epgtypes/__init__.py create mode 100644 fHDHR/__init__.py create mode 100644 fHDHR/cli/__init__.py create mode 100644 fHDHR/cli/run.py create mode 100644 fHDHR/config/__init__.py create mode 100644 fHDHR/epghandler/__init__.py create mode 100644 fHDHR/epghandler/epgtypes/__init__.py rename epghandler/epgtypes/empty.py => fHDHR/epghandler/epgtypes/blocks.py (67%) rename {epghandler => fHDHR/epghandler}/epgtypes/zap2it.py (54%) create mode 100644 fHDHR/epghandler/xmltv.py create mode 100644 fHDHR/fHDHRerrors/__init__.py rename {fakehdhr => fHDHR/fHDHRweb}/__init__.py (64%) create mode 100644 fHDHR/originservice/__init__.py create mode 100644 fHDHR/originservice/nextpvr.py rename {ssdpserver => fHDHR/ssdpserver}/__init__.py (93%) create mode 100644 fHDHR/tools/__init__.py delete mode 100644 fhdhrconfig/__init__.py delete mode 100644 proxyservice/__init__.py diff --git a/Dockerfile b/Dockerfile index 4ea8a97..3c7d79c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,4 @@ COPY ./ /app/ WORKDIR /app 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"] diff --git a/README.md b/README.md index 7ea2201..cd19703 100644 --- a/README.md +++ b/README.md @@ -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: +**f**un **H**ome **D**istribution -**H**iatus +**H**iatus & **R**ecreation diff --git a/config.all.ini b/config.all.ini index af3d0f3..b1367e6 100644 --- a/config.all.ini +++ b/config.all.ini @@ -13,14 +13,14 @@ # weight = 300 # epg_update_frequency = 43200 -[fakehdhr] +[fhdhr] # address = 0.0.0.0 # port = 5004 # discovery_address = 0.0.0.0 # tuner_count = 3 # friendlyname = fHDHR-Locast # stream_type = ffmpeg -# epg_method = proxy +# epg_method = origin [zap2it] # delay = 5 diff --git a/config.example.ini b/config.example.ini index 2eec403..c16fdb3 100644 --- a/config.example.ini +++ b/config.example.ini @@ -1,9 +1,9 @@ [nextpvr] address = localhost port = 8866 -pin = +pin = -[fakehdhr] +[fhdhr] # address = 0.0.0.0 # port = 5004 # discovery_address = 0.0.0.0 diff --git a/data/internal_config/fakehdhr.ini b/data/internal_config/fhdhr.ini similarity index 58% rename from data/internal_config/fakehdhr.ini rename to data/internal_config/fhdhr.ini index edb9cb5..8114878 100644 --- a/data/internal_config/fakehdhr.ini +++ b/data/internal_config/fhdhr.ini @@ -1,11 +1,11 @@ [main] -uuid = fHDHR_None -cache_dir = fHDHR_None +uuid = +cache_dir = -[empty] +[blocks] epg_update_frequency = 43200 -[fakehdhr] +[fhdhr] address = 0.0.0.0 port = 5004 discovery_address = 0.0.0.0 @@ -18,7 +18,7 @@ bytes_per_read = 1152000 chunksize = 1048576 [dev] -reporting_model = HDHR4-2DT -reporting_firmware_name = hdhomerun4_dvbt -reporting_firmware_ver = 20150826 +reporting_manufacturer = BoronDust +reporting_model = fHDHR +reporting_firmware_ver = 20201001 reporting_tuner_type = Antenna diff --git a/data/internal_config/serviceconf.ini b/data/internal_config/serviceconf.ini index e0dbd87..c6bc553 100644 --- a/data/internal_config/serviceconf.ini +++ b/data/internal_config/serviceconf.ini @@ -1,27 +1,31 @@ +[dev] +reporting_firmware_name = fHDHR_NextPVR + [main] servicename = NextPVR dictpopname = nextpvr -credentials = pin -reponame = FakeHDHR_NextPVR +reponame = fHDHR_NextPVR +required = nextpvr/pin +valid_epg_methods = None,blocks,origin,zap2it -[fakehdhr] +[fhdhr] friendlyname = fHDHR-NextPVR stream_type = direct -epg_method = proxy +epg_method = origin tuner_count = 4 [nextpvr] address = localhost port = 8866 -ssl = fHDHR_False -pin = fHDHR_None +ssl = False +pin = weight = 300 -sidfile = fHDHR_None epg_update_frequency = 43200 +sid = [zap2it] delay = 5 -postalcode = fHDHR_None +postalcode = affiliate_id = gapzap country = USA device = - diff --git a/data/www/images/default-channel-thumb.png b/data/www/images/default-channel-thumb.png deleted file mode 100644 index b228b0c6682fcfeed64f4d4102c9adaccae91f4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1413 zcmeAS@N?(olHy`uVBq!ia0vp^Yk;_sgAGW=G6xy}DaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheoCO|{#S9F5M?jcysy3fA0|Tp+r;B4qMcmuF_r33>${hRnyKq_9t@mrz ztZ>to*;;k(S7E@yfOOYQ7aQx3m2LRT7;W&sOVLqy6UU;XVXwtq)Scu!SX~{pmndCv zIJ>p%`(C>O`|m|n0l#9t)E%7ux>%w0-J{2EzyG|tt0You&6@QYUN7H-r<6Q&U7OdR3YvU@;9hV&5H!M5$;=_t%Q`Bxxo+Xw1`4QNG0 zhv?lUbsp1!=Gay)o+f_5_f_4d#KhJ^hqzi>MY*`Ofl4*NCN12s!HL=uUfJ(gS6kan zS-kr1N*;Ud&GJ(h{<&2><)K+>hUUeuH^4R?eeLmKtx?UoGkM`{pOk;R4{A$H{oy&Y zSL1N$skJd7ZTV9c{;9vWf~Q_%^ZO}@zLKXFpEEsG&}_82UheDlSxJY%9$Bw^(E3!t zUZZL!$*QT5wc-!hQ-4Ivv|Y~gwJ^!}>-u-sr#|#d-R3V@7o3)_>HO0+?dkW>dcK6p zEjv6VPtEtQjWIaX)+)N5*LpF}cg@Yrr|N_L9}LXD@%dDakL0_syruWb)@vTVeCkbN z>W*7&wSkhS&PM(}t*f=!ALt=n<8?gFUd%ZW5x^jPRI8%#zLrnxuw3f54x=mg3(aLY z?}fBYpR({@a+dLat;5x)azZ5Kf~6854u7xgSlPejpK0`}|6F?iXPvtjp7!-_TlAFZ zn*nY8Qxcc=vpluE`9?T(+Y+9yb362o0bL1{BD7cgo(^J}jAPwt(m9-L};$ z>$|=>Z?&#fSuy|3znMm>_x}%{!UhcELv3g60@D0756hlfdqw(dZt9JfywU#!BkV$c zcN^_lV{!murij_;^C?~0`&E|h`E=TETk+HCy-&B^{v^0`&nNAk$EWm}we*#tiRDJ# z^!LlBh1a!3#r%AD{nM|#JD#mtZ6o(&x5(4f7GTQyp13)$%I=Nyse=7RQ{Hdiu#V^H z->X%kw(HKk`oHYrEkv9(8QocP=3Pjh``?-ipWZgg1+@KIYCe1A{Z$!plJ7#(wre^6 zT>aNOH6pO>lc(-=Ag|2h>*tEo>(}02evm^NoN_Bp(=s%_*fQ^BUb&|s zFX;PZP~Pke7M7JiWmv9QKS}R`>@DTF^G}|FI0l?;l@D$-K5nvf%eIO3G4eMeYpZ{m zEG>D|Dm+UHl3XrDMa90d5b~+DSgN^n=1)mq-ad{W`cvE+-dt#Oo)u*E>5U*br*F}V zUG_8oboObW*!QoNzP&GZX8LOC>gZ);t;t)H#k4NOzRSwWW|ijBli!vsJNCo*@vI=J kH_}(GK$6+SDgT(b&akZIznVG+SadRYy85}Sb4q9e0JS}npa1{> diff --git a/data/www/images/default-content-thumb.png b/data/www/images/default-content-thumb.png deleted file mode 100644 index b228b0c6682fcfeed64f4d4102c9adaccae91f4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1413 zcmeAS@N?(olHy`uVBq!ia0vp^Yk;_sgAGW=G6xy}DaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheoCO|{#S9F5M?jcysy3fA0|Tp+r;B4qMcmuF_r33>${hRnyKq_9t@mrz ztZ>to*;;k(S7E@yfOOYQ7aQx3m2LRT7;W&sOVLqy6UU;XVXwtq)Scu!SX~{pmndCv zIJ>p%`(C>O`|m|n0l#9t)E%7ux>%w0-J{2EzyG|tt0You&6@QYUN7H-r<6Q&U7OdR3YvU@;9hV&5H!M5$;=_t%Q`Bxxo+Xw1`4QNG0 zhv?lUbsp1!=Gay)o+f_5_f_4d#KhJ^hqzi>MY*`Ofl4*NCN12s!HL=uUfJ(gS6kan zS-kr1N*;Ud&GJ(h{<&2><)K+>hUUeuH^4R?eeLmKtx?UoGkM`{pOk;R4{A$H{oy&Y zSL1N$skJd7ZTV9c{;9vWf~Q_%^ZO}@zLKXFpEEsG&}_82UheDlSxJY%9$Bw^(E3!t zUZZL!$*QT5wc-!hQ-4Ivv|Y~gwJ^!}>-u-sr#|#d-R3V@7o3)_>HO0+?dkW>dcK6p zEjv6VPtEtQjWIaX)+)N5*LpF}cg@Yrr|N_L9}LXD@%dDakL0_syruWb)@vTVeCkbN z>W*7&wSkhS&PM(}t*f=!ALt=n<8?gFUd%ZW5x^jPRI8%#zLrnxuw3f54x=mg3(aLY z?}fBYpR({@a+dLat;5x)azZ5Kf~6854u7xgSlPejpK0`}|6F?iXPvtjp7!-_TlAFZ zn*nY8Qxcc=vpluE`9?T(+Y+9yb362o0bL1{BD7cgo(^J}jAPwt(m9-L};$ z>$|=>Z?&#fSuy|3znMm>_x}%{!UhcELv3g60@D0756hlfdqw(dZt9JfywU#!BkV$c zcN^_lV{!murij_;^C?~0`&E|h`E=TETk+HCy-&B^{v^0`&nNAk$EWm}we*#tiRDJ# z^!LlBh1a!3#r%AD{nM|#JD#mtZ6o(&x5(4f7GTQyp13)$%I=Nyse=7RQ{Hdiu#V^H z->X%kw(HKk`oHYrEkv9(8QocP=3Pjh``?-ipWZgg1+@KIYCe1A{Z$!plJ7#(wre^6 zT>aNOH6pO>lc(-=Ag|2h>*tEo>(}02evm^NoN_Bp(=s%_*fQ^BUb&|s zFX;PZP~Pke7M7JiWmv9QKS}R`>@DTF^G}|FI0l?;l@D$-K5nvf%eIO3G4eMeYpZ{m zEG>D|Dm+UHl3XrDMa90d5b~+DSgN^n=1)mq-ad{W`cvE+-dt#Oo)u*E>5U*br*F}V zUG_8oboObW*!QoNzP&GZX8LOC>gZ);t;t)H#k4NOzRSwWW|ijBli!vsJNCo*@vI=J kH_}(GK$6+SDgT(b&akZIznVG+SadRYy85}Sb4q9e0JS}npa1{> diff --git a/epghandler/__init__.py b/epghandler/__init__.py deleted file mode 100644 index 28834dd..0000000 --- a/epghandler/__init__.py +++ /dev/null @@ -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'\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'\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() diff --git a/epghandler/epgtypes/__init__.py b/epghandler/epgtypes/__init__.py deleted file mode 100644 index 3ee71a1..0000000 --- a/epghandler/epgtypes/__init__.py +++ /dev/null @@ -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() diff --git a/fHDHR/__init__.py b/fHDHR/__init__.py new file mode 100644 index 0000000..60f4d33 --- /dev/null +++ b/fHDHR/__init__.py @@ -0,0 +1,2 @@ +# coding=utf-8 +fHDHR_VERSION = "v0.2.0-beta" diff --git a/fHDHR/cli/__init__.py b/fHDHR/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fHDHR/cli/run.py b/fHDHR/cli/run.py new file mode 100644 index 0000000..eabcd78 --- /dev/null +++ b/fHDHR/cli/run.py @@ -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() diff --git a/fHDHR/config/__init__.py b/fHDHR/config/__init__.py new file mode 100644 index 0000000..464446a --- /dev/null +++ b/fHDHR/config/__init__.py @@ -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.") diff --git a/fHDHR/epghandler/__init__.py b/fHDHR/epghandler/__init__.py new file mode 100644 index 0000000..3406e3b --- /dev/null +++ b/fHDHR/epghandler/__init__.py @@ -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 diff --git a/fHDHR/epghandler/epgtypes/__init__.py b/fHDHR/epghandler/epgtypes/__init__.py new file mode 100644 index 0000000..10cd60d --- /dev/null +++ b/fHDHR/epghandler/epgtypes/__init__.py @@ -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.") diff --git a/epghandler/epgtypes/empty.py b/fHDHR/epghandler/epgtypes/blocks.py similarity index 67% rename from epghandler/epgtypes/empty.py rename to fHDHR/epghandler/epgtypes/blocks.py index df90a33..027f37f 100644 --- a/epghandler/epgtypes/empty.py +++ b/fHDHR/epghandler/epgtypes/blocks.py @@ -1,38 +1,13 @@ -import os -import json import datetime -class EmptyEPG(): +class BlocksEPG(): - def __init__(self, config, serviceproxy): - - self.config = config.copy() - 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 __init__(self, settings, origserv): + self.config = settings + self.origserv = origserv def update_epg(self): - print('Updating Empty EPG cache file.') - programguide = {} timestamps = [] @@ -53,7 +28,7 @@ class EmptyEPG(): } 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()): programguide[str(c["number"])] = { "callsign": c["callsign"], @@ -85,11 +60,4 @@ class EmptyEPG(): 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 diff --git a/epghandler/epgtypes/zap2it.py b/fHDHR/epghandler/epgtypes/zap2it.py similarity index 54% rename from epghandler/epgtypes/zap2it.py rename to fHDHR/epghandler/epgtypes/zap2it.py index 09a22d7..b9ff3cd 100644 --- a/epghandler/epgtypes/zap2it.py +++ b/fHDHR/epghandler/epgtypes/zap2it.py @@ -1,141 +1,62 @@ -import os import json import time import datetime -import urllib.error import urllib.parse -import urllib.request - -def xmltimestamp_zap(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 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 +from fHDHR.tools import xmldictmaker, WebReq +from fHDHR.fHDHRerrors import EPGSetupError 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.serviceproxy = serviceproxy + self.postalcode = self.config.dict["zap2it"]["postalcode"] - self.postalcode = None - - 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() + self.web_cache_dir = self.config.dict["filedir"]["epg_cache"]["zap2it"]["web_cache"] def get_location(self): - self.postalcode = self.config["zap2it"]["postalcode"] - if 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) + print("Zap2it postalcode not set, attempting to retrieve.") + if not self.postalcode: try: - resp = urllib.request.urlopen(url) - result = resp.read() - except urllib.error.HTTPError as e: - if e.code == 400: - 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 + postalcode_url = 'http://ipinfo.io/json' + postalcode_req = self.web.session.get(postalcode_url) + data = postalcode_req.json() + self.postalcode = data["postal"] except Exception as e: - print(e) - pass - print('Removing stale cache file:', p.name) - p.unlink() + raise EPGSetupError("Unable to automatically optain zap2it postalcode: " + str(e)) + return self.postalcode def update_epg(self): - print('Updating Zap2it EPG cache file.') programguide = {} - self.get_location() - # Start time parameter is now rounded down to nearest `zap_timespan`, in s. 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)) self.remove_stale_cache(zap_time) # 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) parameters = { - 'aid': self.config["zap2it"]['affiliate_id'], - 'country': self.config["zap2it"]['country'], - 'device': self.config["zap2it"]['device'], - 'headendId': self.config["zap2it"]['headendid'], + 'aid': self.config.dict["zap2it"]['affiliate_id'], + 'country': self.config.dict["zap2it"]['country'], + 'device': self.config.dict["zap2it"]['device'], + 'headendId': self.config.dict["zap2it"]['headendid'], 'isoverride': "true", - 'languagecode': self.config["zap2it"]['languagecode'], + 'languagecode': self.config.dict["zap2it"]['languagecode'], 'pref': 'm,p', - 'timespan': self.config["zap2it"]['timespan'], - 'timezone': self.config["zap2it"]['timezone'], - 'userId': self.config["zap2it"]['userid'], - 'postalCode': self.postalcode, - 'lineupId': '%s-%s-DEFAULT' % (self.config["zap2it"]['country'], self.config["zap2it"]['device']), + 'timespan': self.config.dict["zap2it"]['timespan'], + 'timezone': self.config.dict["zap2it"]['timezone'], + 'userId': self.config.dict["zap2it"]['userid'], + 'postalCode': str(self.postalcode or self.get_location()), + 'lineupId': '%s-%s-DEFAULT' % (self.config.dict["zap2it"]['country'], self.config.dict["zap2it"]['device']), 'time': i_time, 'Activity_ID': 1, 'FromPage': "TV%20Guide", @@ -144,7 +65,7 @@ class ZapEPG(): url = 'https://tvlistings.zap2it.com/api/grid?' 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) for c in d['channels']: @@ -168,8 +89,8 @@ class ZapEPG(): progdict = xmldictmaker(event['program'], ["title", "sub-title", "releaseYear", "episodeTitle", "shortDesc", "season", "episode", "id"]) clean_prog_dict = { - "time_start": xmltimestamp_zap(eventdict['startTime']), - "time_end": xmltimestamp_zap(eventdict['endTime']), + "time_start": self.xmltimestamp_zap(eventdict['startTime']), + "time_end": self.xmltimestamp_zap(eventdict['endTime']), "duration_minutes": eventdict['duration'], "thumbnail": str("https://zap2it.tmsimg.com/assets/" + str(eventdict['thumbnail']) + ".jpg"), "title": progdict['title'] or "Unavailable", @@ -182,7 +103,7 @@ class ZapEPG(): "seasonnumber": progdict['season'], "episodenumber": progdict['episode'], "isnew": False, - "id": progdict['id'] or xmltimestamp_zap(eventdict['startTime']), + "id": progdict['id'] or self.xmltimestamp_zap(eventdict['startTime']), } for f in eventdict['filter']: @@ -198,11 +119,38 @@ class ZapEPG(): 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 + + 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() diff --git a/fHDHR/epghandler/xmltv.py b/fHDHR/epghandler/xmltv.py new file mode 100644 index 0000000..8d8a0b3 --- /dev/null +++ b/fHDHR/epghandler/xmltv.py @@ -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'\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) diff --git a/fHDHR/fHDHRerrors/__init__.py b/fHDHR/fHDHRerrors/__init__.py new file mode 100644 index 0000000..a44cf38 --- /dev/null +++ b/fHDHR/fHDHRerrors/__init__.py @@ -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 diff --git a/fakehdhr/__init__.py b/fHDHR/fHDHRweb/__init__.py similarity index 64% rename from fakehdhr/__init__.py rename to fHDHR/fHDHRweb/__init__.py index b8abf1b..8352220 100644 --- a/fakehdhr/__init__.py +++ b/fHDHR/fHDHRweb/__init__.py @@ -4,7 +4,6 @@ from flask import (Flask, send_from_directory, request, Response, from io import BytesIO import xml.etree.ElementTree as ET import json -import time import requests import subprocess import threading @@ -28,7 +27,7 @@ def getSize(txt, font): class HDHR_Hub(): config = None - serviceproxy = None + origserv = None epghandling = None station_scan = False station_list = [] @@ -38,11 +37,11 @@ class HDHR_Hub(): self.tuner_lock = threading.Lock() self.tuners = 0 - def hubprep(self, config, serviceproxy, epghandling): + def hubprep(self, config, origserv, epghandling): 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.serviceproxy = serviceproxy + self.origserv = origserv self.epghandling = epghandling def tuner_usage(self, number): @@ -62,45 +61,55 @@ class HDHR_Hub(): def get_xmltv(self, 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): - 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": - if req_args["type"] == "channel": - imageUri = self.serviceproxy.get_channel_thumbnail(imageid) - elif req_args["type"] == "content": - imageUri = self.serviceproxy.get_content_thumbnail(imageid) + try: req = requests.get(imageUri) return req.content + except Exception as e: + print(e) + return self.generate_image(req_args["type"], req_args["id"]) - elif req_args["source"] == "empty": - if req_args["type"] == "channel": - width = 360 - height = 270 - text = req_args["id"] - fontsize = 72 - elif req_args["type"] == "content": - width = 1080 - height = 1440 - fontsize = 100 - 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_image_type(self, image_data): + header_byte = image_data[0:3].hex().lower() + if header_byte == '474946': + return "image/gif" + elif header_byte == '89504e': + return "image/png" + elif header_byte == 'ffd8ff': + return "image/jpeg" + else: + return "image/jpeg" def get_xmldiscover(self, base_url): out = ET.Element('root') @@ -114,12 +123,12 @@ class HDHR_Hub(): device_out = sub_el(out, 'device') 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, 'manufacturer', "Silicondust") - sub_el(device_out, 'modelName', self.config["dev"]["reporting_model"]) - sub_el(device_out, 'modelNumber', self.config["dev"]["reporting_model"]) + sub_el(device_out, 'friendlyName', self.config.dict["fhdhr"]["friendlyname"]) + sub_el(device_out, 'manufacturer', self.config.dict["dev"]["reporting_manufacturer"]) + sub_el(device_out, 'modelName', self.config.dict["dev"]["reporting_model"]) + sub_el(device_out, 'modelNumber', self.config.dict["dev"]["reporting_model"]) 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.write(b'\n') @@ -128,13 +137,13 @@ class HDHR_Hub(): def get_discover_json(self, base_url): jsondiscover = { - "FriendlyName": self.config["fakehdhr"]["friendlyname"], - "Manufacturer": "Silicondust", - "ModelNumber": self.config["dev"]["reporting_model"], - "FirmwareName": self.config["dev"]["reporting_firmware_name"], - "TunerCount": self.config["fakehdhr"]["tuner_count"], - "FirmwareVersion": self.config["dev"]["reporting_firmware_ver"], - "DeviceID": self.config["main"]["uuid"], + "FriendlyName": self.config.dict["fhdhr"]["friendlyname"], + "Manufacturer": "Borondust", + "ModelNumber": self.config.dict["dev"]["reporting_model"], + "FirmwareName": self.config.dict["dev"]["reporting_firmware_name"], + "TunerCount": self.config.dict["fhdhr"]["tuner_count"], + "FirmwareVersion": self.config.dict["dev"]["reporting_firmware_ver"], + "DeviceID": self.config.dict["main"]["uuid"], "DeviceAuth": "fHDHR", "BaseURL": "http://" + base_url, "LineupURL": "http://" + base_url + "/lineup.json" @@ -143,7 +152,7 @@ class HDHR_Hub(): def get_lineup_status(self): if self.station_scan: - channel_count = self.serviceproxy.get_station_total() + channel_count = self.origserv.get_station_total() jsonlineup = { "ScanInProgress": "true", "Progress": 99, @@ -153,14 +162,14 @@ class HDHR_Hub(): jsonlineup = { "ScanInProgress": "false", "ScanPossible": "true", - "Source": self.config["dev"]["reporting_tuner_type"], - "SourceList": [self.config["dev"]["reporting_tuner_type"]], + "Source": self.config.dict["dev"]["reporting_tuner_type"], + "SourceList": [self.config.dict["dev"]["reporting_tuner_type"]], } return jsonlineup def get_lineup_xml(self, base_url): 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: program_out = sub_el(out, 'Program') sub_el(program_out, 'GuideNumber', station_item['GuideNumber']) @@ -199,11 +208,11 @@ class HDHR_HTTP_Server(): @app.route('/') def root_path(): - return hdhr.config["fakehdhr"]["friendlyname"] + return hdhr.config.dict["fhdhr"]["friendlyname"] @app.route('/favicon.ico', methods=['GET']) def favicon(): - return send_from_directory(hdhr.config["main"]["www_dir"], + return send_from_directory(hdhr.config.dict["filedir"]["www_dir"], 'favicon.ico', mimetype='image/vnd.microsoft.icon') @@ -241,7 +250,7 @@ class HDHR_HTTP_Server(): @app.route('/lineup.json', methods=['GET']) def lineup_json(): 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, response=json.dumps(station_list, indent=4), mimetype='application/json') @@ -263,12 +272,36 @@ class HDHR_HTTP_Server(): mimetype='application/json') @app.route('/images', methods=['GET']) - def images_nothing(): - 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) + def images(): - image = hdhr.get_image(request.args) - return Response(image, content_type='image/png', direct_passthrough=True) + if 'source' not in list(request.args.keys()): + 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']) def watch(): @@ -280,27 +313,24 @@ class HDHR_HTTP_Server(): tuner = hdhr.get_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) - 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": - duration = request.args.get('duration', default=0, type=int) - - if not duration == 0: - duration += time.time() + chunksize = int(hdhr.config.dict["direct_stream"]['chunksize']) req = requests.get(channelUri, stream=True) - hdhr.tuner_usage(1) def generate(): try: - yield '' - 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 + for chunk in req.iter_content(chunk_size=chunksize): yield chunk except GeneratorExit: req.close() @@ -311,7 +341,9 @@ class HDHR_HTTP_Server(): 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, "-c", "copy", "-f", "mpegts", @@ -321,12 +353,11 @@ class HDHR_HTTP_Server(): ] ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE) - hdhr.tuner_usage(1) def generate(): try: 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: break try: @@ -366,12 +397,12 @@ class HDHR_HTTP_Server(): return Response(status=200, response=currenthtmlerror, mimetype='text/html') def __init__(self, config): - self.config = config.copy() + self.config = config def run(self): self.http = WSGIServer(( - self.config["fakehdhr"]["address"], - int(self.config["fakehdhr"]["port"]) + self.config.dict["fhdhr"]["address"], + int(self.config.dict["fhdhr"]["port"]) ), self.app.wsgi_app) try: self.http.serve_forever() @@ -379,7 +410,8 @@ class HDHR_HTTP_Server(): self.http.stop() -def interface_start(config, serviceproxy, epghandling): - hdhr.hubprep(config, serviceproxy, epghandling) +def interface_start(config, origserv, epghandling): + print("Starting fHDHR Web Interface") + hdhr.hubprep(config, origserv, epghandling) fakhdhrserver = HDHR_HTTP_Server(config) fakhdhrserver.run() diff --git a/fHDHR/originservice/__init__.py b/fHDHR/originservice/__init__.py new file mode 100644 index 0000000..92355d5 --- /dev/null +++ b/fHDHR/originservice/__init__.py @@ -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() diff --git a/fHDHR/originservice/nextpvr.py b/fHDHR/originservice/nextpvr.py new file mode 100644 index 0000000..13e3cff --- /dev/null +++ b/fHDHR/originservice/nextpvr.py @@ -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) diff --git a/ssdpserver/__init__.py b/fHDHR/ssdpserver/__init__.py similarity index 93% rename from ssdpserver/__init__.py rename to fHDHR/ssdpserver/__init__.py index 1f2c390..728767b 100644 --- a/ssdpserver/__init__.py +++ b/fHDHR/ssdpserver/__init__.py @@ -24,14 +24,17 @@ logger = logging.getLogger() # mostly from https://github.com/ZeWaren/python-upnp-ssdp-example -def ssdpServerProcess(config): +def ssdpServerProcess(settings): ssdp = SSDPServer() ssdp.ssdp_port = 1900 ssdp.register('local', - 'uuid:' + config["main"]["uuid"] + '::upnp:rootdevice', + 'uuid:' + settings.dict["main"]["uuid"] + '::upnp:rootdevice', 'upnp:rootdevice', - 'http://' + config["fakehdhr"]["discovery_address"] + ':' + - config["fakehdhr"]["port"] + '/device.xml') + 'http://' + settings.dict["fhdhr"]["discovery_address"] + ':' + + 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: ssdp.run() except KeyboardInterrupt: diff --git a/fHDHR/tools/__init__.py b/fHDHR/tools/__init__.py new file mode 100644 index 0000000..e5e99f8 --- /dev/null +++ b/fHDHR/tools/__init__.py @@ -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() diff --git a/fhdhrconfig/__init__.py b/fhdhrconfig/__init__.py deleted file mode 100644 index 06d3658..0000000 --- a/fhdhrconfig/__init__.py +++ /dev/null @@ -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() diff --git a/main.py b/main.py index 25346ba..3f55775 100644 --- a/main.py +++ b/main.py @@ -1,74 +1,14 @@ +#!/usr/bin/env python3 +# coding=utf-8 import os import sys import pathlib -import argparse -from multiprocessing import Process +from multiprocessing import freeze_support -import fhdhrconfig -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) +from fHDHR.cli import run +SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) if __name__ == '__main__': - - # Gather args - 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() + freeze_support() + sys.exit(run.main(SCRIPT_DIR)) diff --git a/proxyservice/__init__.py b/proxyservice/__init__.py deleted file mode 100644 index eeb8b98..0000000 --- a/proxyservice/__init__.py +++ /dev/null @@ -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