def __init__(self, _plugins, _namespace, _instance): self.logger = logging.getLogger(__name__) self.config = _plugins.config_obj.data self.epg_db = DBepg(self.config) self.channels_db = DBChannels(self.config) self.plugins = _plugins self.namespace = _namespace self.instance = _instance
def reset_epg(_config, _name): db_epg = DBepg(_config) db_epg.set_last_update(_name) if _name is None: return 'EPG updated and will refresh all days on next request' else: return 'EPG for plugin {} updated and will refresh all days on next request' \ .format(_name)
def __init__(self, _webserver): self.logger = logging.getLogger(__name__) self.webserver = _webserver self.config = _webserver.plugins.config_obj.data self.epg_db = DBepg(self.config) self.channels_db = DBChannels(self.config) self.plugins = _webserver.plugins self.namespace = _webserver.query_data['name'] self.instance = _webserver.query_data['instance']
def __init__(self, _locast_instance): self.locast_instance = _locast_instance self.instance = _locast_instance.instance self.db = DBepg(self.locast_instance.config_obj.data) self.config_section = _locast_instance.config_section self.episode_adj = int(self.locast_instance.config_obj.data \ [self.locast_instance.config_section]['epg-episode_adjustment']) if self.locast_instance.location.has_dma_changed: self.db.del_instance(self.locast_instance.locast.name, self.instance)
def select_reset_epg(self): db_epg = DBepg(self.config) plugins_epg = db_epg.get_epg_names() html_option = ''.join([ '<td nowrap>Plugin: <select id="name" name="name"</select>', '<option value="">ALL</option>', ]) for name in plugins_epg: html_option = ''.join([ html_option, '<option value="', name['namespace'], '">', name['namespace'], '</option>', ]) return ''.join([html_option, '</select></td></tr>'])
class EPG: def __init__(self, _plugins, _namespace, _instance): self.logger = logging.getLogger(__name__) self.config = _plugins.config_obj.data self.epg_db = DBepg(self.config) self.channels_db = DBChannels(self.config) self.plugins = _plugins self.namespace = _namespace self.instance = _instance def get_epg_xml(self): self.plugins.refresh_epg(self.namespace, self.instance) xml_out = self.gen_header_xml() channel_list = self.channels_db.get_channels(self.namespace, self.instance) self.gen_channel_xml(xml_out, channel_list) self.epg_db.init_get_query(self.namespace, self.instance) day_data = self.epg_db.get_next_row() while day_data: self.gen_program_xml(xml_out, day_data) day_data = self.epg_db.get_next_row() epg_dom = minidom.parseString( ElementTree.tostring(xml_out, encoding='UTF-8', method='xml')) return epg_dom.toprettyxml() def gen_channel_xml(self, _et_root, _channel_list): for sid, ch_data in _channel_list.items(): c_out = self.sub_el(_et_root, 'channel', id=sid) self.sub_el( c_out, 'display-name', text='%s%s%s %s' % (self.config['epg']['epg_prefix'], ch_data['display_number'], self.config['epg']['epg_suffix'], ch_data['display_name'])) self.sub_el( c_out, 'display-name', text='%s%s%s %s' % (self.config['epg']['epg_prefix'], ch_data['display_number'], self.config['epg']['epg_suffix'], ch_data['json']['callsign'])) self.sub_el(c_out, 'display-name', text=ch_data['display_number']) self.sub_el(c_out, 'display-name', text=ch_data['json']['callsign']) self.sub_el(c_out, 'display-name', text=ch_data['display_name']) if self.config['locast']['epg_channel_icon']: self.sub_el(c_out, 'icon', src=ch_data['thumbnail']) return _et_root def gen_program_xml(self, _et_root, _prog_list): for prog_data in _prog_list: prog_out = self.sub_el(_et_root, 'programme', start=prog_data['start'], stop=prog_data['stop'], channel=prog_data['channel']) if prog_data['title']: self.sub_el(prog_out, 'title', text=prog_data['title']) if prog_data['subtitle']: self.sub_el(prog_out, 'sub-title', text=prog_data['subtitle']) descr_add = '' if self.config['epg']['description'] == 'extend': if prog_data['formatted_date']: descr_add += '(' + prog_data['formatted_date'] + ') ' if prog_data['genres']: descr_add += ' / '.join(prog_data['genres']) + ' / ' if prog_data['se_common']: descr_add += prog_data['se_common'] descr_add += '\n' + prog_data['desc'] elif self.config['epg']['description'] == 'brief': descr_add = prog_data['short_desc'] elif self.config['epg']['description'] == 'normal': descr_add = prog_data['desc'] else: self.logger.warning( 'Config value [epg][description] is invalid: ' + self.config['epg']['description']) self.sub_el(prog_out, 'desc', text=descr_add) if prog_data['video_quality']: video_out = self.sub_el(prog_out, 'video') self.sub_el(video_out, 'quality', prog_data['video_quality']) if prog_data['air_date']: self.sub_el(prog_out, 'date', lang='en', text=prog_data['air_date']) self.sub_el(prog_out, 'length', units='minutes', text=str(prog_data['length'])) if prog_data['genres']: for f in prog_data['genres']: if self.config['epg']['genre'] == 'normal': pass elif self.config['epg']['genre'] == 'tvheadend': if f in epg_category.TVHEADEND.keys(): f = epg_category.TVHEADEND[f] else: self.logger.warning( 'Config value [epg][genre] is invalid: ' + self.config['epg']['genre']) self.sub_el(prog_out, 'category', lang='en', text=f.strip()) if prog_data['icon'] and self.config['locast']['epg_program_icon']: self.sub_el(prog_out, 'icon', src=prog_data['icon']) if prog_data['rating']: r = ElementTree.SubElement(prog_out, 'rating') self.sub_el(r, 'value', text=prog_data['rating']) if prog_data['se_common']: self.sub_el(prog_out, 'episode-num', system='common', text=prog_data['se_common']) self.sub_el(prog_out, 'episode-num', system='xmltv_ns', text=prog_data['se_xmltv_ns']) self.sub_el(prog_out, 'episode-num', system='SxxExx', text=prog_data['se_common']) if prog_data['is_new']: self.sub_el(prog_out, 'new') def gen_header_xml(self): xml_out = ElementTree.Element('tv') xml_out.set('source-info-url', 'https://www.locast.org') xml_out.set('source-info-name', 'locast.org') xml_out.set('generator-info-name', 'locastepg') xml_out.set('generator-info-url', 'github.com/rocky4546/tvheadend-locast') xml_out.set('generator-special-thanks', 'locast2plex') return xml_out def sub_el(self, parent, name, text=None, **kwargs): el = ElementTree.SubElement(parent, name, **kwargs) if text: el.text = text return el
class EPG: logger = None def __init__(self, _locast_instance): self.locast_instance = _locast_instance self.instance = _locast_instance.instance self.db = DBepg(self.locast_instance.config_obj.data) self.config_section = _locast_instance.config_section self.episode_adj = int(self.locast_instance.config_obj.data \ [self.locast_instance.config_section]['epg-episode_adjustment']) if self.locast_instance.location.has_dma_changed: self.db.del_instance(self.locast_instance.locast.name, self.instance) def refresh_epg(self): if not self.is_refresh_expired(): self.logger.debug('EPG still new for {} {}, not refreshing'.format( self.locast_instance.locast.name, self.instance)) return if not self.locast_instance.config_obj.data[ self.locast_instance.config_section]['epg-enabled']: self.logger.debug('EPG collection not enabled for {} {}'.format( self.locast_instance.locast.name, self.instance)) return forced_dates, aging_dates = self.dates_to_pull() self.db.del_old_programs(self.locast_instance.locast.name, self.instance) for epg_day in forced_dates: self.refresh_programs(epg_day, False) for epg_day in aging_dates: self.refresh_programs(epg_day, True) def is_refresh_expired(self): """ Makes it so the minimum epg update rate can only occur based on epg_min_refresh_rate """ checking_date = datetime.date.today() last_update = self.db.get_last_update(self.locast_instance.locast.name, self.instance, checking_date) if not last_update: return True expired_date = datetime.datetime.now( ) - datetime.timedelta(seconds=self.locast_instance.config_obj.data[ self.locast_instance.locast.name.lower()]['epg-min_refresh_rate']) if last_update < expired_date: return True return False def dates_to_pull(self): todaydate = datetime.date.today() forced_days = [] aging_days = [] for x in range( 0, self.locast_instance.config_obj.data[ self.locast_instance.locast.name.lower()]['epg-days']): if x < self.locast_instance.config_obj.data[ self.locast_instance.locast.name.lower( )]['epg-days_start_refresh']: forced_days.append(todaydate + datetime.timedelta(days=x)) else: aging_days.append(todaydate + datetime.timedelta(days=x)) return forced_days, aging_days @handle_json_except @handle_url_except def get_url_data(self, _day): url = ( 'https://api.locastnet.org/api/watch/epg/{}?startTime={}'.format( self.locast_instance.location.dma, _day.isoformat())) # pull if successful may not contain any listing data (len=0) req = urllib.request.Request(url) with urllib.request.urlopen(req) as resp: results = json.load(resp) if len(results[0]['listings']) == 0: self.logger.warning( 'EPG Days to collect is too high. {} has no data'.format( _day.isoformat())) return None else: return results def refresh_programs(self, _day, use_cache): if use_cache: last_update = self.db.get_last_update( self.locast_instance.locast.name, self.instance, _day) if last_update: todaydate = datetime.datetime.now() use_cache_after = todaydate - datetime.timedelta( days=self.locast_instance.config_obj.data[ self.locast_instance.locast.name.lower()] ['epg-days_aging_refresh']) if last_update > use_cache_after: return program_list = [] json_data = self.get_url_data(_day) if json_data is not None: for ch_data in json_data: for listing_data in ch_data['listings']: program_json = self.get_program(listing_data) program_list.append(program_json) # push the update to the database self.db.save_program_list(self.locast_instance.locast.name, self.instance, _day, program_list) self.logger.debug('Refreshing EPG data for {}:{} day:{}'.format( self.locast_instance.locast.name, self.instance, _day)) def get_program(self, _program_data): # https://github.com/XMLTV/xmltv/blob/master/xmltv.dtd sid = str(_program_data['stationId']) start_time = utils.tm_local_parse(_program_data['startTime']) dur_min = int(_program_data['duration'] / 60) end_time = utils.tm_local_parse(_program_data['startTime'] + _program_data['duration'] * 1000) title = _program_data['title'] entity_type = _program_data['entityType'] prog_id = _program_data['programId'] if 'description' not in _program_data.keys(): description = 'Unavailable' elif not _program_data['description']: description = 'Unavailable None' else: description = _program_data['description'] if 'shortDescription' not in _program_data.keys(): short_desc = description else: short_desc = _program_data['shortDescription'] video_quality = None if 'videoProperties' in _program_data.keys(): if 'HD' in _program_data['videoProperties']: video_quality = 'HDTV' cc = False if 'audioProperties' in _program_data.keys(): if 'CC' in _program_data['audioProperties']: cc = True live = False if 'videoProperties' in _program_data.keys(): if 'Live' in _program_data['videoProperties']: live = True finale = False if 'videoProperties' in _program_data.keys(): if 'Finale' in _program_data['videoProperties']: finale = True premiere = False if 'videoProperties' in _program_data.keys(): if 'Premiere' in _program_data['videoProperties']: premiere = True if _program_data[ 'entityType'] == 'Movie' and 'releaseYear' in _program_data.keys( ): air_date = str(_program_data['releaseYear']) formatted_date = air_date elif 'airdate' in _program_data.keys(): air_date = utils.date_parse(_program_data['airdate'], '%Y%m%d') formatted_date = utils.date_parse(_program_data['airdate'], '%Y/%m/%d') elif 'gameDate' in _program_data.keys(): date_obj = datetime.datetime.strptime(_program_data['gameDate'], '%Y-%m-%d') air_date = utils.date_obj_parse(date_obj, '%Y%m%d') formatted_date = utils.date_obj_parse(date_obj, '%Y/%m/%d') elif 'releaseDate' in _program_data.keys(): air_date = utils.date_parse(_program_data['releaseDate'], '%Y%m%d') formatted_date = utils.date_parse(_program_data['releaseDate'], '%Y/%m/%d') else: air_date = None formatted_date = None icon = _program_data['preferredImage'] if 'rating' in _program_data.keys(): rating = _program_data['rating'] else: rating = None if 'isNew' in _program_data.keys() and _program_data['isNew']: is_new = True else: is_new = False if 'genres' in _program_data.keys(): genres = [x.strip() for x in _program_data['genres'].split(',')] else: genres = None if 'directors' in _program_data.keys(): directors = _program_data['directors'].split(",") else: directors = None if 'topCast' in _program_data.keys(): actors = _program_data['topCast'].split(",") else: actors = None if 'seasonNumber' in _program_data.keys(): season = _program_data['seasonNumber'] else: season = None if 'episodeNumber' in _program_data.keys(): episode = _program_data['episodeNumber'] + self.episode_adj else: progid_episode = int(prog_id[-3:]) if progid_episode > 0: episode = progid_episode + self.episode_adj else: episode = None if (season is None) and (episode is None): se_common = None se_xmltv_ns = None se_prog_id = None elif (season is not None) and (episode is not None): se_common = 'S%02dE%02d' % (season, episode) se_xmltv_ns = ''.join( [str(season - 1), '.', str(episode - 1), '.0/1']) se_prog_id = _program_data['programId'][:10] + '.' + _program_data[ 'programId'][10:] elif (season is None) and (episode is not None): se_common = 'S%02dE%02d' % (0, episode) se_xmltv_ns = ''.join(['0', '.', str(episode - 1), '.0/1']) se_prog_id = _program_data['programId'][:10] + '.' + _program_data[ 'programId'][10:] else: # (season is not None) and (episode is None): se_common = 'S%02dE%02d' % (season, 0) se_xmltv_ns = ''.join([str(season - 1), '.', '0', '.0/1']) se_prog_id = '' if season is not None: subtitle = 'S%02dE%02d ' % (season, episode) elif episode is not None: subtitle = 'E%02d ' % (episode) else: subtitle = '' if 'episodeTitle' in _program_data.keys(): subtitle += _program_data['episodeTitle'] elif 'eventTitle' in _program_data.keys(): subtitle += _program_data['eventTitle'] elif _program_data[ 'entityType'] == 'Movie' and 'releaseYear' in _program_data.keys( ): subtitle = 'Movie: {}'.format(_program_data['releaseYear']) else: subtitle = None json_result = { 'channel': sid, 'progid': prog_id, 'start': start_time, 'stop': end_time, 'length': dur_min, 'title': title, 'subtitle': subtitle, 'entity_type': entity_type, 'desc': description, 'short_desc': short_desc, 'video_quality': video_quality, 'cc': cc, 'live': live, 'finale': finale, 'premiere': premiere, 'air_date': air_date, 'formatted_date': formatted_date, 'icon': icon, 'rating': rating, 'is_new': is_new, 'genres': genres, 'directors': directors, 'actors': actors, 'season': season, 'episode': episode, 'se_common': se_common, 'se_xmltv_ns': se_xmltv_ns, 'se_progid': se_prog_id } return json_result
class EPG: # https://github.com/XMLTV/xmltv/blob/master/xmltv.dtd def __init__(self, _webserver): self.logger = logging.getLogger(__name__) self.webserver = _webserver self.config = _webserver.plugins.config_obj.data self.epg_db = DBepg(self.config) self.channels_db = DBChannels(self.config) self.plugins = _webserver.plugins self.namespace = _webserver.query_data['name'] self.instance = _webserver.query_data['instance'] def get_epg_xml(self, _webserver): try: _webserver.do_dict_response({ 'code': 200, 'headers': { 'Content-type': 'application/xml; Transfer-Encoding: chunked' }, 'text': None }) self.plugins.refresh_epg(self.namespace, self.instance) xml_out = self.gen_header_xml() channel_list = self.channels_db.get_channels( self.namespace, self.instance) self.gen_channel_xml(xml_out, channel_list) self.write_xml(xml_out, keep_xml_prolog=True) xml_out = None self.epg_db.init_get_query(self.namespace, self.instance) day_data, ns, inst = self.epg_db.get_next_row() self.prog_processed = [] data_written = False while day_data: xml_out = self.gen_minimal_header_xml() self.gen_program_xml(xml_out, day_data, channel_list, ns, inst) did_write_data = self.write_xml(xml_out) data_written = did_write_data or data_written day_data, ns, inst = self.epg_db.get_next_row() self.epg_db.close_query() if data_written: self.webserver.wfile.write(b'</tv>\r\n') else: self.webserver.wfile.write(b'<tv/>\r\n') self.webserver.wfile.flush() except MemoryError as e: self.logger.error('MemoryError parsing large xml') raise e xml_out = None def write_xml(self, _xml, keep_xml_prolog=False): if self.config['epg']['epg_prettyprint']: if not keep_xml_prolog: epg_dom = ElementTree.tostring(_xml) if len(epg_dom) < 20: return False epg_dom = minidom.parseString(epg_dom).toprettyxml()[27:-6] else: epg_dom = minidom.parseString( ElementTree.tostring(_xml, encoding='UTF-8', method='xml')).toprettyxml()[:-6] if len(epg_dom) < 250: return False self.webserver.wfile.write(epg_dom.encode()) else: if not keep_xml_prolog: epg_dom = ElementTree.tostring(_xml)[4:-5] if len(epg_dom) < 10: return False else: epg_dom = ElementTree.tostring(_xml)[:-5] if len(epg_dom) < 250: return False self.webserver.wfile.write(epg_dom + b'\r\n') epg_dom = None return True def gen_channel_xml(self, _et_root, _channel_list): sids_processed = [] for sid, sid_data_list in _channel_list.items(): if sid in sids_processed: continue sids_processed.append(sid) for ch_data in sid_data_list: if not ch_data['enabled']: break updated_chnum = utils.wrap_chnum(ch_data['display_number'], ch_data['namespace'], ch_data['instance'], self.config) c_out = EPG.sub_el(_et_root, 'channel', id=sid) EPG.sub_el(c_out, 'display-name', _text='%s %s' % (updated_chnum, ch_data['display_name'])) EPG.sub_el(c_out, 'display-name', _text='%s %s' % (updated_chnum, ch_data['json']['callsign'])) EPG.sub_el(c_out, 'display-name', _text=updated_chnum) EPG.sub_el(c_out, 'display-name', _text=ch_data['json']['callsign']) EPG.sub_el(c_out, 'display-name', _text=ch_data['display_name']) if self.config['epg']['epg_channel_icon']: EPG.sub_el(c_out, 'icon', src=ch_data['thumbnail']) break return _et_root def gen_program_xml(self, _et_root, _prog_list, _channel_list, _ns, _inst): for prog_data in _prog_list: proginfo = prog_data['start'] + prog_data['channel'] if proginfo in self.prog_processed: continue skip = False try: for ch_data in _channel_list[prog_data['channel']]: if ch_data['namespace'] == _ns \ and ch_data['instance'] == _inst \ and not ch_data['enabled']: skip = True break except KeyError: skip = True if skip: continue self.prog_processed.append(proginfo) prog_out = EPG.sub_el(_et_root, 'programme', start=prog_data['start'], stop=prog_data['stop'], channel=prog_data['channel']) if prog_data['title']: EPG.sub_el(prog_out, 'title', lang='en', _text=prog_data['title']) if prog_data['subtitle']: EPG.sub_el(prog_out, 'sub-title', lang='en', _text=prog_data['subtitle']) descr_add = '' if self.config['epg']['description'] == 'extend': if prog_data['formatted_date']: descr_add += '(' + prog_data['formatted_date'] + ') ' if prog_data['genres']: descr_add += ' / '.join(prog_data['genres']) + ' / ' if prog_data['se_common']: descr_add += prog_data['se_common'] descr_add += '\n' + prog_data['desc'] elif self.config['epg']['description'] == 'brief': descr_add = prog_data['short_desc'] elif self.config['epg']['description'] == 'normal': descr_add = prog_data['desc'] else: self.logger.warning( 'Config value [epg][description] is invalid: ' + self.config['epg']['description']) EPG.sub_el(prog_out, 'desc', lang='en', _text=descr_add) if prog_data['video_quality']: video_out = EPG.sub_el(prog_out, 'video') EPG.sub_el(video_out, 'quality', prog_data['video_quality']) if prog_data['air_date']: EPG.sub_el(prog_out, 'date', _text=prog_data['air_date']) EPG.sub_el(prog_out, 'length', units='minutes', _text=str(prog_data['length'])) if prog_data['genres']: for f in prog_data['genres']: if self.config['epg']['genre'] == 'normal': pass elif self.config['epg']['genre'] == 'tvheadend': if f in epg_category.TVHEADEND.keys(): f = epg_category.TVHEADEND[f] else: self.logger.warning( 'Config value [epg][genre] is invalid: ' + self.config['epg']['genre']) EPG.sub_el(prog_out, 'category', lang='en', _text=f.strip()) if prog_data['icon'] and self.config['epg']['epg_program_icon']: EPG.sub_el(prog_out, 'icon', src=prog_data['icon']) if prog_data['actors'] or prog_data['directors']: r = ElementTree.SubElement(prog_out, 'credits') if prog_data['directors']: for actor in prog_data['directors']: EPG.sub_el(r, 'producer', _text=actor) if prog_data['actors']: for actor in prog_data['actors']: EPG.sub_el(r, 'presenter', _text=actor) if prog_data['rating']: r = ElementTree.SubElement(prog_out, 'rating') EPG.sub_el(r, 'value', _text=prog_data['rating']) if prog_data['se_common']: EPG.sub_el(prog_out, 'episode-num', system='common', _text=prog_data['se_common']) EPG.sub_el(prog_out, 'episode-num', system='dd_progid', _text=prog_data['se_progid']) EPG.sub_el(prog_out, 'episode-num', system='xmltv_ns', _text=prog_data['se_xmltv_ns']) EPG.sub_el(prog_out, 'episode-num', system='SxxExx', _text=prog_data['se_common']) if prog_data['is_new']: EPG.sub_el(prog_out, 'new') else: EPG.sub_el(prog_out, 'previously-shown') if prog_data['cc']: EPG.sub_el(prog_out, 'subtitles', type='teletext') if prog_data['premiere']: EPG.sub_el(prog_out, 'premiere') def gen_header_xml(self): if self.namespace is None: website = utils.CABERNET_URL name = utils.CABERNET_NAME else: website = self.plugins.plugins[ self.namespace].plugin_settings['website'] name = self.plugins.plugins[self.namespace].plugin_settings['name'] xml_out = ElementTree.Element('tv') xml_out.set('source-info-url', website) xml_out.set('source-info-name', name) xml_out.set('generator-info-name', utils.CABERNET_NAME) xml_out.set('generator-info-url', utils.CABERNET_URL) xml_out.set('generator-special-thanks', 'locast2plex') return xml_out def gen_minimal_header_xml(self): return ElementTree.Element('tv') @staticmethod def sub_el(_parent, _name, _text=None, **kwargs): el = ElementTree.SubElement(_parent, _name, **kwargs) if _text: el.text = _text return el
def __init__(self, _locast): self.locast = _locast self.db = DBepg(self.locast.config)