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

Merge pull request #47 from deathbybandaid/dev

EPG and VLC and Tuner Improvements
This commit is contained in:
Deathbybandaid 2020-11-11 12:47:33 -05:00 committed by GitHub
commit 43493258e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 670 additions and 342 deletions

View File

@ -20,6 +20,10 @@ images = pass
ffmpeg_path = ffmpeg
bytes_per_read = 1152000
[vlc]
vlc_path = cvlc
bytes_per_read = 1152000
[direct_stream]
chunksize = 1048576

View File

@ -8,7 +8,7 @@ import fHDHR.tools
fHDHR_VERSION = "v0.4.0-beta"
class fHDHR_OBJ():
class fHDHR_INT_OBJ():
def __init__(self, settings, logger, db):
self.version = fHDHR_VERSION
@ -18,6 +18,17 @@ class fHDHR_OBJ():
self.web = fHDHR.tools.WebReq()
self.origin = OriginServiceWrapper(settings, logger, self.web, db)
self.device = fHDHR_Device(settings, self.version, self.origin, logger, self.web, db)
class fHDHR_OBJ():
def __init__(self, settings, logger, db):
self.fhdhr = fHDHR_INT_OBJ(settings, logger, db)
self.origin = OriginServiceWrapper(self.fhdhr)
self.device = fHDHR_Device(self.fhdhr, self.origin)
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if hasattr(self.fhdhr, name):
return eval("self.fhdhr." + name)

View File

@ -156,7 +156,7 @@ class Config():
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"]:
if self.dict["fhdhr"]["stream_type"] not in ["direct", "ffmpeg", "vlc"]:
raise fHDHR.exceptions.ConfigurationError("Invalid stream type. Exiting...")
opersystem = platform.system()
@ -193,6 +193,24 @@ class Config():
else:
self.dict["ffmpeg"]["version"] = "N/A"
if self.dict["fhdhr"]["stream_type"] == "vlc":
try:
vlc_command = [self.dict["vlc"]["vlc_path"],
"--version",
"pipe:stdout"
]
vlc_proc = subprocess.Popen(vlc_command, stdout=subprocess.PIPE)
vlc_version = vlc_proc.stdout.read()
vlc_proc.terminate()
vlc_proc.communicate()
vlc_version = vlc_version.decode().split("version ")[1].split('\n')[0]
except FileNotFoundError:
vlc_version = None
self.dict["vlc"]["version"] = vlc_version
else:
self.dict["vlc"]["version"] = "N/A"
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":
@ -223,3 +241,8 @@ class Config():
# logger.addHandler(c_handler)
logger.addHandler(f_handler)
return logger
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if name in list(self.dict.keys()):
return self.dict[name]

View File

@ -10,20 +10,20 @@ from .cluster import fHDHR_Cluster
class fHDHR_Device():
def __init__(self, settings, fhdhr_version, origin, logger, web, db):
def __init__(self, fhdhr, origin):
self.channels = Channels(settings, origin, logger, db)
self.channels = Channels(fhdhr, origin)
self.epg = EPG(settings, self.channels, origin, logger, web, db)
self.epg = EPG(fhdhr, self.channels, origin)
self.tuners = Tuners(settings, self.epg, logger)
self.tuners = Tuners(fhdhr, self.epg)
self.watch = WatchStream(settings, self.channels, self.tuners, logger, web)
self.watch = WatchStream(fhdhr, self.channels, self.tuners)
self.images = imageHandler(settings, self.epg, logger, web)
self.images = imageHandler(fhdhr, self.epg)
self.station_scan = Station_Scan(settings, self.channels, logger, db)
self.station_scan = Station_Scan(fhdhr, self.channels)
self.ssdp = SSDPServer(settings, fhdhr_version, logger, db)
self.ssdp = SSDPServer(fhdhr)
self.cluster = fHDHR_Cluster(settings, self.ssdp, logger, db, web)
self.cluster = fHDHR_Cluster(fhdhr, self.ssdp)

View File

@ -6,13 +6,11 @@ from fHDHR.tools import hours_between_datetime
class ChannelNumbers():
def __init__(self, settings, logger, db):
self.config = settings
self.logger = logger
self.db = db
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def get_number(self, channel_id):
cnumbers = self.db.get_fhdhr_value("channel_numbers", "list") or {}
cnumbers = self.fhdhr.db.get_fhdhr_value("channel_numbers", "list") or {}
if channel_id in list(cnumbers.keys()):
return cnumbers[channel_id]
@ -26,20 +24,19 @@ class ChannelNumbers():
return str(float(i))
def set_number(self, channel_id, channel_number):
cnumbers = self.db.get_fhdhr_value("channel_numbers", "list") or {}
cnumbers = self.fhdhr.db.get_fhdhr_value("channel_numbers", "list") or {}
cnumbers[channel_id] = str(float(channel_number))
self.db.set_fhdhr_value("channel_numbers", "list", cnumbers)
self.fhdhr.db.set_fhdhr_value("channel_numbers", "list", cnumbers)
class Channels():
def __init__(self, settings, origin, logger, db):
self.config = settings
self.logger = logger
self.origin = origin
self.db = db
def __init__(self, fhdhr, origin):
self.fhdhr = fhdhr
self.channel_numbers = ChannelNumbers(settings, logger, db)
self.origin = origin
self.channel_numbers = ChannelNumbers(fhdhr)
self.list = {}
self.list_update_time = None
@ -72,7 +69,7 @@ class Channels():
channel_dict_list = self.verify_channel_info(channel_dict_list)
self.append_channel_info(channel_dict_list)
if not self.list_update_time:
self.logger.info("Found " + str(len(self.list)) + " channels for " + str(self.config.dict["main"]["servicename"]))
self.fhdhr.logger.info("Found " + str(len(self.list)) + " channels for " + str(self.fhdhr.config.dict["main"]["servicename"]))
self.list_update_time = datetime.datetime.now()
channel_list = []
@ -87,6 +84,7 @@ class Channels():
station_list.append({
'GuideNumber': c['number'],
'GuideName': c['name'],
'Tags': ",".join(c['tags']),
'URL': self.get_fhdhr_stream_url(base_url, c['number']),
})
return station_list
@ -123,15 +121,22 @@ class Channels():
"""Some Channel Information is Critical"""
cleaned_channel_dict_list = []
for station_item in channel_dict_list:
if "callsign" not in list(station_item.keys()):
station_item["callsign"] = station_item["name"]
if "id" not in list(station_item.keys()):
station_item["id"] = station_item["name"]
if "tags" not in list(station_item.keys()):
station_item["tags"] = []
if "number" not in list(station_item.keys()):
station_item["number"] = self.channel_numbers.get_number(station_item["id"])
else:
station_item["number"] = str(float(station_item["number"]))
self.channel_numbers.set_number(station_item["id"], station_item["number"])
cleaned_channel_dict_list.append(station_item)
return cleaned_channel_dict_list

View File

@ -4,28 +4,26 @@ from collections import OrderedDict
class fHDHR_Cluster():
def __init__(self, settings, ssdp, logger, db, web):
self.config = settings
self.logger = logger
self.ssdp = ssdp
self.db = db
self.web = web
def __init__(self, fhdhr, ssdp):
self.fhdhr = fhdhr
self.friendlyname = self.config.dict["fhdhr"]["friendlyname"]
self.ssdp = ssdp
self.friendlyname = self.fhdhr.config.dict["fhdhr"]["friendlyname"]
self.location = None
self.location_url = None
if settings.dict["fhdhr"]["discovery_address"]:
self.location = ('http://' + settings.dict["fhdhr"]["discovery_address"] + ':' +
str(settings.dict["fhdhr"]["port"]))
if fhdhr.config.dict["fhdhr"]["discovery_address"]:
self.location = ('http://' + fhdhr.config.dict["fhdhr"]["discovery_address"] + ':' +
str(fhdhr.config.dict["fhdhr"]["port"]))
self.location_url = urllib.parse.quote(self.location)
self.startup_sync()
def cluster(self):
return self.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
return self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
def get_list(self):
cluster = self.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
return_dict = {}
for location in list(cluster.keys()):
if location != self.location:
@ -51,90 +49,90 @@ class fHDHR_Cluster():
return defdict
def startup_sync(self):
cluster = self.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
for location in list(cluster.keys()):
if location != self.location:
sync_url = location + "/api/cluster?method=get"
try:
sync_open = self.web.session.get(sync_url)
sync_open = self.fhdhr.web.session.get(sync_url)
retrieved_cluster = sync_open.json()
if self.location not in list(retrieved_cluster.keys()):
return self.leave()
except self.web.exceptions.ConnectionError:
self.logger.error("Unreachable: " + location)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
def leave(self):
self.db.set_fhdhr_value("cluster", "dict", self.default_cluster())
self.fhdhr.db.set_fhdhr_value("cluster", "dict", self.default_cluster())
def disconnect(self):
cluster = self.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
for location in list(cluster.keys()):
if location != self.location:
sync_url = location + "/api/cluster?method=del&location=" + self.location
try:
self.web.session.get(sync_url)
except self.web.exceptions.ConnectionError:
self.logger.error("Unreachable: " + location)
self.fhdhr.web.session.get(sync_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
self.leave()
def sync(self, location):
sync_url = location + "/api/cluster?method=get"
try:
sync_open = self.web.session.get(sync_url)
self.db.set_fhdhr_value("cluster", "dict", sync_open.json())
except self.web.exceptions.ConnectionError:
self.logger.error("Unreachable: " + location)
sync_open = self.fhdhr.web.session.get(sync_url)
self.fhdhr.db.set_fhdhr_value("cluster", "dict", sync_open.json())
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
def push_sync(self):
cluster = self.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
for location in list(cluster.keys()):
if location != self.location:
sync_url = location + "/api/cluster?method=sync&location=" + self.location_url
try:
self.web.session.get(sync_url)
except self.web.exceptions.ConnectionError:
self.logger.error("Unreachable: " + location)
self.fhdhr.web.session.get(sync_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
def add(self, location):
cluster = self.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
if location not in list(cluster.keys()):
cluster[location] = {"base_url": location}
location_info_url = location + "/discover.json"
try:
location_info_req = self.web.session.get(location_info_url)
except self.web.exceptions.ConnectionError:
self.logger.error("Unreachable: " + location)
location_info_req = self.fhdhr.web.session.get(location_info_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
del cluster[location]
self.db.set_fhdhr_value("cluster", "dict", cluster)
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)
return
location_info = location_info_req.json()
cluster[location]["name"] = location_info["FriendlyName"]
cluster_info_url = location + "/api/cluster?method=get"
try:
cluster_info_req = self.web.session.get(cluster_info_url)
except self.web.exceptions.ConnectionError:
self.logger.error("Unreachable: " + location)
cluster_info_req = self.fhdhr.web.session.get(cluster_info_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
del cluster[location]
self.db.set_fhdhr_value("cluster", "dict", cluster)
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)
return
cluster_info = cluster_info_req.json()
for cluster_key in list(cluster_info.keys()):
if cluster_key not in list(cluster.keys()):
cluster[cluster_key] = cluster_info[cluster_key]
self.db.set_fhdhr_value("cluster", "dict", cluster)
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)
self.push_sync()
def remove(self, location):
cluster = self.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
if location in list(cluster.keys()):
del cluster[location]
sync_url = location + "/api/cluster?method=leave"
try:
self.web.session.get(sync_url)
except self.web.exceptions.ConnectionError:
self.logger.error("Unreachable: " + location)
self.fhdhr.web.session.get(sync_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
self.push_sync()
self.db.set_fhdhr_value("cluster", "dict", cluster)
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)

View File

@ -15,27 +15,49 @@ for entry in os.scandir(device_dir + '/epgtypes'):
class EPG():
def __init__(self, settings, channels, origin, logger, web, db):
self.config = settings
self.logger = logger
def __init__(self, fhdhr, channels, origin):
self.fhdhr = fhdhr
self.origin = origin
self.channels = channels
self.web = web
self.db = db
self.epgdict = {}
self.epg_method_selfadd()
self.epg_methods = self.config.dict["epg"]["method"]
self.def_method = self.config.dict["epg"]["def_method"]
self.epg_methods = self.fhdhr.config.dict["epg"]["method"]
self.def_method = self.fhdhr.config.dict["epg"]["def_method"]
self.sleeptime = {}
for epg_method in self.epg_methods:
if epg_method in list(self.config.dict.keys()):
if "update_frequency" in list(self.config.dict[epg_method].keys()):
self.sleeptime[epg_method] = self.config.dict[epg_method]["update_frequency"]
if epg_method in list(self.fhdhr.config.dict.keys()):
if "update_frequency" in list(self.fhdhr.config.dict[epg_method].keys()):
self.sleeptime[epg_method] = self.fhdhr.config.dict[epg_method]["update_frequency"]
if epg_method not in list(self.sleeptime.keys()):
self.sleeptime[epg_method] = self.config.dict["epg"]["update_frequency"]
self.sleeptime[epg_method] = self.fhdhr.config.dict["epg"]["update_frequency"]
def clear_epg_cache(self, method=None):
if not method:
method = self.def_method
if (method == self.fhdhr.config.dict["main"]["dictpopname"] or
method not in self.fhdhr.config.dict["main"]["valid_epg_methods"]):
method = "origin"
epgtypename = method
if method in [self.fhdhr.config.dict["main"]["dictpopname"], "origin"]:
epgtypename = self.fhdhr.config.dict["main"]["dictpopname"]
self.fhdhr.logger.info("Clearing " + epgtypename + " EPG cache.")
method_to_call = getattr(self, method)
if hasattr(method_to_call, 'clear_cache'):
func_to_call = getattr(method_to_call, 'clear_cache')
func_to_call()
if method in list(self.epgdict.keys()):
del self.epgdict[method]
self.fhdhr.db.delete_fhdhr_value("epg_dict", method)
def whats_on_now(self, channel):
epgdict = self.get_epg()
@ -62,16 +84,16 @@ class EPG():
if not method:
method = self.def_method
if (method == self.config.dict["main"]["dictpopname"] or
method not in self.config.dict["main"]["valid_epg_methods"]):
if (method == self.fhdhr.config.dict["main"]["dictpopname"] or
method not in self.fhdhr.config.dict["main"]["valid_epg_methods"]):
method = "origin"
if method not in list(self.epgdict.keys()):
epgdict = self.db.get_fhdhr_value("epg_dict", method) or None
epgdict = self.fhdhr.db.get_fhdhr_value("epg_dict", method) or None
if not epgdict:
self.update(method)
self.epgdict[method] = self.db.get_fhdhr_value("epg_dict", method) or {}
self.epgdict[method] = self.fhdhr.db.get_fhdhr_value("epg_dict", method) or {}
else:
self.epgdict[method] = epgdict
return self.epgdict[method]
@ -103,21 +125,21 @@ class EPG():
def epg_method_selfadd(self):
for method in epgtype_list:
exec("%s = %s" % ("self." + str(method), str(method) + "." + str(method) + "EPG(self.config, self.channels, self.logger, self.web, self.db)"))
exec("%s = %s" % ("self." + str(method), str(method) + "." + str(method) + "EPG(self.fhdhr, self.channels)"))
def update(self, method=None):
if not method:
method = self.def_method
if (method == self.config.dict["main"]["dictpopname"] or
method not in self.config.dict["main"]["valid_epg_methods"]):
if (method == self.fhdhr.config.dict["main"]["dictpopname"] or
method not in self.fhdhr.config.dict["main"]["valid_epg_methods"]):
method = "origin"
epgtypename = method
if method in [self.config.dict["main"]["dictpopname"], "origin"]:
epgtypename = self.config.dict["main"]["dictpopname"]
if method in [self.fhdhr.config.dict["main"]["dictpopname"], "origin"]:
epgtypename = self.fhdhr.config.dict["main"]["dictpopname"]
self.logger.info("Updating " + epgtypename + " EPG cache.")
self.fhdhr.logger.info("Updating " + epgtypename + " EPG cache.")
method_to_call = getattr(self, method)
func_to_call = getattr(method_to_call, 'update_epg')
if method == 'origin':
@ -136,9 +158,9 @@ class EPG():
programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start'])
self.epgdict = programguide
self.db.set_fhdhr_value("epg_dict", method, programguide)
self.db.set_fhdhr_value("update_time", method, time.time())
self.logger.info("Wrote " + epgtypename + " EPG cache.")
self.fhdhr.db.set_fhdhr_value("epg_dict", method, programguide)
self.fhdhr.db.set_fhdhr_value("update_time", method, time.time())
self.fhdhr.logger.info("Wrote " + epgtypename + " EPG cache.")
def run(self):
for epg_method in self.epg_methods:
@ -146,7 +168,7 @@ class EPG():
try:
while True:
for epg_method in self.epg_methods:
if time.time() >= (self.db.get_fhdhr_value("update_time", epg_method) + self.sleeptime[epg_method]):
if time.time() >= (self.fhdhr.db.get_fhdhr_value("update_time", epg_method) + self.sleeptime[epg_method]):
self.update(epg_method)
time.sleep(3600)
except KeyboardInterrupt:

View File

@ -3,11 +3,10 @@ import datetime
class blocksEPG():
def __init__(self, settings, channels, logger, web, db):
self.config = settings
self.logger = logger
def __init__(self, fhdhr, channels):
self.fhdhr = fhdhr
self.channels = channels
self.db = db
def update_epg(self):
programguide = {}

View File

@ -1,5 +1,3 @@
import json
import time
import datetime
import urllib.parse
@ -9,23 +7,21 @@ from fHDHR.exceptions import EPGSetupError
class zap2itEPG():
def __init__(self, settings, channels, logger, web, db):
self.config = settings
self.logger = logger
def __init__(self, fhdhr, channels):
self.fhdhr = fhdhr
self.channels = channels
self.web = web
self.db = db
self.postalcode = self.config.dict["zap2it"]["postalcode"]
self.postalcode = self.fhdhr.config.dict["zap2it"]["postalcode"]
self.web_cache_dir = self.config.dict["filedir"]["epg_cache"]["zap2it"]["web_cache"]
self.fhdhr.web_cache_dir = self.fhdhr.config.dict["filedir"]["epg_cache"]["zap2it"]["web_cache"]
def get_location(self):
self.logger.warning("Zap2it postalcode not set, attempting to retrieve.")
self.fhdhr.logger.warning("Zap2it postalcode not set, attempting to retrieve.")
if not self.postalcode:
try:
postalcode_url = 'http://ipinfo.io/json'
postalcode_req = self.web.session.get(postalcode_url)
postalcode_req = self.fhdhr.web.session.get(postalcode_url)
data = postalcode_req.json()
self.postalcode = data["postal"]
except Exception as e:
@ -37,40 +33,19 @@ class zap2itEPG():
# Start time parameter is now rounded down to nearest `zap_timespan`, in s.
zap_time = datetime.datetime.utcnow().timestamp()
zap_time_window = int(self.config.dict["zap2it"]["timespan"]) * 3600
self.remove_stale_cache(zap_time)
zap_time_window = int(self.fhdhr.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.dict["zap2it"]["timespan"]))):
i_time = zap_time + (i * zap_time_window)
i_times = []
for i in range(int(7 * 24 / int(self.fhdhr.config.dict["zap2it"]["timespan"]))):
i_times.append(zap_time + (i * zap_time_window))
parameters = {
'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.dict["zap2it"]['languagecode'],
'pref': 'm,p',
'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",
}
cached_items = self.get_cached(i_times)
for result in cached_items:
url = 'https://tvlistings.zap2it.com/api/grid?'
url += urllib.parse.urlencode(parameters)
result = self.get_cached(str(i_time), self.config.dict["zap2it"]['delay'], url)
d = json.loads(result)
for c in d['channels']:
for c in result['channels']:
cdict = xmldictmaker(c, ["callSign", "name", "channelNo", "channelId", "thumbnail"])
@ -119,7 +94,8 @@ class zap2itEPG():
if 'New' in eventdict['flag'] and 'live' not in eventdict['flag']:
clean_prog_dict["isnew"] = True
programguide[str(cdict["channelNo"])]["listing"].append(clean_prog_dict)
if not any(d['id'] == clean_prog_dict['id'] for d in programguide[str(cdict["channelNo"])]["listing"]):
programguide[str(cdict["channelNo"])]["listing"].append(clean_prog_dict)
return programguide
@ -129,29 +105,69 @@ class zap2itEPG():
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():
self.logger.info('FROM CACHE: ' + str(cache_path))
with open(cache_path, 'rb') as f:
return f.read()
def get_cached(self, i_times):
# Fetch data in `zap_timespan` chunks.
for i_time in i_times:
parameters = {
'aid': self.fhdhr.config.dict["zap2it"]['affiliate_id'],
'country': self.fhdhr.config.dict["zap2it"]['country'],
'device': self.fhdhr.config.dict["zap2it"]['device'],
'headendId': self.fhdhr.config.dict["zap2it"]['headendid'],
'isoverride': "true",
'languagecode': self.fhdhr.config.dict["zap2it"]['languagecode'],
'pref': 'm,p',
'timespan': self.fhdhr.config.dict["zap2it"]['timespan'],
'timezone': self.fhdhr.config.dict["zap2it"]['timezone'],
'userId': self.fhdhr.config.dict["zap2it"]['userid'],
'postalCode': str(self.postalcode or self.get_location()),
'lineupId': '%s-%s-DEFAULT' % (self.fhdhr.config.dict["zap2it"]['country'], self.fhdhr.config.dict["zap2it"]['device']),
'time': i_time,
'Activity_ID': 1,
'FromPage': "TV%20Guide",
}
url = 'https://tvlistings.zap2it.com/api/grid?'
url += urllib.parse.urlencode(parameters)
self.get_cached_item(str(i_time), url)
cache_list = self.fhdhr.db.get_cacheitem_value("cache_list", "offline_cache", "zap2it") or []
return [self.fhdhr.db.get_cacheitem_value(x, "offline_cache", "zap2it") for x in cache_list]
def get_cached_item(self, cache_key, url):
cacheitem = self.fhdhr.db.get_cacheitem_value(cache_key, "offline_cache", "zap2it")
if cacheitem:
self.fhdhr.logger.info('FROM CACHE: ' + str(cache_key))
return cacheitem
else:
self.logger.info('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
self.fhdhr.logger.info('Fetching: ' + url)
try:
resp = self.fhdhr.web.session.get(url)
except self.fhdhr.web.exceptions.HTTPError:
self.fhdhr.logger.info('Got an error! Ignoring it.')
return
result = resp.json()
self.fhdhr.db.set_cacheitem_value(cache_key, "offline_cache", result, "zap2it")
cache_list = self.fhdhr.db.get_cacheitem_value("cache_list", "offline_cache", "zap2it") or []
cache_list.append(cache_key)
self.fhdhr.db.set_cacheitem_value("cache_list", "offline_cache", cache_list, "zap2it")
def remove_stale_cache(self, zap_time):
for p in self.web_cache_dir.glob('*'):
try:
t = int(p.name)
if t >= zap_time:
continue
except Exception as e:
self.logger.error(e)
pass
self.logger.info('Removing stale cache file: ' + p.name)
p.unlink()
cache_list = self.fhdhr.db.get_cacheitem_value("cache_list", "offline_cache", "zap2it") or []
cache_to_kill = []
for cacheitem in cache_list:
cachedate = int(cacheitem)
if cachedate < zap_time:
cache_to_kill.append(cacheitem)
self.fhdhr.db.delete_cacheitem_value(cacheitem, "offline_cache", "zap2it")
self.fhdhr.logger.info('Removing stale cache: ' + str(cacheitem))
self.fhdhr.db.set_cacheitem_value("cache_list", "offline_cache", [x for x in cache_list if x not in cache_to_kill], "zap2it")
def clear_cache(self):
cache_list = self.fhdhr.db.get_cacheitem_value("cache_list", "offline_cache", "zap2it") or []
for cacheitem in cache_list:
self.fhdhr.db.delete_cacheitem_value(cacheitem, "offline_cache", "zap2it")
self.fhdhr.logger.info('Removing cache: ' + str(cacheitem))
self.fhdhr.db.delete_cacheitem_value("cache_list", "offline_cache", "zap2it")

View File

@ -6,18 +6,15 @@ import PIL.ImageFont
class imageHandler():
def __init__(self, settings, epg, logger, web):
self.config = settings
self.logger = logger
self.epg = epg
self.web = web
def __init__(self, fhdhr, epg):
self.fhdhr = fhdhr
def get_epg_image(self, image_type, content_id):
imageUri = self.epg.get_thumbnail(image_type, str(content_id))
if not imageUri:
return self.generate_image(image_type, str(content_id))
req = self.web.session.get(imageUri)
req = self.fhdhr.web.session.get(imageUri)
return req.content
def getSize(self, txt, font):
@ -38,7 +35,7 @@ class imageHandler():
colorBackground = "#228822"
colorText = "#717D7E"
colorOutline = "#717D7E"
fontname = str(self.config.dict["filedir"]["font"])
fontname = str(self.fhdhr.config.dict["filedir"]["font"])
font = PIL.ImageFont.truetype(fontname, fontsize)
text_width, text_height = self.getSize(message, font)

View File

@ -5,47 +5,44 @@ import struct
class fHDHR_Detect():
def __init__(self, settings, logger, db):
self.config = settings
self.db = db
self.db.delete_fhdhr_value("ssdp_detect", "list")
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.fhdhr.db.delete_fhdhr_value("ssdp_detect", "list")
def set(self, location):
detect_list = self.db.get_fhdhr_value("ssdp_detect", "list") or []
detect_list = self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []
if location not in detect_list:
detect_list.append(location)
self.db.set_fhdhr_value("ssdp_detect", "list", detect_list)
self.fhdhr.db.set_fhdhr_value("ssdp_detect", "list", detect_list)
def get(self):
return self.db.get_fhdhr_value("ssdp_detect", "list") or []
return self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []
class SSDPServer():
def __init__(self, settings, fhdhr_version, logger, db):
self.config = settings
self.logger = logger
self.db = db
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.detect_method = fHDHR_Detect(settings, logger, db)
self.detect_method = fHDHR_Detect(fhdhr)
if settings.dict["fhdhr"]["discovery_address"]:
if fhdhr.config.dict["fhdhr"]["discovery_address"]:
self.sock = None
self.proto = "ipv4"
self.port = 1900
self.iface = None
self.address = None
self.server = 'fHDHR/%s UPnP/1.0' % fhdhr_version
self.server = 'fHDHR/%s UPnP/1.0' % fhdhr.version
allowed_protos = ("ipv4", "ipv6")
if self.proto not in allowed_protos:
raise ValueError("Invalid proto - expected one of {}".format(allowed_protos))
self.nt = 'urn:schemas-upnp-org:device:MediaServer:1'
self.usn = 'uuid:' + settings.dict["main"]["uuid"] + '::' + self.nt
self.location = ('http://' + settings.dict["fhdhr"]["discovery_address"] + ':' +
str(settings.dict["fhdhr"]["port"]) + '/device.xml')
self.usn = 'uuid:' + fhdhr.config.dict["main"]["uuid"] + '::' + self.nt
self.location = ('http://' + fhdhr.config.dict["fhdhr"]["discovery_address"] + ':' +
str(fhdhr.config.dict["fhdhr"]["port"]) + '/device.xml')
self.al = self.location
self.max_age = 1800
self._iface = None
@ -104,14 +101,14 @@ class SSDPServer():
self.m_search()
def on_recv(self, data, address):
self.logger.debug("Received packet from {}: {}".format(address, data))
self.fhdhr.logger.debug("Received packet from {}: {}".format(address, data))
(host, port) = address
try:
header, payload = data.decode().split('\r\n\r\n')[:2]
except ValueError:
self.logger.error("Error with Received packet from {}: {}".format(address, data))
self.fhdhr.logger.error("Error with Received packet from {}: {}".format(address, data))
return
lines = header.split('\r\n')
@ -124,19 +121,19 @@ class SSDPServer():
if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
# SSDP discovery
self.logger.debug("Received qualifying M-SEARCH from {}".format(address))
self.logger.debug("M-SEARCH data: {}".format(headers))
self.fhdhr.logger.debug("Received qualifying M-SEARCH from {}".format(address))
self.fhdhr.logger.debug("M-SEARCH data: {}".format(headers))
notify = self.notify_payload
self.logger.debug("Created NOTIFY: {}".format(notify))
self.fhdhr.logger.debug("Created NOTIFY: {}".format(notify))
try:
self.sock.sendto(notify, address)
except OSError as e:
# Most commonly: We received a multicast from an IP not in our subnet
self.logger.debug("Unable to send NOTIFY to {}: {}".format(address, e))
self.fhdhr.logger.debug("Unable to send NOTIFY to {}: {}".format(address, e))
pass
elif cmd[0] == 'NOTIFY' and cmd[1] == '*':
# SSDP presence
self.logger.debug("NOTIFY data: {}".format(headers))
self.fhdhr.logger.debug("NOTIFY data: {}".format(headers))
try:
if headers["server"].startswith("fHDHR"):
if headers["location"] != self.location:
@ -144,7 +141,7 @@ class SSDPServer():
except KeyError:
return
else:
self.logger.debug('Unknown SSDP command %s %s' % (cmd[0], cmd[1]))
self.fhdhr.logger.debug('Unknown SSDP command %s %s' % (cmd[0], cmd[1]))
def m_search(self):
data = self.msearch_payload

View File

@ -3,31 +3,31 @@ from multiprocessing import Process
class Station_Scan():
def __init__(self, settings, channels, logger, db):
self.config = settings
self.logger = logger
def __init__(self, fhdhr, channels):
self.fhdhr = fhdhr
self.channels = channels
self.db = db
self.db.delete_fhdhr_value("station_scan", "scanning")
self.fhdhr.db.delete_fhdhr_value("station_scan", "scanning")
def scan(self):
self.logger.info("Channel Scan Requested by Client.")
self.fhdhr.logger.info("Channel Scan Requested by Client.")
scan_status = self.db.get_fhdhr_value("station_scan", "scanning")
scan_status = self.fhdhr.db.get_fhdhr_value("station_scan", "scanning")
if not scan_status:
self.db.set_fhdhr_value("station_scan", "scanning", 1)
self.fhdhr.db.set_fhdhr_value("station_scan", "scanning", 1)
chanscan = Process(target=self.runscan)
chanscan.start()
else:
self.logger.info("Channel Scan Already In Progress!")
self.fhdhr.logger.info("Channel Scan Already In Progress!")
def runscan(self):
self.channels.get_channels(forceupdate=True)
self.logger.info("Requested Channel Scan Complete.")
self.db.delete_fhdhr_value("station_scan", "scanning")
self.fhdhr.logger.info("Requested Channel Scan Complete.")
self.fhdhr.db.delete_fhdhr_value("station_scan", "scanning")
def scanning(self):
scan_status = self.db.get_fhdhr_value("station_scan", "scanning")
scan_status = self.fhdhr.db.get_fhdhr_value("station_scan", "scanning")
if not scan_status:
return False
else:

View File

@ -6,8 +6,8 @@ from fHDHR.tools import humanized_time
class Tuner():
def __init__(self, inum, epg, logger):
self.logger = logger
def __init__(self, fhdhr, inum, epg):
self.fhdhr = fhdhr
self.number = inum
self.epg = epg
self.tuner_lock = threading.Lock()
@ -17,18 +17,19 @@ class Tuner():
if self.tuner_lock.locked():
raise TunerError("Tuner #" + str(self.number) + " is not available.")
self.logger.info("Tuner #" + str(self.number) + " to be used for stream.")
self.fhdhr.logger.info("Tuner #" + str(self.number) + " to be used for stream.")
self.tuner_lock.acquire()
self.status = {
"status": "Active",
"method": stream_args["method"],
"accessed": stream_args["accessed"],
"channel": stream_args["channel"],
"proxied_url": stream_args["channelUri"],
"time_start": datetime.datetime.utcnow(),
}
def close(self):
self.logger.info("Tuner #" + str(self.number) + " Shutting Down.")
self.fhdhr.logger.info("Tuner #" + str(self.number) + " Shutting Down.")
self.set_off_status()
self.tuner_lock.release()
@ -39,7 +40,7 @@ class Tuner():
humanized_time(
int((datetime.datetime.utcnow() - current_status["time_start"]).total_seconds())))
current_status["time_start"] = str(current_status["time_start"])
current_status["epg"] = self.epg.whats_on_now(current_status["accessed"].split("v")[-1])
current_status["epg"] = self.epg.whats_on_now(current_status["channel"])
return current_status
def set_off_status(self):
@ -48,29 +49,31 @@ class Tuner():
class Tuners():
def __init__(self, settings, epg, logger):
self.config = settings
self.logger = logger
def __init__(self, fhdhr, epg):
self.fhdhr = fhdhr
self.epg = epg
self.max_tuners = int(self.config.dict["fhdhr"]["tuner_count"])
self.max_tuners = int(self.fhdhr.config.dict["fhdhr"]["tuner_count"])
self.tuners = {}
for i in range(1, self.max_tuners + 1):
exec("%s = %s" % ("self.tuner_" + str(i), "Tuner(i, epg, logger)"))
self.tuners[i] = Tuner(fhdhr, i, epg)
def tuner_grab(self, stream_args, tunernum=None):
def tuner_grab(self, stream_args):
tunerselected = None
if tunernum:
if tunernum not in range(1, self.max_tuners + 1):
raise TunerError("Tuner " + str(tunernum) + " does not exist.")
eval("self.tuner_" + str(tunernum) + ".grab(stream_args)")
tunerselected = tunernum
if stream_args["tuner"]:
if int(stream_args["tuner"]) not in list(self.tuners.keys()):
raise TunerError("Tuner " + str(stream_args["tuner"]) + " does not exist.")
self.tuners[int(stream_args["tuner"])].grab(stream_args)
tunerselected = int(stream_args["tuner"])
else:
for tunernum in range(1, self.max_tuners + 1):
try:
eval("self.tuner_" + str(tunernum) + ".grab(stream_args)")
self.tuners[int(tunernum)].grab(stream_args)
except TunerError:
continue
else:
@ -83,18 +86,18 @@ class Tuners():
return tunerselected
def tuner_close(self, tunernum):
eval("self.tuner_" + str(tunernum) + ".close()")
self.tuners[int(tunernum)].close()
def status(self):
all_status = {}
for tunernum in range(1, self.max_tuners + 1):
all_status[tunernum] = eval("self.tuner_" + str(tunernum) + ".get_status()")
all_status[tunernum] = self.tuners[int(tunernum)].get_status()
return all_status
def available_tuner_count(self):
available_tuners = 0
for tunernum in range(1, self.max_tuners + 1):
tuner_status = eval("self.tuner_" + str(tunernum) + ".get_status()")
tuner_status = self.tuners[int(tunernum)].get_status()
if tuner_status["status"] == "Inactive":
available_tuners += 1
return available_tuners
@ -102,7 +105,7 @@ class Tuners():
def inuse_tuner_count(self):
inuse_tuners = 0
for tunernum in range(1, self.max_tuners + 1):
tuner_status = eval("self.tuner_" + str(tunernum) + ".get_status()")
tuner_status = self.tuners[int(tunernum)].get_status()
if tuner_status["status"] == "Active":
inuse_tuners += 1
return inuse_tuners

View File

@ -6,21 +6,20 @@ from fHDHR.exceptions import TunerError
class WatchStream():
def __init__(self, settings, origserv, tuners, logger, web):
self.config = settings
self.logger = logger
def __init__(self, fhdhr, origserv, tuners):
self.fhdhr = fhdhr
self.origserv = origserv
self.tuners = tuners
self.web = web
def direct_stream(self, stream_args, tunernum):
chunksize = int(self.tuners.config.dict["direct_stream"]['chunksize'])
chunksize = int(self.fhdhr.config.dict["direct_stream"]['chunksize'])
if not stream_args["duration"] == 0:
stream_args["duration"] += time.time()
req = self.web.session.get(stream_args["channelUri"], stream=True)
req = self.fhdhr.web.session.get(stream_args["channelUri"], stream=True)
def generate():
try:
@ -28,30 +27,23 @@ class WatchStream():
if not stream_args["duration"] == 0 and not time.time() < stream_args["duration"]:
req.close()
self.logger.info("Requested Duration Expired.")
self.fhdhr.logger.info("Requested Duration Expired.")
break
yield chunk
except GeneratorExit:
req.close()
self.logger.info("Connection Closed.")
self.fhdhr.logger.info("Connection Closed.")
self.tuners.tuner_close(tunernum)
return generate()
def ffmpeg_stream(self, stream_args, tunernum):
bytes_per_read = int(self.config.dict["ffmpeg"]["bytes_per_read"])
bytes_per_read = int(self.fhdhr.config.dict["ffmpeg"]["bytes_per_read"])
ffmpeg_command = [self.config.dict["ffmpeg"]["ffmpeg_path"],
"-i", stream_args["channelUri"],
"-c", "copy",
"-f", "mpegts",
"-nostats", "-hide_banner",
"-loglevel", "fatal",
"pipe:stdout"
]
ffmpeg_command = self.transcode_profiles(stream_args)
if not stream_args["duration"] == 0:
stream_args["duration"] += time.time()
@ -65,25 +57,62 @@ class WatchStream():
if not stream_args["duration"] == 0 and not time.time() < stream_args["duration"]:
ffmpeg_proc.terminate()
ffmpeg_proc.communicate()
self.logger.info("Requested Duration Expired.")
self.fhdhr.logger.info("Requested Duration Expired.")
break
videoData = ffmpeg_proc.stdout.read(bytes_per_read)
if not videoData:
break
try:
yield videoData
except Exception as e:
ffmpeg_proc.terminate()
ffmpeg_proc.communicate()
self.logger.info("Connection Closed: " + str(e))
yield videoData
except GeneratorExit:
ffmpeg_proc.terminate()
ffmpeg_proc.communicate()
self.logger.info("Connection Closed.")
self.fhdhr.logger.info("Connection Closed.")
self.tuners.tuner_close(tunernum)
except Exception as e:
ffmpeg_proc.terminate()
ffmpeg_proc.communicate()
self.fhdhr.logger.info("Connection Closed: " + str(e))
self.tuners.tuner_close(tunernum)
return generate()
def vlc_stream(self, stream_args, tunernum):
bytes_per_read = int(self.fhdhr.config.dict["vlc"]["bytes_per_read"])
vlc_command = self.transcode_profiles(stream_args)
if not stream_args["duration"] == 0:
stream_args["duration"] += time.time()
vlc_proc = subprocess.Popen(vlc_command, stdout=subprocess.PIPE)
def generate():
try:
while True:
if not stream_args["duration"] == 0 and not time.time() < stream_args["duration"]:
vlc_proc.terminate()
vlc_proc.communicate()
self.fhdhr.logger.info("Requested Duration Expired.")
break
videoData = vlc_proc.stdout.read(bytes_per_read)
if not videoData:
break
yield videoData
except GeneratorExit:
vlc_proc.terminate()
vlc_proc.communicate()
self.fhdhr.logger.info("Connection Closed.")
self.tuners.tuner_close(tunernum)
except Exception as e:
vlc_proc.terminate()
vlc_proc.communicate()
self.fhdhr.logger.info("Connection Closed: " + str(e))
self.tuners.tuner_close(tunernum)
return generate()
@ -93,14 +122,16 @@ class WatchStream():
try:
tunernum = self.tuners.tuner_grab(stream_args)
except TunerError as e:
self.logger.info("A " + stream_args["method"] + " stream request for channel " +
str(stream_args["channel"]) + " was rejected do to " + str(e))
self.fhdhr.logger.info("A " + stream_args["method"] + " stream request for channel " +
str(stream_args["channel"]) + " was rejected do to " + str(e))
return
self.logger.info("Attempting a " + stream_args["method"] + " stream request for channel " + str(stream_args["channel"]))
self.fhdhr.logger.info("Attempting a " + stream_args["method"] + " stream request for channel " + str(stream_args["channel"]))
if stream_args["method"] == "ffmpeg":
return self.ffmpeg_stream(stream_args, tunernum)
if stream_args["method"] == "vlc":
return self.vlc_stream(stream_args, tunernum)
elif stream_args["method"] == "direct":
return self.direct_stream(stream_args, tunernum)
@ -108,10 +139,106 @@ class WatchStream():
stream_args["channelUri"] = self.origserv.get_channel_stream(str(stream_args["channel"]))
if not stream_args["channelUri"]:
self.logger.error("Could not Obtain Channel Stream.")
self.fhdhr.logger.error("Could not Obtain Channel Stream.")
stream_args["content_type"] = "video/mpeg"
else:
channelUri_headers = self.web.session.head(stream_args["channelUri"]).headers
channelUri_headers = self.fhdhr.web.session.head(stream_args["channelUri"]).headers
stream_args["content_type"] = channelUri_headers['Content-Type']
return stream_args
def transcode_profiles(self, stream_args):
# TODO implement actual profiles here
"""
heavy: transcode to AVC with the same resolution, frame-rate, and interlacing as the
original stream. For example 1080i60 AVC 1080i60, 720p60 AVC 720p60.
mobile: trancode to AVC progressive not exceeding 1280x720 30fps.
internet720: transcode to low bitrate AVC progressive not exceeding 1280x720 30fps.
internet480: transcode to low bitrate AVC progressive not exceeding 848x480 30fps for
16:9 content, not exceeding 640x480 30fps for 4:3 content.
internet360: transcode to low bitrate AVC progressive not exceeding 640x360 30fps for
16:9 content, not exceeding 480x360 30fps for 4:3 content.
internet240: transcode to low bitrate AVC progressive not exceeding 432x240 30fps for
16:9 content, not exceeding 320x240 30fps for 4:3 content
"""
if stream_args["transcode"]:
self.fhdhr.logger.info("Client requested a " + stream_args["transcode"] + " transcode for stream.")
log_level = self.fhdhr.config.dict["logging"]["level"].lower()
if stream_args["method"] == "direct":
return None
elif stream_args["method"] == "ffmpeg":
ffmpeg_command = [
self.fhdhr.config.dict["ffmpeg"]["ffmpeg_path"],
"-i", stream_args["channelUri"],
"-c", "copy",
"-f", "mpegts",
]
if not stream_args["transcode"]:
ffmpeg_command.extend([])
elif stream_args["transcode"] == "heavy":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "mobile":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet720":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet480":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet360":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet240":
ffmpeg_command.extend([])
loglevel_dict = {
"debug": "debug",
"info": "info",
"error": "error",
"warning": "warning",
"critical": "fatal",
}
if log_level not in ["info", "debug"]:
ffmpeg_command.extend(["-nostats", "-hide_banner"])
ffmpeg_command.extend(["-loglevel", loglevel_dict[log_level]])
ffmpeg_command.extend(["pipe:stdout"])
return ffmpeg_command
elif stream_args["method"] == "vlc":
vlc_command = [
self.fhdhr.config.dict["vlc"]["vlc_path"],
"-I", "dummy", stream_args["channelUri"],
]
loglevel_dict = {
"debug": "3",
"info": "0",
"error": "1",
"warning": "2",
"critical": "1",
}
vlc_command.extend(["--log-verbose=", loglevel_dict[log_level]])
if log_level not in ["info", "debug"]:
vlc_command.extend(["--quiet"])
if not stream_args["transcode"]:
vlc_command.extend([])
elif stream_args["transcode"] == "heavy":
vlc_command.extend([])
elif stream_args["transcode"] == "mobile":
vlc_command.extend([])
elif stream_args["transcode"] == "internet720":
vlc_command.extend([])
elif stream_args["transcode"] == "internet480":
vlc_command.extend([])
elif stream_args["transcode"] == "internet360":
vlc_command.extend([])
elif stream_args["transcode"] == "internet240":
vlc_command.extend([])
vlc_command.extend(["--sout", "#std{mux=ts,access=file,dst=-}"])
return vlc_command

View File

@ -4,6 +4,7 @@ from flask import Flask
from .pages import fHDHR_Pages
from .files import fHDHR_Files
from .api import fHDHR_API
from .watch import fHDHR_WATCH
class fHDHR_HTTP_Server():
@ -23,6 +24,9 @@ class fHDHR_HTTP_Server():
self.api = fHDHR_API(fhdhr)
self.add_endpoints(self.api, "api")
self.watch = fHDHR_WATCH(fhdhr)
self.add_endpoints(self.watch, "watch")
def add_endpoints(self, index_list, index_name):
item_list = [x for x in dir(index_list) if self.isapath(x)]
for item in item_list:

View File

@ -4,6 +4,7 @@ from .channels import Channels
from .lineup_post import Lineup_Post
from .xmltv import xmlTV
from .m3u import M3U
from .epg import EPG
from .debug import Debug_JSON
from .images import Images
@ -18,6 +19,7 @@ class fHDHR_API():
self.channels = Channels(fhdhr)
self.xmltv = xmlTV(fhdhr)
self.m3u = M3U(fhdhr)
self.epg = EPG(fhdhr)
self.debug = Debug_JSON(fhdhr)
self.lineup_post = Lineup_Post(fhdhr)

48
fHDHR/http/api/epg.py Normal file
View File

@ -0,0 +1,48 @@
from flask import Response, request, redirect
import urllib.parse
import json
class EPG():
"""Methods to create xmltv.xml"""
endpoints = ["/api/epg"]
endpoint_name = "api_epg"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
method = request.args.get('method', default="get", type=str)
source = request.args.get('source', default=self.fhdhr.config.dict["epg"]["def_method"], type=str)
if source not in self.fhdhr.config.dict["main"]["valid_epg_methods"]:
return "%s Invalid xmltv method" % source
redirect_url = request.args.get('redirect', default=None, type=str)
if method == "get":
epgdict = self.fhdhr.device.epg.get_epg(source)
epg_json = json.dumps(epgdict, indent=4)
return Response(status=200,
response=epg_json,
mimetype='application/json')
elif method == "update":
self.fhdhr.device.epg.update(source)
elif method == "clearcache":
self.fhdhr.device.epg.clear_epg_cache(source)
else:
return "%s Invalid Method" % method
if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method))
else:
return "%s Success" % method

View File

@ -47,6 +47,9 @@ class xmlTV():
elif method == "update":
self.fhdhr.device.epg.update(source)
elif method == "clearcache":
self.fhdhr.device.epg.clear_epg_cache(source)
else:
return "%s Invalid Method" % method

View File

@ -10,8 +10,6 @@ from .discover_json import Discover_JSON
from .lineup_json import Lineup_JSON
from .lineup_status_json import Lineup_Status_JSON
from .watch import Watch
class fHDHR_Files():
@ -27,5 +25,3 @@ class fHDHR_Files():
self.discover_json = Discover_JSON(fhdhr)
self.lineup_json = Lineup_JSON(fhdhr)
self.lineup_status_json = Lineup_Status_JSON(fhdhr)
self.watch = Watch(fhdhr)

View File

@ -25,6 +25,7 @@ class Lineup_XML():
program_out = sub_el(out, 'Program')
sub_el(program_out, 'GuideNumber', station_item['GuideNumber'])
sub_el(program_out, 'GuideName', station_item['GuideName'])
sub_el(program_out, 'Tags', ",".join(station_item['Tags']))
sub_el(program_out, 'URL', station_item['URL'])
fakefile = BytesIO()

View File

@ -28,6 +28,7 @@ class Diagnostics_HTML():
["device.xml", "device.xml"],
["discover.json", "discover.json"],
["lineup.json", "lineup.json"],
["lineup.xml", "lineup.xml"],
["lineup_status.json", "lineup_status.json"],
["cluster.json", "/api/cluster?method=get"]
]

View File

@ -31,19 +31,6 @@ class Guide_HTML():
fakefile.write("<h4 id=\"mcetoc_1cdobsl3g0\" style=\"text-align: center;\"><span style=\"text-decoration: underline;\"><strong><em>What's On %s</em></strong></span></h4>\n" % friendlyname)
fakefile.write("\n")
# a list of 2 part lists containing button information
button_list = [
["Force xmlTV Update", "/api/xmltv?method=update&redirect=%2Fguide"],
]
fakefile.write("<div style=\"text-align: center;\">\n")
for button_item in button_list:
button_label = button_item[0]
button_path = button_item[1]
fakefile.write(" <p><button onclick=\"OpenLink('%s')\">%s</a></button></p>\n" % (button_path, button_label))
fakefile.write("</div>\n")
fakefile.write("\n")
fakefile.write("<table style=\"width:100%\">\n")
fakefile.write(" <tr>\n")
fakefile.write(" <th>Play</th>\n")

View File

@ -45,7 +45,7 @@ class fHDHR_Page_Elements():
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/version", "Version"),
"<button class=\"pull-left\" onclick=\"OpenLink('%s')\">%s</a></button>" % ("/diagnostics", "Diagnostics"),
"<a class=\"pull-right\" style=\"padding: 5px;\" href=\"%s\">%s</a>" % ("/api/xmltv?method=get&source=origin", "xmltv"),
"<a class=\"pull-right\" style=\"padding: 5px;\" href=\"%s\">%s</a>" % ("/api/xmltv?method=get&source=" + self.fhdhr.device.epg.def_method, "xmltv"),
"<a class=\"pull-right\" style=\"padding: 5px;\" href=\"%s\">%s</a>" % ("/api/m3u?method=get&channel=all", "m3u"),
"</div>",

View File

@ -34,6 +34,8 @@ class Streams_HTML():
fakefile.write(" </tr>\n")
tuner_status = self.fhdhr.device.tuners.status()
for tuner in list(tuner_status.keys()):
print(tuner_status[tuner])
for tuner in list(tuner_status.keys()):
fakefile.write(" <tr>\n")
fakefile.write(" <td>%s</td>\n" % (str(tuner)))

View File

@ -36,7 +36,8 @@ class Version_HTML():
["Python", sys.version],
["Operating System", self.fhdhr.config.dict["main"]["opersystem"]],
["Using Docker", self.fhdhr.config.dict["main"]["isdocker"]],
["ffmpeg", self.fhdhr.config.dict["ffmpeg"]["version"]]
["ffmpeg", self.fhdhr.config.dict["ffmpeg"]["version"]],
["vlc", self.fhdhr.config.dict["vlc"]["version"]]
]
for item in table_guts:

View File

@ -27,7 +27,8 @@ class xmlTV_HTML():
fakefile.write("<table class=\"center\" style=\"width:50%\">\n")
fakefile.write(" <tr>\n")
fakefile.write(" <th>Version</th>\n")
fakefile.write(" <th>Link</th>\n")
fakefile.write(" <th>XMLTV Link</th>\n")
fakefile.write(" <th>EPG Link</th>\n")
fakefile.write(" <th>Options</th>\n")
fakefile.write(" </tr>\n")
@ -39,12 +40,16 @@ class xmlTV_HTML():
fakefile.write(" <tr>\n")
fakefile.write(" <td>%s</td>\n" % (epg_method_name))
fakefile.write(" <td><a href=\"%s\">%s</a>\n" % ("/api/xmltv?method=get&source=" + epg_method, epg_method_name))
fakefile.write(" <td><a href=\"%s\">%s</a>\n" % ("/api/epg?method=get&source=" + epg_method, epg_method_name))
fakefile.write(" <td>\n")
fakefile.write(" <div>\n")
fakefile.write(
" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" %
("/api/xmltv?method=update&source=" + epg_method + "&redirect=%2Fxmltv", "Update"))
fakefile.write(
" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" %
("/api/xmltv?method=clearcache&source=" + epg_method + "&redirect=%2Fxmltv", "Clear Cache"))
fakefile.write(" </div>\n")
fakefile.write(" </td>\n")

View File

@ -0,0 +1,12 @@
from .watch import Watch
from .tuner import Tuner
class fHDHR_WATCH():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.watch = Watch(fhdhr)
self.tuner = Tuner(fhdhr)

46
fHDHR/http/watch/tuner.py Normal file
View File

@ -0,0 +1,46 @@
from flask import Response, request, stream_with_context, abort
class Tuner():
endpoints = ['/tuner<tuner>/<channel>']
endpoint_name = "tuner"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, tuner, channel, *args):
return self.get(tuner, channel, *args)
def get(self, tuner, channel, *args):
if channel.startswith("v"):
channel_number = channel.replace('v', '')
elif channel.startswith("ch"):
channel_freq = channel.replace('ch', '').split("-")[0]
subchannel = 0
if "-" in channel:
subchannel = channel.replace('ch', '').split("-")[1]
abort(503, "Not Implemented %s-%s" % (str(channel_freq), str(subchannel)))
if channel_number not in list(self.fhdhr.device.channels.list.keys()):
abort(404, "Not Found")
base_url = request.url_root[:-1]
stream_args = {
"channel": channel_number,
"method": request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str),
"duration": request.args.get('duration', default=0, type=int),
"transcode": request.args.get('transcode', default=None, type=int),
"accessed": self.fhdhr.device.channels.get_fhdhr_stream_url(base_url, channel_number),
"tuner": tuner
}
stream_args = self.fhdhr.device.watch.get_stream_info(stream_args)
if not stream_args["channelUri"]:
abort(503, "Service Unavailable")
if stream_args["channelUri"]:
if stream_args["method"] == "direct":
return Response(self.fhdhr.device.watch.get_stream(stream_args), content_type=stream_args["content_type"], direct_passthrough=True)
elif stream_args["method"] == "ffmpeg":
return Response(stream_with_context(self.fhdhr.device.watch.get_stream(stream_args)), mimetype="video/mpeg")

View File

@ -13,17 +13,36 @@ class Watch():
def get(self, channel, *args):
if channel.startswith("v"):
channel_number = channel.replace('v', '')
elif channel.startswith("ch"):
channel_freq = channel.replace('ch', '').split("-")[0]
subchannel = 0
if "-" in channel:
subchannel = channel.replace('ch', '').split("-")[1]
abort(503, "Not Implemented %s-%s" % (str(channel_freq), str(subchannel)))
if channel_number not in list(self.fhdhr.device.channels.list.keys()):
abort(404, "Not Found")
base_url = request.url_root[:-1]
stream_args = {
"channel": channel.replace('v', ''),
"channel": channel_number,
"method": request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str),
"duration": request.args.get('duration', default=0, type=int),
"accessed": self.fhdhr.device.channels.get_fhdhr_stream_url(base_url, channel.replace('v', '')),
"transcode": request.args.get('transcode', default=None, type=int),
"accessed": self.fhdhr.device.channels.get_fhdhr_stream_url(base_url, channel_number),
"tuner": None
}
stream_args = self.fhdhr.device.watch.get_stream_info(stream_args)
if not stream_args["channelUri"]:
abort(503, "Service Unavailable")
if stream_args["channelUri"]:
if stream_args["method"] == "direct":
return Response(self.fhdhr.device.watch.get_stream(stream_args), content_type=stream_args["content_type"], direct_passthrough=True)
elif stream_args["method"] == "ffmpeg":
return Response(stream_with_context(self.fhdhr.device.watch.get_stream(stream_args)), mimetype="video/mpeg")
abort(503)
elif stream_args["method"] == "vlc":
return Response(stream_with_context(self.fhdhr.device.watch.get_stream(stream_args)), mimetype="video/mpeg")

View File

@ -26,12 +26,10 @@ class OriginChannels_StandIN():
class OriginServiceWrapper():
def __init__(self, settings, logger, web, db):
self.config = settings
self.logger = logger
self.web = web
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.servicename = settings.dict["main"]["servicename"]
self.servicename = fhdhr.config.dict["main"]["servicename"]
self.setup_success = None
self.setup()
@ -39,16 +37,16 @@ class OriginServiceWrapper():
def setup(self):
try:
self.origin = OriginService(self.config, self.logger, self.web)
self.origin = OriginService(self.fhdhr)
self.setup_success = True
self.logger.info("%s Setup Success" % self.servicename)
self.fhdhr.logger.info("%s Setup Success" % self.servicename)
except fHDHR.exceptions.OriginSetupError as e:
self.logger.error(e)
self.fhdhr.logger.error(e)
self.setup_success = False
if self.setup_success:
self.channels = OriginChannels(self.config, self.origin, self.logger, self.web)
self.epg = OriginEPG(self.config, self.logger, self.web)
self.channels = OriginChannels(self.fhdhr, self.origin)
self.epg = OriginEPG(self.fhdhr)
else:
self.channels = OriginChannels_StandIN()
self.epg = OriginEPG_StandIN()
@ -83,7 +81,13 @@ class OriginServiceWrapper():
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if hasattr(self.fhdhr, name):
return eval("self.fhdhr." + name)
if hasattr(self.origin, name):
return eval("self.origin." + name)
elif hasattr(self.channels, name):
return eval("self.channels." + name)
elif hasattr(self.epg, name):
return eval("self.epg." + name)
else:
raise AttributeError(name)

View File

@ -4,26 +4,24 @@ import json
class OriginChannels():
def __init__(self, settings, origin, logger, web):
self.config = settings
def __init__(self, fhdhr, origin):
self.fhdhr = fhdhr
self.origin = origin
self.logger = logger
self.web = web
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"]),
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
self.origin.sid
))
data_req = self.origin.web.session.get(data_url)
data_req = self.fhdhr.web.session.get(data_url)
data_dict = xmltodict.parse(data_req.content)
if 'channels' not in list(data_dict['rsp'].keys()):
self.logger.error("Could not retrieve channel list")
self.fhdhr.logger.error("Could not retrieve channel list")
return []
channel_o_list = data_dict['rsp']['channels']['channel']
@ -47,9 +45,9 @@ class OriginChannels():
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"]),
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
str(chandict["number"]),
str(chandict["number"]),
))

View File

@ -6,26 +6,24 @@ import fHDHR.tools
class OriginEPG():
def __init__(self, settings, logger, web):
self.config = settings
self.logger = logger
self.web = web
def __init__(self, fhdhr):
self.fhdhr = fhdhr
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"]),
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.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"],
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
self.fhdhr.config.dict["origin"]["sid"],
str(content_id)
))
return item_thumb_url
@ -57,12 +55,12 @@ class OriginEPG():
}
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"]),
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
str(cdict["id"]),
))
epg_req = self.web.session.get(epg_url)
epg_req = self.fhdhr.web.session.get(epg_url)
epg_dict = xmltodict.parse(epg_req.content)
for program_listing in epg_dict["rsp"]["listings"]:

View File

@ -7,48 +7,47 @@ import fHDHR.exceptions
class OriginService():
def __init__(self, settings, logger, web):
self.config = settings
self.logger = logger
self.web = web
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.login()
def login(self):
self.logger.info("Logging into NextPVR")
self.fhdhr.logger.info("Logging into NextPVR")
self.sid = self.get_sid()
if not self.sid:
raise fHDHR.exceptions.OriginSetupError("NextPVR Login Failed")
else:
self.logger.info("NextPVR Login Success")
self.config.write(self.config.dict["main"]["dictpopname"], 'sid', self.sid)
self.fhdhr.logger.info("NextPVR Login Success")
self.fhdhr.config.write(self.fhdhr.config.dict["main"]["dictpopname"], 'sid', self.sid)
def get_sid(self):
if self.config.dict["origin"]["sid"]:
return self.config.dict["origin"]["sid"]
if self.fhdhr.config.dict["origin"]["sid"]:
return self.fhdhr.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"]),
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
))
initiate_req = self.web.session.get(initiate_url)
initiate_req = self.fhdhr.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()
md5PIN = hashlib.md5(str(self.fhdhr.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"]),
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
sid,
clientKey
))
login_req = self.web.session.get(login_url)
login_req = self.fhdhr.web.session.get(login_url)
login_dict = xmltodict.parse(login_req.content)
loginsuccess = None
@ -60,9 +59,9 @@ class OriginService():
def get_status_dict(self):
nextpvr_address = ('%s%s:%s' %
("https://" if self.config.dict["origin"]["ssl"] else "http://",
self.config.dict["origin"]["address"],
str(self.config.dict["origin"]["port"]),
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
))
ret_status_dict = {
"Login": "Success",