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: # 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