14__title__ =
"mashups_api - Simple-to-use Python interface to Mashups of RSS feeds and HTML video data"
15__author__=
"R.D. Vaughan"
17This python script is intended to perform a variety of utility functions to search and access text
18meta data, video and image URLs from various Internet sources. These routines process the RSS feeds and information into MNV standard channel, directory and item RSS XML files. The specific Mashups are specified through a user XML preference file usually found at
19"~/.mythtv/MythNetvision/userGrabberPrefs/xxxxMashup.xml" where "xxxx" is the specific mashup name matching the associated grabber name that calls these functions.
31import os, struct, sys, time, datetime, shutil,
urllib.request, urllib.parse, urllib.error
32from socket
import gethostname, gethostbyname
33from copy
import deepcopy
36from .mashups_exceptions
import (MashupsUrlError, MashupsHttpError, MashupsRssError, MashupsVideoNotFound, MashupsConfigFileError, MashupsUrlDownloadError)
40 """Wraps a stream with an encoder"""
49 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
50 if isinstance(obj, str):
52 self.
out.buffer.write(obj)
55 """Delegate everything but write to the stream"""
56 return getattr(self.
out, attr)
58if isinstance(sys.stdout, io.TextIOWrapper):
64 from io
import StringIO
65 from lxml
import etree
67 sys.stderr.write(
'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
72 """Main interface to any Mashup
73 This is done to support a common naming framework
for all python Netvision plugins
74 no matter their site target.
76 Supports MNV Mashup Search
and Treeview methods
77 The apikey
is a
not required
for Mashups
87 search_all_languages = False,
89 """apikey (str/unicode):
90 Specify the target site API key. Applications need their own key in some cases
93 When
True, the returned meta data
is being returned has the key
and values massaged to match MythTV
94 When
False, the returned meta data
is being returned matches what target site returned
96 interactive (
True/
False): (This option
is not supported by all target site apis)
97 When
True, uses built-
in console UI
is used to select the correct show.
98 When
False, the first search result
is used.
100 select_first (
True/
False): (This option
is not supported currently implemented
in any grabbers)
101 Automatically selects the first series search result (rather
102 than showing the user a list of more than one series).
103 Is overridden by interactive =
False,
or specifying a custom_ui
106 shows verbose debugging information
108 custom_ui (xx_ui.BaseUI subclass): (This option
is not supported currently implemented
in any grabbers)
109 A callable subclass of interactive
class (overrides interactive option)
111 language (2 character language abbreviation): (This option
is not supported by all target site apis)
112 The language of the returned data. Is also the language search
113 uses. Default
is "en" (English). For full list, run..
115 search_all_languages (
True/
False): (This option
is not supported by all target site apis)
116 By default, a Netvision grabber will only search
in the language specified using
117 the language option. When this
is True, it will search
for the
123 if apikey
is not None:
124 self.
config[
'apikey'] = apikey
128 self.
config[
'debug_enabled'] = debug
136 self.
config[
'custom_ui'] = custom_ui
138 self.
config[
'interactive'] = interactive
140 self.
config[
'select_first'] = select_first
143 self.
config[
'language'] = language
145 self.
config[
'language'] =
'en'
147 self.
config[
'search_all_languages'] = search_all_languages
149 self.
error_messages = {
'MashupsUrlError':
"! Error: The URL (%s) cause the exception error (%s)\n",
'MashupsHttpError':
"! Error: An HTTP communications error with the Mashups was raised (%s)\n",
'MashupsRssError':
"! Error: Invalid RSS meta data\nwas received from the Mashups error (%s). Skipping item.\n",
'MashupsVideoNotFound':
"! Error: Video search with the Mashups did not return any results (%s)\n",
'MashupsConfigFileError':
"! Error: mashups_config.xml file missing\nit should be located in and named as (%s).\n",
'MashupsUrlDownloadError':
"! Error: Downloading a RSS feed or Web page (%s).\n", }
152 self.
channel = {
'channel_title':
'',
'channel_link':
'http://www.mythtv.org/wiki/MythNetvision',
'channel_description':
"Mashups combines media from multiple sources to create a new work",
'channel_numresults': 0,
'channel_returned': 0,
'channel_startindex': 0}
156 self.
config[
'image_extentions'] = [
"png",
"jpg",
"bmp"]
166 '''Check if there is a specific generic tree view icon. If not default to the channel icon.
171 if self.tree_key
not in self.feed_icons:
173 if self.feed
not in self.feed_icons[self.tree_key]:
177 dir_icon = self.feed_icons[self.tree_key][self.feed]
180 self.
tree_dir_icon =
'%%SHAREDIR%%/mythnetvision/icons/%s.png' % (dir_icon, )
186 ''' Read the MNV Mashups grabber "mashups_config.xml" configuration file
190 url =
'%s/nv_python_libs/configs/XML/mashups_config.xml' % (baseProcessingDir, )
191 if not os.path.isfile(url):
194 if self.
config[
'debug_enabled']:
199 except Exception
as errormsg:
206 '''Read the mashups_config.xml and user preference xxxxxMashup.xml file.
207 If the xxxxxMashup.xml file does not exist then copy the default.
214 fileName =
'%s.xml' % self.mashup_title.replace(
'treeview',
'').replace(
'search',
'')
215 userPreferenceFile =
'%s/%s' % (self.
mashups_config.
find(
'userPreferenceFile').text, fileName)
216 if userPreferenceFile[0] ==
'~':
217 self.
mashups_config.
find(
'userPreferenceFile').text =
"%s%s" % (os.path.expanduser(
"~"), userPreferenceFile[1:])
221 defaultConfig =
'%s/nv_python_libs/configs/XML/defaultUserPrefs/%s' % (baseProcessingDir, fileName, )
222 prefDir = self.
mashups_config.
find(
'userPreferenceFile').text.replace(
'/'+fileName,
'')
225 if not os.path.isdir(prefDir):
231 if self.
config[
'debug_enabled']:
236 except Exception
as errormsg:
245 defaultPrefs = etree.parse(defaultConfig)
246 except Exception
as errormsg:
248 urlFilter = etree.XPath(
'//sourceURL[@url=$url and @name=$name]', namespaces=self.
common.namespaces)
249 globalmaxFilter = etree.XPath(
'./../..', namespaces=self.
common.namespaces)
250 for sourceURL
in self.
userPrefs.xpath(
'//sourceURL'):
251 url = sourceURL.attrib[
'url']
252 name = sourceURL.attrib[
'name']
253 defaultSourceURL = urlFilter(defaultPrefs, url=url, name=name)
254 if len(defaultSourceURL):
255 defaultSourceURL[0].attrib[
'enabled'] = sourceURL.attrib[
'enabled']
256 if sourceURL.attrib.get(
'max'):
257 defaultSourceURL[0].attrib[
'max'] = sourceURL.attrib[
'max']
258 directory = globalmaxFilter(sourceURL)[0]
259 if directory.attrib.get(
'globalmax'):
260 defaultDir = directory.attrib.get(
'globalmax')
261 globalmaxFilter(defaultSourceURL[0])[0].attrib[
'globalmax'] = directory.attrib[
'globalmax']
264 tagName = defaultPrefs.getroot().tag
268 for element
in defaultPrefs.iter(tag=etree.Comment):
269 docComment+=etree.tostring(element, encoding=
'UTF-8', pretty_print=
True)[:-1]
272 fd.write((
'<%s>\n' % tagName)+docComment)
273 fd.write(
''.join(etree.tostring(element, encoding=
'UTF-8', pretty_print=
True)
for element
in defaultPrefs.xpath(
'/%s/*' % tagName))+(
'</%s>\n'% tagName))
289 """Common name for a video search. Used to interface with MythTV plugin NetVision
290 Display the results and exit
295 except Exception
as e:
296 sys.stderr.write(
'%s' % e)
299 if self.
config[
'debug_enabled']:
300 print(
"self.userPrefs:")
301 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
305 fullPath =
'%s/nv_python_libs/%s' % (self.
common.baseProcessingDir,
'mnvsearch')
306 sys.path.append(fullPath)
308 exec(
'''import mnvsearch_api''')
309 except Exception
as errmsg:
310 sys.stderr.write(
'! Error: Dynamic import of mnvsearch_api functions\nmessage(%s)\n' % (errmsg))
312 mnvsearch_api.common = self.
common
313 mnvsearch = mnvsearch_api.Videos(
None, debug=self.
config[
'debug_enabled'], language=self.
config[
'language'])
314 mnvsearch.page_limit = self.page_limit
317 self.
common.buildFunctionDict()
323 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
326 self.
channel[
'channel_title'] = self.grabber_title
329 rssTree.append(channelTree)
333 self.
common.buildFunctionDict()
334 mnvXpath = etree.FunctionNamespace(
'http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format')
335 mnvXpath.prefix =
'mnvXpath'
336 for key
in list(self.
common.functionDict.keys()):
337 mnvXpath[key] = self.
common.functionDict[key]
340 self.
common.pagenumber = pagenumber
341 self.
common.page_limit = self.page_limit
343 self.
common.searchterm = title.encode(
"utf-8")
345 'searchterm': urllib.parse.quote_plus(title.encode(
"utf-8")),
346 'pagemax': self.page_limit,
347 'language': self.
config[
'language'],
350 xsltFilename = etree.XPath(
'./@xsltFile', namespaces=self.
common.namespaces)
351 sourceData = etree.XML(
'<xml></xml>')
352 for source
in self.
userPrefs.xpath(
'//search//sourceURL[@enabled="true"]'):
353 if source.attrib.get(
'mnvsearch'):
355 urlName = source.attrib.get(
'name')
357 uniqueName =
'%s;%s' % (urlName, source.attrib.get(
'url'))
359 uniqueName =
'RSS;%s' % (source.attrib.get(
'url'))
360 url = etree.XML(
'<url></url>')
361 etree.SubElement(url,
"name").text = uniqueName
362 if source.attrib.get(
'pageFunction'):
363 searchParms[
'pagenum'] = self.
common.functionDict[source.attrib[
'pageFunction']](
'dummy',
'dummy')
365 searchParms[
'pagenum'] = pagenumber
366 etree.SubElement(url,
"href").text = source.attrib.get(
'url') % searchParms
367 if len(xsltFilename(source)):
368 for xsltName
in xsltFilename(source):
369 etree.SubElement(url,
"xslt").text = xsltName.strip()
370 etree.SubElement(url,
"parserType").text = source.attrib.get(
'type')
371 sourceData.append(url)
373 if self.
config[
'debug_enabled']:
375 sys.stdout.write(etree.tostring(sourceData, encoding=
'UTF-8', pretty_print=
True))
379 if sourceData.find(
'url')
is not None:
382 resultTree = self.
common.getUrlData(sourceData)
383 except Exception
as errormsg:
385 if self.
config[
'debug_enabled']:
387 sys.stdout.write(etree.tostring(resultTree, encoding=
'UTF-8', pretty_print=
True))
391 itemFilter = etree.XPath(
'.//item', namespaces=self.
common.namespaces)
393 for result
in resultTree.findall(
'results'):
394 channelTree.xpath(
'numresults')[0].text = self.
common.numresults
395 channelTree.xpath(
'returned')[0].text = self.
common.returned
396 channelTree.xpath(
'startindex')[0].text = self.
common.startindex
397 for item
in itemFilter(result):
398 channelTree.append(item)
401 for source
in self.
userPrefs.xpath(
'//search//sourceURL[@enabled="true" and @mnvsearch]'):
402 results = mnvsearch.searchForVideos(title, pagenumber, feedtitle=source.xpath(
'./@mnvsearch')[0])
403 if len(list(results[0].keys())):
404 channelTree.xpath(
'returned')[0].text =
'%s' % (int(channelTree.xpath(
'returned')[0].text)+results[1])
405 channelTree.xpath(
'startindex')[0].text =
'%s' % (int(channelTree.xpath(
'startindex')[0].text)+results[2])
406 channelTree.xpath(
'numresults')[0].text =
'%s' % (int(channelTree.xpath(
'numresults')[0].text)+results[3])
408 for key
in sorted(results[0].keys()):
410 channelTree.append(results[0][key])
414 if len(rssTree.xpath(
'//item')):
416 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
417 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))
424 '''Gather the Mashups Internet sources then get the videos meta data in each of them
425 Display the results and exit
430 except Exception
as e:
431 sys.stderr.write(
'%s' % e)
434 if self.
config[
'debug_enabled']:
435 print(
"self.userPrefs:")
436 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
443 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
446 self.
channel[
'channel_title'] = self.grabber_title
449 rssTree.append(channelTree)
454 self.
common.buildFunctionDict()
455 mnvXpath = etree.FunctionNamespace(
'http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format')
456 mnvXpath.prefix =
'mnvXpath'
457 for key
in list(self.
common.functionDict.keys()):
458 mnvXpath[key] = common.functionDict[key]
461 xsltFilename = etree.XPath(
'./@xsltFile', namespaces=self.
common.namespaces)
462 sourceData = etree.XML(
'<xml></xml>')
463 for source
in self.
userPrefs.xpath(
'//directory//sourceURL[@enabled="true"]'):
464 urlName = source.attrib.get(
'name')
466 uniqueName =
'%s;%s' % (urlName, source.attrib.get(
'url'))
468 uniqueName =
'RSS;%s' % (source.attrib.get(
'url'))
469 url = etree.XML(
'<url></url>')
470 etree.SubElement(url,
"name").text = uniqueName
471 etree.SubElement(url,
"href").text = source.attrib.get(
'url')
472 if source.attrib.get(
'parameter')
is not None:
473 etree.SubElement(url,
"parameter").text = source.attrib.get(
'parameter')
474 if len(xsltFilename(source)):
475 for xsltName
in xsltFilename(source):
476 etree.SubElement(url,
"xslt").text = xsltName.strip()
477 etree.SubElement(url,
"parserType").text = source.attrib.get(
'type')
478 sourceData.append(url)
480 if self.
config[
'debug_enabled']:
482 sys.stdout.write(etree.tostring(sourceData, encoding=
'UTF-8', pretty_print=
True))
486 if sourceData.find(
'url')
is not None:
489 resultTree = self.
common.getUrlData(sourceData)
490 except Exception
as errormsg:
492 if self.
config[
'debug_enabled']:
494 sys.stdout.write(etree.tostring(resultTree, encoding=
'UTF-8', pretty_print=
True))
498 categoryElement =
None
499 xsltShowName = etree.XPath(
'//directory//sourceURL[@url=$url]/../@name', namespaces=self.
common.namespaces)
500 channelThumbnail = etree.XPath(
'.//directoryThumbnail', namespaces=self.
common.namespaces)
501 directoryFilter = etree.XPath(
'.//directory', namespaces=self.
common.namespaces)
502 itemFilter = etree.XPath(
'.//item', namespaces=self.
common.namespaces)
503 feedFilter = etree.XPath(
'//directory//sourceURL[@url=$url]')
504 channelThumbnail = etree.XPath(
'.//directoryThumbnail', namespaces=self.
common.namespaces)
505 specialDirectoriesFilter = etree.XPath(
'.//specialDirectories')
506 specialDirectoriesKeyFilter = etree.XPath(
'.//specialDirectories/*[name()=$name]/@key')
507 specialDirectoriesDict = {}
509 for result
in resultTree.findall(
'results'):
510 if len(specialDirectoriesFilter(result)):
511 for element
in specialDirectoriesFilter(result)[0]:
512 if not element.tag
in list(specialDirectoriesDict.keys()):
513 specialDirectoriesElement = etree.XML(
'<directory></directory>')
514 specialDirectoriesElement.attrib[
'name'] = element.attrib[
'dirname']
515 if element.attrib.get(
'count'):
516 count = int(element.attrib[
'count'])
519 if element.attrib.get(
'thumbnail'):
520 specialDirectoriesElement.attrib[
'thumbnail'] = element.attrib[
'thumbnail']
522 specialDirectoriesElement.attrib[
'thumbnail'] = self.
channel_icon
523 specialDirectoriesDict[element.tag] = [specialDirectoriesElement, element.attrib[
'reverse'], count]
524 for result
in resultTree.findall(
'results'):
525 names = result.find(
'name').text.split(
';')
526 names[0] = self.
common.massageText(names[0])
527 if len(xsltShowName(self.
userPrefs, url=names[1])):
528 names[0] = self.
common.massageText(xsltShowName(self.
userPrefs, url=names[1])[0].strip())
529 if names[0] ==
'RSS':
530 names[0] = self.
common.massageText(rssName(result.find(
'result'))[0].text)
533 url = feedFilter(self.
userPrefs, url=names[1])
535 if url[0].attrib.get(
'max'):
537 urlMax = int(url[0].attrib.get(
'max'))
540 elif url[0].getparent().getparent().attrib.get(
'globalmax'):
542 urlMax = int(url[0].getparent().getparent().attrib.get(
'globalmax'))
549 if names[0] != categoryDir:
550 if categoryDir
is not None:
551 channelTree.append(categoryElement)
552 categoryElement = etree.XML(
'<directory></directory>')
553 categoryElement.attrib[
'name'] = names[0]
554 if len(channelThumbnail(result)):
555 categoryElement.attrib[
'thumbnail'] = channelThumbnail(result)[0].text
558 categoryDir = names[0]
561 for key
in list(specialDirectoriesDict.keys()):
564 for sortData
in specialDirectoriesKeyFilter(result, name=key):
565 sortDict[sortData] = count
568 if specialDirectoriesDict[key][1] ==
'true':
569 sortedKeys = sorted(list(sortDict.keys()), reverse=
True)
571 sortedKeys = sorted(list(sortDict.keys()), reverse=
False)
572 if specialDirectoriesDict[key][2] == 0:
573 number = len(sortDict)
575 number = specialDirectoriesDict[key][2]
576 for count
in range(number):
577 if count == len(sortDict):
579 specialDirectoriesDict[key][0].append(deepcopy(itemFilter(result)[sortDict[sortedKeys[count]]]))
581 if len(directoryFilter(result)):
582 for directory
in directoryFilter(result):
583 if not len(itemFilter(directory)):
585 tmpDirElement = etree.XML(
'<directory></directory>')
586 tmpDirElement.attrib[
'name'] = directory.attrib[
'name']
587 if directory.attrib.get(
'thumbnail'):
588 tmpDirElement.attrib[
'thumbnail'] = directory.attrib[
'thumbnail']
592 for item
in itemFilter(directory):
593 tmpDirElement.append(item)
598 categoryElement.append(tmpDirElement)
603 for item
in itemFilter(result):
604 categoryElement.append(item)
611 if categoryElement
is not None:
612 if len(itemFilter(categoryElement)):
613 channelTree.append(categoryElement)
615 for key
in list(specialDirectoriesDict.keys()):
616 if len(itemFilter(specialDirectoriesDict[key][0])):
617 channelTree.append(specialDirectoriesDict[key][0])
620 if len(rssTree.xpath(
'//item')):
622 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
623 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))
def __init__(self, outstream, encoding=None)
def __getattr__(self, attr)
def getUserPreferences(self)
def displayTreeView(self)
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
def getMashupsConfig(self)
def setTreeViewIcon(self, dir_icon=None)
Start - Utility functions.
def searchForVideos(self, title, pagenumber)
End of Utility functions.
static pid_list_t::iterator find(const PIDInfoMap &map, pid_list_t &list, pid_list_t::iterator begin, pid_list_t::iterator end, bool find_open)