Files
DevOps/Kodi/Lenovo/addons/plugin.video.pseudotv.live/resources/lib/xmltvs.py

558 lines
28 KiB
Python

# Copyright (C) 2024 Lunatixz
#
#
# This file is part of PseudoTV Live.
#
# PseudoTV Live is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PseudoTV Live is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PseudoTV Live. If not, see <http://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import xmltv
from globals import *
from seasonal import Seasonal
#todo check for empty recordings/channel meta and trigger refresh/rebuild empty xmltv via Kodi json rpc?
class XMLTVS:
def __init__(self):
self.XMLTVDATA = self._load()
def log(self, msg, level=xbmc.LOGDEBUG):
return log('%s: %s'%(self.__class__.__name__,msg),level)
def _load(self, file: str=XMLTVFLEPATH) -> dict:
self.log('_load')
channels, recordings = self.cleanSelf(self.loadChannels(file),'id')
return {'data' : self.loadData(file),
'channels' : channels,
'recordings' : recordings,
'programmes' : self.cleanSelf(self.loadProgrammes(file),'channel')}
def _save(self, file: str=XMLTVFLEPATH, reset: bool=True) -> bool:
self.log('_save, file = %s'%(file))
if reset: data = self.resetData()
else: data = self.XMLTVDATA['data']
with FileLock():
writer = xmltv.Writer(encoding = DEFAULT_ENCODING,
date = data['date'],
source_info_url = self.cleanString(data['source-info-url']),
source_info_name = self.cleanString(data['source-info-name']),
generator_info_url = self.cleanString(data['generator-info-url']),
generator_info_name = self.cleanString(data['generator-info-name']))
self.XMLTVDATA['programmes'] = self.sortProgrammes(self.XMLTVDATA['programmes'])
self.XMLTVDATA['channels'] = self.cleanChannels(self.sortChannels(self.XMLTVDATA['channels']) , self.XMLTVDATA['programmes'])
self.XMLTVDATA['recordings'] = self.cleanChannels(self.sortChannels(self.XMLTVDATA['recordings']), self.XMLTVDATA['programmes'])
for channel in (self.XMLTVDATA['recordings'] + self.XMLTVDATA['channels']):
writer.addChannel(channel)
for program in self.XMLTVDATA['programmes']:
writer.addProgramme(program)
try:
self.log('_save, saving to %s'%(file))
fle = FileAccess.open(file, "w")
writer.write(fle, pretty_print=True)
fle.close()
except Exception as e:
self.log("_save, failed!", xbmc.LOGERROR)
DIALOG.notificationDialog(LANGUAGE(32000))
self.buildGenres()
return self._reload()
def _reload(self) -> bool:
self.log('_reload')
self.__init__()
return True
def _error(self, name, file, e):
#hacky; try to log malformed xml's by printing error position..
if not 'no element found: line 1, column 0' in str(e):
try:
match = re.compile(r'line\ (.*?),\ column\ (.*)', re.IGNORECASE).search(str(e))
if match:
fle = FileAccess.open(file,'r')
file = fle.readlines()
fle.close()
self.log('%s, failed! parser error %s\nLine: %s\n Error: %s'%(name,e,file[int(match.group(1))],file[int(match.group(1))][int(match.group(2))-5:]), xbmc.LOGERROR)
else: raise Exception('no parser match %s'%(str(e)))
except Exception as en: self.log('%s, failed! %s\n%s'%(name,e,en), xbmc.LOGERROR)
def resetData(self):
self.log('resetData')
return {'date' : datetime.datetime.fromtimestamp(float(time.time())).strftime(DTFORMAT),
'generator-info-name' : self.cleanString('%s Guidedata'%(ADDON_NAME)),
'generator-info-url' : self.cleanString(ADDON_ID),
'source-info-name' : self.cleanString(ADDON_NAME),
'source-info-url' : self.cleanString(ADDON_ID)}
def loadData(self, file: str=XMLTVFLEPATH) -> dict:
self.log('loadData, file = %s'%file)
try:
fle = FileAccess.open(file, 'r')
data = (xmltv.read_data(fle) or self.resetData())
fle.close()
return data
except Exception as e:
self._error('loadData',file,e)
return self.resetData()
def loadChannels(self, file: str=XMLTVFLEPATH) -> list:
self.log('loadChannels, file = %s'%file)
try:
fle = FileAccess.open(file, 'r')
data = (xmltv.read_channels(fle) or [])
fle.close()
return data
except Exception as e:
self._error('loadChannels',file,e)
return []
def loadProgrammes(self, file: str=XMLTVFLEPATH) -> list:
self.log('loadProgrammes, file = %s'%file)
try:
fle = FileAccess.open(file, 'r')
data = (xmltv.read_programmes(fle) or [])
fle.close()
return data
except Exception as e:
self._error('loadProgrammes',file,e)
return []
def loadStopTimes(self, channels: list=[], programmes: list=[], fallback=None):
if not channels: channels = self.getChannels()
if not programmes: programmes = self.getProgrammes()
if not fallback: fallback = datetime.datetime.fromtimestamp(roundTimeDown(getUTCstamp(),offset=60)).strftime(DTFORMAT)
for channel in channels:
try:
firstStart = min([program['start'] for program in programmes if program['channel'] == channel['id']], default=fallback)
lastStop = max([program['stop'] for program in programmes if program['channel'] == channel['id']], default=fallback)
self.log('loadStopTimes, channel = %s, first-start = %s, last-stop = %s, fallback = %s'%(channel['id'],firstStart,lastStop,fallback))
if firstStart > fallback: raise Exception('First start-time in the future, rebuild channel with fallback')
yield channel['id'],datetime.datetime.timestamp(strpTime(lastStop, DTFORMAT))
except Exception as e:
self.log("loadStopTimes, channel = %s failed!\nMalformed XMLTV channel/programmes %s! rebuilding channel with default stop-time %s"%(channel.get('id'),e,fallback), xbmc.LOGWARNING)
yield channel['id'],datetime.datetime.timestamp(strpTime(fallback, DTFORMAT))
def hasProgrammes(self, channels: list=[], programmes: list=[], now=None):
if not channels: channels = self.getChannels()
if not programmes: programmes = self.getProgrammes()
if not now: now = datetime.datetime.fromtimestamp(roundTimeDown(getUTCstamp(),offset=60)).strftime(DTFORMAT)
for channel in channels:
try:
valid = False
lastStop = max([program['stop'] for program in programmes if program['channel'] == channel['id']], default=now)
if lastStop > now: valid = True
self.log('hasProgrammes, channel = %s, valid = %s'%(channel['id'],valid))
yield channel['id'],valid
except Exception as e:
self.log("hasProgrammes, channel = %s failed!\nMalformed XMLTV channel/programmes %s! valid = False %s"%(channel.get('id'),e), xbmc.LOGWARNING)
yield channel['id'],False
def cleanString(self, text: str) -> str:
if text == ', ' or not text: text = LANGUAGE(32020) #"Unavailable"
return bytes(text,DEFAULT_ENCODING).decode(DEFAULT_ENCODING,'ignore')
def cleanSelf(self, items: list, key: str='id', slug: str='@%s'%(slugify(ADDON_NAME))) -> list: # remove imports (Non PseudoTV Live), key = {'id':channels,'channel':programmes}
if not slug: return items
channels = list([item for item in items if item.get(key,'').endswith(slug) and len(item.get(key,'').replace(slug,'')) == 32])
recordings = list([item for item in items if item.get(key,'').endswith(slug) and len(item.get(key,'').replace(slug,'')) == 16])
if key == 'id': #stations
self.log('cleanSelf, slug = %s, key = %s: returning channels = %s, recordings = %s'%(slug,key,len(channels),len(recordings)))
return self.sortChannels(setDictLST(channels)), self.sortChannels(setDictLST(recordings))
else: #programmes
programmes = self.cleanProgrammes(channels) + recordings
self.log('cleanSelf, slug = %s, key = %s: returning programmes = %s'%(slug,key,len(programmes)))
return self.sortProgrammes(programmes)
def cleanChannels(self, channels: list, programmes: list) -> list: # remove stations with no guidedata
stations = list(set([program.get('channel') for program in programmes]))
tmpChannels = [channel for station in stations for channel in channels if channel.get('id') == station]
self.log('cleanChannels, before = %s, after = %s'%(len(channels),len(tmpChannels)))
return tmpChannels
def cleanProgrammes(self, programmes: list) -> list:
now = (datetime.datetime.fromtimestamp(float(getUTCstamp())) - datetime.timedelta(days=MIN_GUIDEDAYS)) #allow some old programmes to avoid empty cells
holiday = Seasonal().getHoliday()
def __filterProgrammes(program):
citem = decodePlot(program.get('desc',([{}],''))[0][0]).get('citem',{})
if citem.get('holiday') and citem.get('holiday',{}).get('name',str(random.random())) != holiday.get('name',str(random.random())): return None
elif (strpTime(program.get('stop',now).rstrip(),DTFORMAT) < now): return None # remove expired content, ignore "recordings" ie. media=True
return program
tmpProgrammes = [prog for prog in [__filterProgrammes(program) for program in programmes] if prog is not None]
self.log('cleanProgrammes, before = %s, after = %s'%(len(programmes),len(tmpProgrammes)))
return tmpProgrammes
def sortChannels(self, channels: list) -> list:
try: return sorted(channels, key=itemgetter('display-name'))
except: return channels
def sortProgrammes(self, programmes: list) -> list:
try:
programmes.sort(key=itemgetter('start'))
programmes.sort(key=itemgetter('channel'))
self.log('sortProgrammes, programmes = %s'%(len(programmes)))
return programmes
except Exception as e:
self.log("sortProgrammes, failed! %s"%(e), xbmc.LOGERROR)
return []
def getRecordings(self) -> list:
self.log('getRecordings')
return self.sortChannels(self.XMLTVDATA.get('recordings',[]))
def getChannels(self) -> list:
self.log('getChannels')
return self.sortChannels(self.XMLTVDATA.get('channels',[]))
def getProgrammes(self) -> list:
self.log('getProgrammes')
return self.sortProgrammes(self.XMLTVDATA.get('programmes',[]))
def findChannel(self, citem: dict, channels: list=[]) -> tuple:
if not channels: channels = self.getChannels()
for idx, eitem in enumerate(channels):
if citem.get('id') == eitem.get('id',str(random.random())):
self.log('findChannel, found citem = %s'%(eitem))
return idx, eitem
return None, {}
def findRecording(self, ritem: dict, recordings: list=[]) -> tuple:
if not recordings: recordings = self.getRecordings()
for idx, eitem in enumerate(recordings):
if (ritem.get('id') == eitem.get('id',str(random.random()))) or (ritem.get('name','').lower() == eitem.get('display-name')[0][0].lower()):
self.log('findRecording, found ritem = %s'%(eitem))
return idx, eitem
return None, {}
def getProgramItem(self, citem: dict, fItem: dict) -> dict:
''' Convert fileItem to Programme (XMLTV) item '''
item = {}
item['channel'] = citem['id']
item['radio'] = citem['radio']
item['start'] = fItem['start']
item['stop'] = fItem['stop']
item['title'] = fItem['label']
item['desc'] = fItem['plot']
item['length'] = fItem['duration']
item['sub-title'] = (fItem.get('episodetitle') or '')
item['categories'] = (fItem.get('genre') or ['Undefined'])[:5]#trim list to five
item['type'] = fItem.get('type','video')
item['new'] = int(fItem.get('playcount','1')) == 0
item['thumb'] = cleanImage((getThumb(fItem,EPG_ARTWORK) or {0:FANART,1:COLOR_LOGO}[EPG_ARTWORK])) #unify thumbnail by user preference
fItem.get('art',{})['thumb'] = cleanImage(getThumb(fItem,{0:1,1:0}[EPG_ARTWORK]) or {0:FANART,1:COLOR_LOGO}[{0:1,1:0}[EPG_ARTWORK]]) #unify thumbnail artwork, opposite of EPG_Artwork
item['date'] = fItem.get('premiered','')
item['catchup-id'] = VOD_URL.format(addon=ADDON_ID,title=quoteString(item['title']),chid=quoteString(citem['id']),vid=quoteString(encodeString((fItem.get('originalfile') or fItem.get('file','')))),name=quoteString(citem['name']))
fItem['catchup-id'] = item['catchup-id']
if (item['type'] != 'movie' and ((fItem.get("season",0) > 0) and (fItem.get("episode",0) > 0))):
item['episode-num'] = {'xmltv_ns':'%s.%s'%(fItem.get("season",1)-1,fItem.get("episode",1)-1), # todo support totaleps <episode-num system="xmltv_ns">..44/47</episode-num>https://github.com/kodi-pvr/pvr.iptvsimple/pull/884
'onscreen':'S%sE%s'%(str(fItem.get("season",0)).zfill(2),str(fItem.get("episode",0)).zfill(2))}
item['rating'] = cleanMPAA(fItem.get('mpaa') or 'NA')
item['stars'] = (fItem.get('rating') or '0')
item['writer'] = fItem.get('writer',[])[:5] #trim list to five
item['director'] = fItem.get('director',[])[:5] #trim list to five
item['actor'] = ['%s - %s'%(actor.get('name'),actor.get('role',LANGUAGE(32020))) for actor in fItem.get('cast',[])[:5] if actor.get('name')]
fItem['citem'] = citem #channel item (stale data due to xmltv storage) use for reference
item['fitem'] = fItem #raw kodi fileitem/listitem, contains citem both passed through 'plot' xmltv param.
streamdetails = fItem.get('streamdetails',{})
if streamdetails:
item['subtitle'] = list(set([sub.get('language','') for sub in streamdetails.get('subtitle',[]) if sub.get('language')]))
item['language'] = ', '.join(list(set([aud.get('language','') for aud in streamdetails.get('audio',[]) if aud.get('language')])))
item['audio'] = True if True in list(set([aud.get('codec','') for aud in streamdetails.get('audio',[]) if aud.get('channels',0) >= 2])) else False
item.setdefault('video',{})['aspect'] = list(set([vid.get('aspect','') for vid in streamdetails.get('video',[]) if vid.get('aspect','')]))
return item
def addRecording(self, ritem: dict, fitem: dict):
self.log('addRecording = %s'%(ritem.get('id')))
sitem = ({'id' : ritem['id'],
'display-name' : [(self.cleanString(ritem['name']), LANG)],
'icon' : [{'src':ritem['logo']}]})
self.log('addRecording, sitem = %s'%(sitem))
idx, recording = self.findRecording(ritem)
if idx is None:
self.XMLTVDATA['recordings'].append(sitem)
else:
self.XMLTVDATA['recordings'][idx] = sitem # replace existing channel meta
fitem['start'] = getUTCstamp()
fitem['stop'] = fitem['start'] + fitem['duration']
if self.addProgram(ritem['id'],self.getProgramItem(ritem,fitem),encodeDESC=True):
return self._save()
def addChannel(self, citem: dict) -> bool:
mitem = ({'id' : citem['id'],
'display-name' : [(self.cleanString(citem['name']), LANG)],
'icon' : [{'src':citem['logo']}]})
self.log('addChannel, mitem = %s'%(mitem))
idx, channel = self.findChannel(mitem)
if idx is None: self.XMLTVDATA['channels'].append(mitem)
else: self.XMLTVDATA['channels'][idx] = mitem # replace existing channel meta
return True
def addProgram(self, id: str, item: dict, encodeDESC: bool=True) -> bool:
pitem = {'channel' : id,
'category' : [(self.cleanString(genre.replace('Unknown','Undefined')),LANG) for genre in item['categories']],
'title' : [(self.cleanString(item['title']), LANG)],
'desc' : [(encodePlot(self.cleanString(item['desc']),item['fitem']), LANG) if encodeDESC else (self.cleanString(item['desc']), LANG)],
'stop' : (datetime.datetime.fromtimestamp(float(item['stop'])).strftime(DTFORMAT)),
'start' : (datetime.datetime.fromtimestamp(float(item['start'])).strftime(DTFORMAT)),
'icon' : [{'src': item['thumb']}],
'length' : {'units': 'seconds', 'length': str(item['length'])}}
if item.get('sub-title'):
pitem['sub-title'] = [(self.cleanString(item['sub-title']), LANG)]
if item.get('stars'):
pitem['star-rating'] = [{'value': '%s/10'%(int(round(float(item['stars']))))}]
if item.get('writer'):
pitem.setdefault('credits',{})['writer'] = [self.cleanString(writer) for writer in item['writer']]
if item.get('director'):
pitem.setdefault('credits',{})['director'] = [self.cleanString(director) for director in item['director']]
if item.get('actor'):
pitem.setdefault('credits',{})['actor'] = [self.cleanString(actor) for actor in item['actor']]
if item.get('catchup-id'):
pitem['catchup-id'] = item['catchup-id']
if item.get('date'):
try: pitem['date'] = (strpTime(item['date'], '%Y-%m-%d')).strftime('%Y%m%d')
except: pass
if item.get('new',False):
pitem['new'] = '' #write empty tag, tag == True
rating = item.get('rating','NA')
if rating != 'NA':
if rating.lower().startswith('tv'):
pitem['rating'] = [{'system': 'VCHIP', 'value': rating}]
else:
pitem['rating'] = [{'system': 'MPAA', 'value': rating}] #todo support international rating systems
if item.get('episode-num'):
pitem['episode-num'] = [(item['episode-num'].get('xmltv_ns',''), 'xmltv_ns'),
(item['episode-num'].get('onscreen',''), 'onscreen')]
if item.get('audio',False):
pitem['audio'] = [{'stereo': 'stereo'}]
# if item.get('video',{}):
# pitem['video'] = [{'aspect': item.get('video',{}).get('aspect')}]
# if item.get('language',''):
# pitem['language'] = [(item.get('language'), LANG)]
# if item.get('subtitle',[]):
# pitem['subtitles'] = [{'type': 'teletext', 'language': ('%s'%(sub), LANG)} for sub in item.get('subtitle',[])]
##### TODO #####
# 'country' : [('USA', LANG)],#todo
# 'premiere': (u'Not really. Just testing', u'en'),
self.log('addProgram = %s'%(pitem.get('channel')))
self.XMLTVDATA['programmes'].append(pitem)
return True
def clrProgrammes(self, citem: dict) -> bool:
self.XMLTVDATA['programmes'] = [program for program in self.XMLTVDATA['programmes'] if program.get('channel') != citem.get('id')]
self.log('clrProgrammes, removing channel %s programmes' % citem.get('id'))
return True
def delBroadcast(self, citem: dict) -> bool:# remove single channel and all programmes from XMLTVDATA
channels = self.XMLTVDATA['channels'].copy()
programmes = self.XMLTVDATA['programmes'].copy()
self.XMLTVDATA['channels'] = list([channel for channel in channels if channel.get('id') != citem.get('id')])
self.XMLTVDATA['programmes'] = list([program for program in programmes if program.get('channel') != citem.get('id')])
self.log('delBroadcast, removing channel %s; channels: before = %s, after = %s; programmes: before = %s, after = %s'%(citem.get('id'),len(channels),len(self.XMLTVDATA['channels']),len(programmes),len(self.XMLTVDATA['programmes'])))
return True
def delRecording(self, ritem: dict):
self.log('[%s] delRecording'%((ritem.get('id') or ritem.get('label'))))
recordings = self.XMLTVDATA['recordings'].copy()
programmes = self.XMLTVDATA['programmes'].copy()
idx, recording = self.findRecording(ritem)
if idx is not None:
self.XMLTVDATA['recordings'].pop(idx)
if not ritem.get('id'): ritem['id'] = recording['id']
self.XMLTVDATA['programmes'] = list([program for program in programmes if program.get('channel') != ritem.get('id')])
return self._save()
def importXMLTV(self, file: str, m3uChannels: dict={}):
self.log('importXMLTV, file = %s, m3uChannels = %s'%(file,len(m3uChannels)))
def matchChannel(channel, channels, programmes):
importChannels.extend(list([chan for chan in channels if chan.get('id') == channel.get('id')]))
importProgrammes.extend(list([prog for prog in programmes if prog.get('channel') == channel.get('id')]))
try:
if file.startswith('http'):
files = []
for file in file.split(','): #handle possible list.
url = file
file = os.path.join(TEMP_LOC,'%s'%(slugify(url)))
files.append(file)
setURL(url,file)
else:
files = [file]
for file in files:
importChannels, importProgrammes = [],[]
channels, programmes = self.loadChannels(file), self.loadProgrammes(file)
if m3uChannels: #filter imported programmes by m3uchannels list.
poolit(matchChannel)(m3uChannels, **{'channels':channels,'programmes':programmes})
else: #no filter, import everything!
importChannels = channels
importProgrammes = programmes
importChannels, importProgrammes = self.chkImport(importChannels, importProgrammes)
self.log('importXMLTV, found importChannels = %s, importProgrammes = %s from %s'%(len(importChannels),len(importProgrammes),file))
self.XMLTVDATA.get('channels',[]).extend(self.sortChannels(importChannels))
self.XMLTVDATA.get('programmes',[]).extend(self.sortProgrammes(importProgrammes))
except Exception as e: self.log("importXMLTV, failed! %s"%(e), xbmc.LOGERROR)
def chkImport(self, channels: list, programmes: list) -> tuple: # parse for empty programmes, inject single cell entry.
try:
def addSingleEntry(channel, start=None, length=10800): #create a single entry with min. channel meta, use as a filler.
if start is None: start = datetime.datetime.fromtimestamp(roundTimeDown(getUTCstamp(),offset=60))
pitem = {'channel' : channel.get('id'),
'title' : [(channel.get('display-name',[{'',LANG}])[0][0], LANG)],
'desc' : [(xbmc.getLocalizedString(161), LANG)],
'stop' : ((start + datetime.timedelta(seconds=length)).strftime(DTFORMAT)),
'start' : (start.strftime(DTFORMAT)),
'icon' : [{'src': (channel.get('icon','') or [{}])[0].get('src')}],
'length' : {'units': 'seconds', 'length': str(length)}}
self.log('addSingleEntry = %s'%(pitem))
return pitem
def chkPrograms(channel):
for program in programmes:
if channel.get('id') == program.get('channel'):
try: return tmpChannels.remove(channel)
except: continue
tmpChannels = channels.copy()
poolit(chkPrograms)(channels)
for channel in tmpChannels: programmes.append(addSingleEntry(channel)) #append single cell entry for channels missing programmes
self.log("chkImport, added %s single entries"%(len(tmpChannels)))
except Exception as e:
self.log("chkImport, failed! %s"%(e), xbmc.LOGERROR)
return channels, programmes
def buildGenres(self):
self.log('buildGenres') #todo custom user color selector.
def parseGenres(plines):
epggenres = {}
for line in plines:
try:
names = line.childNodes[0].data
items = names.split('/')
data = {'genre':names,'name':names,'genreId':line.attributes['genreId'].value}
epggenres[names.lower()] = data
for item in items:
name = item.strip()
if name and not epggenres.get(name.lower()):
epgdata = data.copy()
epgdata['name'] = name
epggenres[name.lower()] = epgdata
except: continue
return epggenres
def matchGenres(programmes):
for program in programmes:
categories = [cat[0] for cat in program.get('category',[])]
catcombo = '/'.join(categories)
for category in categories:
match = epggenres.get(category.lower())
if match and not epggenres.get(catcombo.lower()):
epggenres[catcombo.lower()] = match
break
if FileAccess.exists(GENREFLE_DEFAULT):
try:
fle = FileAccess.open(GENREFLE_DEFAULT, "r")
dom = parse(fle)
fle.close()
epggenres = parseGenres(dom.getElementsByTagName('genre'))
matchGenres(self.XMLTVDATA.get('programmes',[]))
epggenres = dict(sorted(sorted(list(epggenres.items()), key=lambda v:v[1]['name']), key=lambda v:v[1]['genreId']))
doc = Document()
root = doc.createElement('genres')
doc.appendChild(root)
name = doc.createElement('name')
name.appendChild(doc.createTextNode('%s'%(ADDON_NAME)))
root.appendChild(name)
for key in epggenres:
gen = doc.createElement('genre')
gen.setAttribute('genreId',epggenres[key].get('genreId'))
gen.appendChild(doc.createTextNode(key.title().replace('Tv','TV').replace('Nr','NR').replace('Na','NA')))
root.appendChild(gen)
try:
with FileLock():
xmlData = FileAccess.open(GENREFLEPATH, "w")
xmlData.write(doc.toprettyxml(indent=' ',encoding=DEFAULT_ENCODING))
xmlData.close()
except Exception as e: self.log("buildGenres failed! %s"%(e), xbmc.LOGERROR)
except Exception as e: self.log("buildGenres failed! %s"%(e), xbmc.LOGERROR)