14 __title__ =
"mashups_api - Simple-to-use Python interface to Mashups of RSS feeds and HTML video data"
15 __author__=
"R.D. Vaughan"
17 This python script is intended to perform a variety of utility functions to search and access text
18 meta 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.
31 import os, struct, sys, time, datetime, shutil, urllib.request, urllib.parse, urllib.error
32 from socket
import gethostname, gethostbyname
33 from copy
import deepcopy
36 from .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)
58 if isinstance(sys.stdout, io.TextIOWrapper):
64 from io
import StringIO
65 from lxml
import etree
66 except Exception
as e:
67 sys.stderr.write(
'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
75 for digit
in etree.LIBXML_VERSION:
76 version+=str(digit)+
'.'
77 version = version[:-1]
80 ! Error - The installed version of the "lxml" python library "libxml" version is too old.
81 At least "libxml" version 2.7.2 must be installed. Your version is (%s).
87 """Main interface to any Mashup
88 This is done to support a common naming framework for all python Netvision plugins
89 no matter their site target.
91 Supports MNV Mashup Search and Treeview methods
92 The apikey is a not required for Mashups
102 search_all_languages = False,
104 """apikey (str/unicode):
105 Specify the target site API key. Applications need their own key in some cases
108 When True, the returned meta data is being returned has the key and values massaged to match MythTV
109 When False, the returned meta data is being returned matches what target site returned
111 interactive (True/False): (This option is not supported by all target site apis)
112 When True, uses built-in console UI is used to select the correct show.
113 When False, the first search result is used.
115 select_first (True/False): (This option is not supported currently implemented in any grabbers)
116 Automatically selects the first series search result (rather
117 than showing the user a list of more than one series).
118 Is overridden by interactive = False, or specifying a custom_ui
121 shows verbose debugging information
123 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
124 A callable subclass of interactive class (overrides interactive option)
126 language (2 character language abbreviation): (This option is not supported by all target site apis)
127 The language of the returned data. Is also the language search
128 uses. Default is "en" (English). For full list, run..
130 search_all_languages (True/False): (This option is not supported by all target site apis)
131 By default, a Netvision grabber will only search in the language specified using
132 the language option. When this is True, it will search for the
138 if apikey
is not None:
139 self.
config[
'apikey'] = apikey
143 self.
config[
'debug_enabled'] = debug
151 self.
config[
'custom_ui'] = custom_ui
153 self.
config[
'interactive'] = interactive
155 self.
config[
'select_first'] = select_first
162 self.
config[
'search_all_languages'] = search_all_languages
164 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", }
167 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}
169 self.
channel_icon =
'%SHAREDIR%/mythnetvision/icons/mashups.png'
171 self.
config[
'image_extentions'] = [
"png",
"jpg",
"bmp"]
181 '''Check if there is a specific generic tree view icon. If not default to the channel icon.
182 return self.tree_dir_icon
186 if self.tree_key
not in self.feed_icons:
188 if self.feed
not in self.feed_icons[self.tree_key]:
192 dir_icon = self.feed_icons[self.tree_key][self.feed]
195 self.
tree_dir_icon =
'%%SHAREDIR%%/mythnetvision/icons/%s.png' % (dir_icon, )
201 ''' Read the MNV Mashups grabber "mashups_config.xml" configuration file
205 url =
'%s/nv_python_libs/configs/XML/mashups_config.xml' % (baseProcessingDir, )
206 if not os.path.isfile(url):
209 if self.
config[
'debug_enabled']:
214 except Exception
as errormsg:
221 '''Read the mashups_config.xml and user preference xxxxxMashup.xml file.
222 If the xxxxxMashup.xml file does not exist then copy the default.
229 fileName =
'%s.xml' % self.mashup_title.replace(
'treeview',
'').replace(
'search',
'')
230 userPreferenceFile =
'%s/%s' % (self.
mashups_config.
find(
'userPreferenceFile').text, fileName)
231 if userPreferenceFile[0] ==
'~':
232 self.
mashups_config.
find(
'userPreferenceFile').text =
"%s%s" % (os.path.expanduser(
"~"), userPreferenceFile[1:])
236 defaultConfig =
'%s/nv_python_libs/configs/XML/defaultUserPrefs/%s' % (baseProcessingDir, fileName, )
237 prefDir = self.
mashups_config.
find(
'userPreferenceFile').text.replace(
'/'+fileName,
'')
240 if not os.path.isdir(prefDir):
246 if self.
config[
'debug_enabled']:
251 except Exception
as errormsg:
260 defaultPrefs = etree.parse(defaultConfig)
261 except Exception
as errormsg:
263 urlFilter = etree.XPath(
'//sourceURL[@url=$url and @name=$name]', namespaces=self.
common.namespaces)
264 globalmaxFilter = etree.XPath(
'./../..', namespaces=self.
common.namespaces)
265 for sourceURL
in self.
userPrefs.xpath(
'//sourceURL'):
266 url = sourceURL.attrib[
'url']
267 name = sourceURL.attrib[
'name']
268 defaultSourceURL = urlFilter(defaultPrefs, url=url, name=name)
269 if len(defaultSourceURL):
270 defaultSourceURL[0].attrib[
'enabled'] = sourceURL.attrib[
'enabled']
271 if sourceURL.attrib.get(
'max'):
272 defaultSourceURL[0].attrib[
'max'] = sourceURL.attrib[
'max']
273 directory = globalmaxFilter(sourceURL)[0]
274 if directory.attrib.get(
'globalmax'):
275 defaultDir = directory.attrib.get(
'globalmax')
276 globalmaxFilter(defaultSourceURL[0])[0].attrib[
'globalmax'] = directory.attrib[
'globalmax']
279 tagName = defaultPrefs.getroot().tag
283 for element
in defaultPrefs.iter(tag=etree.Comment):
284 docComment+=etree.tostring(element, encoding=
'UTF-8', pretty_print=
True)[:-1]
287 fd.write((
'<%s>\n' % tagName)+docComment)
288 fd.write(
''.join(etree.tostring(element, encoding=
'UTF-8', pretty_print=
True)
for element
in defaultPrefs.xpath(
'/%s/*' % tagName))+(
'</%s>\n'% tagName))
304 """Common name for a video search. Used to interface with MythTV plugin NetVision
305 Display the results and exit
310 except Exception
as e:
311 sys.stderr.write(
'%s' % e)
314 if self.
config[
'debug_enabled']:
315 print(
"self.userPrefs:")
316 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
320 fullPath =
'%s/nv_python_libs/%s' % (self.
common.baseProcessingDir,
'mnvsearch')
321 sys.path.append(fullPath)
323 exec(
'''import mnvsearch_api''')
324 except Exception
as errmsg:
325 sys.stderr.write(
'! Error: Dynamic import of mnvsearch_api functions\nmessage(%s)\n' % (errmsg))
327 mnvsearch_api.common = self.
common
328 mnvsearch = mnvsearch_api.Videos(
None, debug=self.
config[
'debug_enabled'], language=self.
config[
'language'])
329 mnvsearch.page_limit = self.page_limit
332 self.
common.buildFunctionDict()
338 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
341 self.
channel[
'channel_title'] = self.grabber_title
344 rssTree.append(channelTree)
348 self.
common.buildFunctionDict()
349 mnvXpath = etree.FunctionNamespace(
'http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format')
350 mnvXpath.prefix =
'mnvXpath'
351 for key
in list(self.
common.functionDict.keys()):
352 mnvXpath[key] = self.
common.functionDict[key]
355 self.
common.pagenumber = pagenumber
356 self.
common.page_limit = self.page_limit
358 self.
common.searchterm = title.encode(
"utf-8")
360 'searchterm': urllib.parse.quote_plus(title.encode(
"utf-8")),
361 'pagemax': self.page_limit,
362 'language': self.
config[
'language'],
365 xsltFilename = etree.XPath(
'./@xsltFile', namespaces=self.
common.namespaces)
366 sourceData = etree.XML(
'<xml></xml>')
367 for source
in self.
userPrefs.xpath(
'//search//sourceURL[@enabled="true"]'):
368 if source.attrib.get(
'mnvsearch'):
370 urlName = source.attrib.get(
'name')
372 uniqueName =
'%s;%s' % (urlName, source.attrib.get(
'url'))
374 uniqueName =
'RSS;%s' % (source.attrib.get(
'url'))
375 url = etree.XML(
'<url></url>')
376 etree.SubElement(url,
"name").text = uniqueName
377 if source.attrib.get(
'pageFunction'):
378 searchParms[
'pagenum'] = self.
common.functionDict[source.attrib[
'pageFunction']](
'dummy',
'dummy')
380 searchParms[
'pagenum'] = pagenumber
381 etree.SubElement(url,
"href").text = source.attrib.get(
'url') % searchParms
382 if len(xsltFilename(source)):
383 for xsltName
in xsltFilename(source):
384 etree.SubElement(url,
"xslt").text = xsltName.strip()
385 etree.SubElement(url,
"parserType").text = source.attrib.get(
'type')
386 sourceData.append(url)
388 if self.
config[
'debug_enabled']:
390 sys.stdout.write(etree.tostring(sourceData, encoding=
'UTF-8', pretty_print=
True))
394 if sourceData.find(
'url')
is not None:
397 resultTree = self.
common.getUrlData(sourceData)
398 except Exception
as errormsg:
400 if self.
config[
'debug_enabled']:
402 sys.stdout.write(etree.tostring(resultTree, encoding=
'UTF-8', pretty_print=
True))
406 itemFilter = etree.XPath(
'.//item', namespaces=self.
common.namespaces)
408 for result
in resultTree.findall(
'results'):
409 channelTree.xpath(
'numresults')[0].text = self.
common.numresults
410 channelTree.xpath(
'returned')[0].text = self.
common.returned
411 channelTree.xpath(
'startindex')[0].text = self.
common.startindex
412 for item
in itemFilter(result):
413 channelTree.append(item)
416 for source
in self.
userPrefs.xpath(
'//search//sourceURL[@enabled="true" and @mnvsearch]'):
417 results = mnvsearch.searchForVideos(title, pagenumber, feedtitle=source.xpath(
'./@mnvsearch')[0])
418 if len(list(results[0].keys())):
419 channelTree.xpath(
'returned')[0].text =
'%s' % (int(channelTree.xpath(
'returned')[0].text)+results[1])
420 channelTree.xpath(
'startindex')[0].text =
'%s' % (int(channelTree.xpath(
'startindex')[0].text)+results[2])
421 channelTree.xpath(
'numresults')[0].text =
'%s' % (int(channelTree.xpath(
'numresults')[0].text)+results[3])
423 for key
in sorted(results[0].keys()):
425 channelTree.append(results[0][key])
429 if len(rssTree.xpath(
'//item')):
431 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
432 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))
439 '''Gather the Mashups Internet sources then get the videos meta data in each of them
440 Display the results and exit
445 except Exception
as e:
446 sys.stderr.write(
'%s' % e)
449 if self.
config[
'debug_enabled']:
450 print(
"self.userPrefs:")
451 sys.stdout.write(etree.tostring(self.
userPrefs, encoding=
'UTF-8', pretty_print=
True))
458 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
461 self.
channel[
'channel_title'] = self.grabber_title
464 rssTree.append(channelTree)
469 self.
common.buildFunctionDict()
470 mnvXpath = etree.FunctionNamespace(
'http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format')
471 mnvXpath.prefix =
'mnvXpath'
472 for key
in list(self.
common.functionDict.keys()):
473 mnvXpath[key] = common.functionDict[key]
476 xsltFilename = etree.XPath(
'./@xsltFile', namespaces=self.
common.namespaces)
477 sourceData = etree.XML(
'<xml></xml>')
478 for source
in self.
userPrefs.xpath(
'//directory//sourceURL[@enabled="true"]'):
479 urlName = source.attrib.get(
'name')
481 uniqueName =
'%s;%s' % (urlName, source.attrib.get(
'url'))
483 uniqueName =
'RSS;%s' % (source.attrib.get(
'url'))
484 url = etree.XML(
'<url></url>')
485 etree.SubElement(url,
"name").text = uniqueName
486 etree.SubElement(url,
"href").text = source.attrib.get(
'url')
487 if source.attrib.get(
'parameter')
is not None:
488 etree.SubElement(url,
"parameter").text = source.attrib.get(
'parameter')
489 if len(xsltFilename(source)):
490 for xsltName
in xsltFilename(source):
491 etree.SubElement(url,
"xslt").text = xsltName.strip()
492 etree.SubElement(url,
"parserType").text = source.attrib.get(
'type')
493 sourceData.append(url)
495 if self.
config[
'debug_enabled']:
497 sys.stdout.write(etree.tostring(sourceData, encoding=
'UTF-8', pretty_print=
True))
501 if sourceData.find(
'url')
is not None:
504 resultTree = self.
common.getUrlData(sourceData)
505 except Exception
as errormsg:
507 if self.
config[
'debug_enabled']:
509 sys.stdout.write(etree.tostring(resultTree, encoding=
'UTF-8', pretty_print=
True))
513 categoryElement =
None
514 xsltShowName = etree.XPath(
'//directory//sourceURL[@url=$url]/../@name', namespaces=self.
common.namespaces)
515 channelThumbnail = etree.XPath(
'.//directoryThumbnail', namespaces=self.
common.namespaces)
516 directoryFilter = etree.XPath(
'.//directory', namespaces=self.
common.namespaces)
517 itemFilter = etree.XPath(
'.//item', namespaces=self.
common.namespaces)
518 feedFilter = etree.XPath(
'//directory//sourceURL[@url=$url]')
519 channelThumbnail = etree.XPath(
'.//directoryThumbnail', namespaces=self.
common.namespaces)
520 specialDirectoriesFilter = etree.XPath(
'.//specialDirectories')
521 specialDirectoriesKeyFilter = etree.XPath(
'.//specialDirectories/*[name()=$name]/@key')
522 specialDirectoriesDict = {}
524 for result
in resultTree.findall(
'results'):
525 if len(specialDirectoriesFilter(result)):
526 for element
in specialDirectoriesFilter(result)[0]:
527 if not element.tag
in list(specialDirectoriesDict.keys()):
528 specialDirectoriesElement = etree.XML(
'<directory></directory>')
529 specialDirectoriesElement.attrib[
'name'] = element.attrib[
'dirname']
530 if element.attrib.get(
'count'):
531 count = int(element.attrib[
'count'])
534 if element.attrib.get(
'thumbnail'):
535 specialDirectoriesElement.attrib[
'thumbnail'] = element.attrib[
'thumbnail']
537 specialDirectoriesElement.attrib[
'thumbnail'] = self.
channel_icon
538 specialDirectoriesDict[element.tag] = [specialDirectoriesElement, element.attrib[
'reverse'], count]
539 for result
in resultTree.findall(
'results'):
540 names = result.find(
'name').text.split(
';')
541 names[0] = self.
common.massageText(names[0])
542 if len(xsltShowName(self.
userPrefs, url=names[1])):
543 names[0] = self.
common.massageText(xsltShowName(self.
userPrefs, url=names[1])[0].strip())
544 if names[0] ==
'RSS':
545 names[0] = self.
common.massageText(rssName(result.find(
'result'))[0].text)
548 url = feedFilter(self.
userPrefs, url=names[1])
550 if url[0].attrib.get(
'max'):
552 urlMax = int(url[0].attrib.get(
'max'))
555 elif url[0].getparent().getparent().attrib.get(
'globalmax'):
557 urlMax = int(url[0].getparent().getparent().attrib.get(
'globalmax'))
564 if names[0] != categoryDir:
565 if categoryDir
is not None:
566 channelTree.append(categoryElement)
567 categoryElement = etree.XML(
'<directory></directory>')
568 categoryElement.attrib[
'name'] = names[0]
569 if len(channelThumbnail(result)):
570 categoryElement.attrib[
'thumbnail'] = channelThumbnail(result)[0].text
573 categoryDir = names[0]
576 for key
in list(specialDirectoriesDict.keys()):
579 for sortData
in specialDirectoriesKeyFilter(result, name=key):
580 sortDict[sortData] = count
583 if specialDirectoriesDict[key][1] ==
'true':
584 sortedKeys = sorted(list(sortDict.keys()), reverse=
True)
586 sortedKeys = sorted(list(sortDict.keys()), reverse=
False)
587 if specialDirectoriesDict[key][2] == 0:
588 number = len(sortDict)
590 number = specialDirectoriesDict[key][2]
591 for count
in range(number):
592 if count == len(sortDict):
594 specialDirectoriesDict[key][0].append(deepcopy(itemFilter(result)[sortDict[sortedKeys[count]]]))
596 if len(directoryFilter(result)):
597 for directory
in directoryFilter(result):
598 if not len(itemFilter(directory)):
600 tmpDirElement = etree.XML(
'<directory></directory>')
601 tmpDirElement.attrib[
'name'] = directory.attrib[
'name']
602 if directory.attrib.get(
'thumbnail'):
603 tmpDirElement.attrib[
'thumbnail'] = directory.attrib[
'thumbnail']
607 for item
in itemFilter(directory):
608 tmpDirElement.append(item)
613 categoryElement.append(tmpDirElement)
618 for item
in itemFilter(result):
619 categoryElement.append(item)
626 if categoryElement
is not None:
627 if len(itemFilter(categoryElement)):
628 channelTree.append(categoryElement)
630 for key
in list(specialDirectoriesDict.keys()):
631 if len(itemFilter(specialDirectoriesDict[key][0])):
632 channelTree.append(specialDirectoriesDict[key][0])
635 if len(rssTree.xpath(
'//item')):
637 sys.stdout.write(
'<?xml version="1.0" encoding="UTF-8"?>\n')
638 sys.stdout.write(etree.tostring(rssTree, encoding=
'UTF-8', pretty_print=
True))