diff --git a/data/internal_config/fhdhr.ini b/data/internal_config/fhdhr.ini index f19b3e5..639ada2 100644 --- a/data/internal_config/fhdhr.ini +++ b/data/internal_config/fhdhr.ini @@ -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 diff --git a/fHDHR/__init__.py b/fHDHR/__init__.py index 6989192..bb94edb 100644 --- a/fHDHR/__init__.py +++ b/fHDHR/__init__.py @@ -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) diff --git a/fHDHR/config/__init__.py b/fHDHR/config/__init__.py index e4a7257..efb37e6 100644 --- a/fHDHR/config/__init__.py +++ b/fHDHR/config/__init__.py @@ -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] diff --git a/fHDHR/device/__init__.py b/fHDHR/device/__init__.py index f4d4363..b14c1c1 100644 --- a/fHDHR/device/__init__.py +++ b/fHDHR/device/__init__.py @@ -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) diff --git a/fHDHR/device/channels.py b/fHDHR/device/channels.py index 61f4ceb..5184b57 100644 --- a/fHDHR/device/channels.py +++ b/fHDHR/device/channels.py @@ -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 diff --git a/fHDHR/device/cluster.py b/fHDHR/device/cluster.py index 5193805..5f67700 100644 --- a/fHDHR/device/cluster.py +++ b/fHDHR/device/cluster.py @@ -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) diff --git a/fHDHR/device/epg.py b/fHDHR/device/epg.py index 1dea86a..6e864f6 100644 --- a/fHDHR/device/epg.py +++ b/fHDHR/device/epg.py @@ -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: diff --git a/fHDHR/device/epgtypes/blocks.py b/fHDHR/device/epgtypes/blocks.py index 3604256..536fa50 100644 --- a/fHDHR/device/epgtypes/blocks.py +++ b/fHDHR/device/epgtypes/blocks.py @@ -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 = {} diff --git a/fHDHR/device/epgtypes/zap2it.py b/fHDHR/device/epgtypes/zap2it.py index 1557ab0..d7950f9 100644 --- a/fHDHR/device/epgtypes/zap2it.py +++ b/fHDHR/device/epgtypes/zap2it.py @@ -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") diff --git a/fHDHR/device/images.py b/fHDHR/device/images.py index 6af392e..171dbca 100644 --- a/fHDHR/device/images.py +++ b/fHDHR/device/images.py @@ -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) diff --git a/fHDHR/device/ssdp.py b/fHDHR/device/ssdp.py index 341ef3d..5fdae10 100644 --- a/fHDHR/device/ssdp.py +++ b/fHDHR/device/ssdp.py @@ -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 diff --git a/fHDHR/device/station_scan.py b/fHDHR/device/station_scan.py index 55f0233..3f9717d 100644 --- a/fHDHR/device/station_scan.py +++ b/fHDHR/device/station_scan.py @@ -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: diff --git a/fHDHR/device/tuners.py b/fHDHR/device/tuners.py index 11e39ba..241219f 100644 --- a/fHDHR/device/tuners.py +++ b/fHDHR/device/tuners.py @@ -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 diff --git a/fHDHR/device/watch.py b/fHDHR/device/watch.py index 91b0a18..37e6f3b 100644 --- a/fHDHR/device/watch.py +++ b/fHDHR/device/watch.py @@ -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 diff --git a/fHDHR/http/__init__.py b/fHDHR/http/__init__.py index 30b3ecf..3d81d6c 100644 --- a/fHDHR/http/__init__.py +++ b/fHDHR/http/__init__.py @@ -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: diff --git a/fHDHR/http/api/__init__.py b/fHDHR/http/api/__init__.py index 4cd78a3..cda1d06 100644 --- a/fHDHR/http/api/__init__.py +++ b/fHDHR/http/api/__init__.py @@ -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) diff --git a/fHDHR/http/api/epg.py b/fHDHR/http/api/epg.py new file mode 100644 index 0000000..1f9b7fa --- /dev/null +++ b/fHDHR/http/api/epg.py @@ -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 diff --git a/fHDHR/http/api/xmltv.py b/fHDHR/http/api/xmltv.py index 0186789..b9672e4 100644 --- a/fHDHR/http/api/xmltv.py +++ b/fHDHR/http/api/xmltv.py @@ -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 diff --git a/fHDHR/http/files/__init__.py b/fHDHR/http/files/__init__.py index 0aa0fec..1821936 100644 --- a/fHDHR/http/files/__init__.py +++ b/fHDHR/http/files/__init__.py @@ -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) diff --git a/fHDHR/http/files/lineup_xml.py b/fHDHR/http/files/lineup_xml.py index 884366b..31e2bdf 100644 --- a/fHDHR/http/files/lineup_xml.py +++ b/fHDHR/http/files/lineup_xml.py @@ -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() diff --git a/fHDHR/http/pages/diagnostics_html.py b/fHDHR/http/pages/diagnostics_html.py index 47b59d0..22d73dd 100644 --- a/fHDHR/http/pages/diagnostics_html.py +++ b/fHDHR/http/pages/diagnostics_html.py @@ -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"] ] diff --git a/fHDHR/http/pages/guide_html.py b/fHDHR/http/pages/guide_html.py index f2049b4..6e02813 100644 --- a/fHDHR/http/pages/guide_html.py +++ b/fHDHR/http/pages/guide_html.py @@ -31,19 +31,6 @@ class Guide_HTML(): fakefile.write("
| Play | \n") diff --git a/fHDHR/http/pages/page_elements.py b/fHDHR/http/pages/page_elements.py index 17a66b5..923a80b 100644 --- a/fHDHR/http/pages/page_elements.py +++ b/fHDHR/http/pages/page_elements.py @@ -45,7 +45,7 @@ class fHDHR_Page_Elements(): "" % ("/version", "Version"), "" % ("/diagnostics", "Diagnostics"), - "%s" % ("/api/xmltv?method=get&source=origin", "xmltv"), + "%s" % ("/api/xmltv?method=get&source=" + self.fhdhr.device.epg.def_method, "xmltv"), "%s" % ("/api/m3u?method=get&channel=all", "m3u"), "", diff --git a/fHDHR/http/pages/streams_html.py b/fHDHR/http/pages/streams_html.py index e8e72c8..fd17777 100644 --- a/fHDHR/http/pages/streams_html.py +++ b/fHDHR/http/pages/streams_html.py @@ -34,6 +34,8 @@ class Streams_HTML(): fakefile.write("|||||||||
|---|---|---|---|---|---|---|---|---|---|
| %s | \n" % (str(tuner))) diff --git a/fHDHR/http/pages/version_html.py b/fHDHR/http/pages/version_html.py index fcb54b5..3b4ee1b 100644 --- a/fHDHR/http/pages/version_html.py +++ b/fHDHR/http/pages/version_html.py @@ -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: diff --git a/fHDHR/http/pages/xmltv_html.py b/fHDHR/http/pages/xmltv_html.py index 5a97258..447356b 100644 --- a/fHDHR/http/pages/xmltv_html.py +++ b/fHDHR/http/pages/xmltv_html.py @@ -27,7 +27,8 @@ class xmlTV_HTML(): fakefile.write("
| Version | \n") - fakefile.write("Link | \n") + fakefile.write("XMLTV Link | \n") + fakefile.write("EPG Link | \n") fakefile.write("Options | \n") fakefile.write("
|---|---|---|---|---|
| %s | \n" % (epg_method_name)) fakefile.write("%s\n" % ("/api/xmltv?method=get&source=" + epg_method, epg_method_name)) + fakefile.write(" | %s\n" % ("/api/epg?method=get&source=" + epg_method, epg_method_name)) fakefile.write(" | \n")
fakefile.write(" \n")
fakefile.write(
" \n" %
("/api/xmltv?method=update&source=" + epg_method + "&redirect=%2Fxmltv", "Update"))
+ fakefile.write(
+ " \n" %
+ ("/api/xmltv?method=clearcache&source=" + epg_method + "&redirect=%2Fxmltv", "Clear Cache"))
fakefile.write(" \n")
fakefile.write(" | \n")
diff --git a/fHDHR/http/watch/__init__.py b/fHDHR/http/watch/__init__.py
new file mode 100644
index 0000000..8e87a18
--- /dev/null
+++ b/fHDHR/http/watch/__init__.py
@@ -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)
diff --git a/fHDHR/http/watch/tuner.py b/fHDHR/http/watch/tuner.py
new file mode 100644
index 0000000..cb331fc
--- /dev/null
+++ b/fHDHR/http/watch/tuner.py
@@ -0,0 +1,46 @@
+from flask import Response, request, stream_with_context, abort
+
+
+class Tuner():
+ endpoints = ['/tuner