00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014 __title__ ="pbs_api - Simple-to-use Python interface to the PBS videos (http://video.pbs.org/)"
00015 __author__="R.D. Vaughan"
00016 __purpose__='''
00017 This python script is intended to perform a variety of utility functions to search and access text
00018 meta data, video and image URLs from the PBS Web site. These routines process videos
00019 provided by PBS (http://video.pbs.org/). The specific PBS RSS feeds that are processed are controled through a user XML preference file usually found at
00020 "~/.mythtv/MythNetvision/userGrabberPrefs/pbs.xml"
00021 '''
00022
00023 __version__="v0.1.1"
00024
00025
00026
00027 import os, struct, sys, re, time, datetime, shutil, urllib
00028 from string import capitalize
00029 import logging
00030 from threading import Thread
00031 from copy import deepcopy
00032 from operator import itemgetter, attrgetter
00033
00034 from pbs_exceptions import (PBSUrlError, PBSHttpError, PBSRssError, PBSVideoNotFound, PBSConfigFileError, PBSUrlDownloadError)
00035
00036 class OutStreamEncoder(object):
00037 """Wraps a stream with an encoder"""
00038 def __init__(self, outstream, encoding=None):
00039 self.out = outstream
00040 if not encoding:
00041 self.encoding = sys.getfilesystemencoding()
00042 else:
00043 self.encoding = encoding
00044
00045 def write(self, obj):
00046 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
00047 if isinstance(obj, unicode):
00048 try:
00049 self.out.write(obj.encode(self.encoding))
00050 except IOError:
00051 pass
00052 else:
00053 try:
00054 self.out.write(obj)
00055 except IOError:
00056 pass
00057
00058 def __getattr__(self, attr):
00059 """Delegate everything but write to the stream"""
00060 return getattr(self.out, attr)
00061 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
00062 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
00063
00064
00065 try:
00066 from StringIO import StringIO
00067 from lxml import etree
00068 except Exception, e:
00069 sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
00070 sys.exit(1)
00071
00072
00073
00074
00075
00076 version = ''
00077 for digit in etree.LIBXML_VERSION:
00078 version+=str(digit)+'.'
00079 version = version[:-1]
00080 if version < '2.7.2':
00081 sys.stderr.write(u'''
00082 ! Error - The installed version of the "lxml" python library "libxml" version is too old.
00083 At least "libxml" version 2.7.2 must be installed. Your version is (%s).
00084 ''' % version)
00085 sys.exit(1)
00086
00087
00088
00089 try:
00090 '''Import the python mashups support classes
00091 '''
00092 import nv_python_libs.mashups.mashups_api as mashups_api
00093 except Exception, e:
00094 sys.stderr.write('''
00095 The subdirectory "nv_python_libs/mashups" containing the modules mashups_api and
00096 mashups_exceptions.py (v0.1.0 or greater),
00097 They should have been included with the distribution of pbs.py.
00098 Error(%s)
00099 ''' % e)
00100 sys.exit(1)
00101 if mashups_api.__version__ < '0.1.0':
00102 sys.stderr.write("\n! Error: Your current installed mashups_api.py version is (%s)\nYou must at least have version (0.1.0) or higher.\n" % mashups_api.__version__)
00103 sys.exit(1)
00104
00105
00106 class Videos(object):
00107 """Main interface to http://video.pbs.org/
00108 This is done to support a common naming framework for all python Netvision plugins no matter their
00109 site target.
00110
00111 Supports search methods
00112 The apikey is a not required to access http://video.pbs.org/
00113 """
00114 def __init__(self,
00115 apikey,
00116 mythtv = True,
00117 interactive = False,
00118 select_first = False,
00119 debug = False,
00120 custom_ui = None,
00121 language = None,
00122 search_all_languages = False,
00123 ):
00124 """apikey (str/unicode):
00125 Specify the target site API key. Applications need their own key in some cases
00126
00127 mythtv (True/False):
00128 When True, the returned meta data is being returned has the key and values massaged to match MythTV
00129 When False, the returned meta data is being returned matches what target site returned
00130
00131 interactive (True/False): (This option is not supported by all target site apis)
00132 When True, uses built-in console UI is used to select the correct show.
00133 When False, the first search result is used.
00134
00135 select_first (True/False): (This option is not supported currently implemented in any grabbers)
00136 Automatically selects the first series search result (rather
00137 than showing the user a list of more than one series).
00138 Is overridden by interactive = False, or specifying a custom_ui
00139
00140 debug (True/False):
00141 shows verbose debugging information
00142
00143 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
00144 A callable subclass of interactive class (overrides interactive option)
00145
00146 language (2 character language abbreviation): (This option is not supported by all target site apis)
00147 The language of the returned data. Is also the language search
00148 uses. Default is "en" (English). For full list, run..
00149
00150 search_all_languages (True/False): (This option is not supported by all target site apis)
00151 By default, a Netvision grabber will only search in the language specified using
00152 the language option. When this is True, it will search for the
00153 show in any language
00154
00155 """
00156 self.config = {}
00157
00158 if apikey is not None:
00159 self.config['apikey'] = apikey
00160 else:
00161 pass
00162
00163 self.config['debug_enabled'] = debug
00164 self.common = common
00165 self.common.debug = debug
00166
00167 self.log_name = u'PBS_Grabber'
00168 self.common.logger = self.common.initLogger(path=sys.stderr, log_name=self.log_name)
00169 self.logger = self.common.logger
00170
00171 self.config['custom_ui'] = custom_ui
00172
00173 self.config['interactive'] = interactive
00174
00175 self.config['select_first'] = select_first
00176
00177 self.config['search_all_languages'] = search_all_languages
00178
00179 self.error_messages = {'PBSUrlError': u"! Error: The URL (%s) cause the exception error (%s)\n", 'PBSHttpError': u"! Error: An HTTP communications error with the PBS was raised (%s)\n", 'PBSRssError': u"! Error: Invalid RSS meta data\nwas received from the PBS error (%s). Skipping item.\n", 'PBSVideoNotFound': u"! Error: Video search with the PBS did not return any results (%s)\n", 'PBSConfigFileError': u"! Error: pbs_config.xml file missing\nit should be located in and named as (%s).\n", 'PBSUrlDownloadError': u"! Error: Downloading a RSS feed or Web page (%s).\n", }
00180
00181
00182 self.channel = {'channel_title': u'PBS', 'channel_link': u'http://video.pbs.org/', 'channel_description': u"Discover award-winning programming – right at your fingertips – on PBS Video. Catch the episodes you may have missed and watch your favorite shows whenever you want.", 'channel_numresults': 0, 'channel_returned': 1, u'channel_startindex': 0}
00183
00184 self.channel_icon = u'%SHAREDIR%/mythnetvision/icons/pbs.png'
00185
00186 self.config[u'image_extentions'] = ["png", "jpg", "bmp"]
00187
00188
00189 mashups_api.common = self.common
00190 self.mashups_api = mashups_api.Videos(u'')
00191 self.mashups_api.channel = self.channel
00192 if language:
00193 self.mashups_api.config['language'] = self.config['language']
00194 self.mashups_api.config['debug_enabled'] = self.config['debug_enabled']
00195 self.mashups_api.getUserPreferences = self.getUserPreferences
00196
00197
00198
00199
00200
00201
00202
00203
00204 def getPBSConfig(self):
00205 ''' Read the MNV PBS grabber "pbs_config.xml" configuration file
00206 return nothing
00207 '''
00208
00209 url = u'file://%s/nv_python_libs/configs/XML/pbs_config.xml' % (baseProcessingDir, )
00210 if not os.path.isfile(url[7:]):
00211 raise PBSConfigFileError(self.error_messages['PBSConfigFileError'] % (url[7:], ))
00212
00213 if self.config['debug_enabled']:
00214 print url
00215 print
00216 try:
00217 self.pbs_config = etree.parse(url)
00218 except Exception, e:
00219 raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
00220 return
00221
00222
00223
00224 def getUserPreferences(self):
00225 '''Read the pbs_config.xml and user preference pbs.xml file.
00226 If the pbs.xml file does not exist then create it.
00227 If the pbs.xml file is too old then update it.
00228 return nothing
00229 '''
00230
00231 self.getPBSConfig()
00232
00233
00234 userPreferenceFile = self.pbs_config.find('userPreferenceFile').text
00235 if userPreferenceFile[0] == '~':
00236 self.pbs_config.find('userPreferenceFile').text = u"%s%s" % (os.path.expanduser(u"~"), userPreferenceFile[1:])
00237 if os.path.isfile(self.pbs_config.find('userPreferenceFile').text):
00238
00239 url = u'file://%s' % (self.pbs_config.find('userPreferenceFile').text, )
00240 if self.config['debug_enabled']:
00241 print url
00242 print
00243 try:
00244 self.userPrefs = etree.parse(url)
00245 except Exception, e:
00246 raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
00247
00248 nextUpdateSecs = int(self.userPrefs.find('updateDuration').text)*86400
00249 nextUpdate = time.localtime(os.path.getmtime(self.pbs_config.find('userPreferenceFile').text)+nextUpdateSecs)
00250 now = time.localtime()
00251 if nextUpdate > now or self.Search:
00252 self.mashups_api.userPrefs = self.userPrefs
00253 return
00254 create = False
00255 else:
00256 create = True
00257
00258
00259 self.updatePBS(create)
00260 return
00261
00262
00263 def updatePBS(self, create=False):
00264 ''' Create or update the pbs.xml user preferences file
00265 return nothing
00266 '''
00267 userPBS = u'''
00268 <userPBS>
00269 <!--
00270 All PBS shows that have represented on the http://video.pbs.org/ web page are included
00271 in as directories. A user may enable it disable an individual show so that it will be
00272 included in the treeview. By default ALL shows are enabled.
00273 NOTE: As the search is based on the treeview data disabling shows will also reduve the
00274 number of possible search results .
00275 Updates to the "pbs.xml" file is made every X number of days as determined by the value of
00276 the "updateDuration" element in this file. The default is every 3 days.
00277 -->
00278 <!-- Number of days between updates to the config file -->
00279 <updateDuration>3</updateDuration>
00280
00281 <!--
00282 The PBS Search
00283 "enabled" If you want to remove a source URL then change the "enabled" attribute to "false".
00284 "xsltFile" The XSLT file name that is used to translate data into MNV item format
00285 "type" The source type "xml", "html" and "xhtml"
00286 "url" The link that is used to retrieve the information from the Internet
00287 "pageFunction" Identifies a XPath extension function that returns the start page/index for the
00288 specific source.
00289 "mnvsearch" (optional) Identifies that search items are to include items from the MNV table using the
00290 mnvsearch_api.py functions. This attributes value must match the "feedtitle" value
00291 as it is in the "internetcontentarticles" table. When present the "xsltFile",
00292 "url" and "pageFunction" attributes are left empty as they will be ignored.
00293 -->
00294 <search name="PBS Search">
00295 <subDirectory name="PBS">
00296 <sourceURL enabled="true" name="PBS" xsltFile="" type="xml" url="" pageFunction="" mnvsearch="PBS"/>
00297 </subDirectory>
00298 </search>
00299
00300 <!--
00301 The PBS Video RSS feed and HTML URLs.
00302 "globalmax" (optional) Is a way to limit the number of items processed per source for all
00303 treeview URLs. A value of zero (0) means there are no limitations.
00304 "max" (optional) Is a way to limit the number of items processed for an individual sourceURL.
00305 This value will override any "globalmax" setting. A value of zero (0) means
00306 there are no limitations and would be the same if the attribute was no included at all.
00307 "enabled" If you want to remove a source URL then change the "enabled" attribute to "false".
00308 "xsltFile" The XSLT file name that is used to translate data into MNV item format
00309 "type" The source type "xml", "html" and "xhtml"
00310 "url" The link that is used to retrieve the information from the Internet
00311 "parameter" (optional) Specifies source specific parameter that are passed to the XSLT stylesheet.
00312 Multiple parameters require the use of key/value pairs. Example:
00313 parameter="key1:value1;key2:value2" with the ";" as the separator value.
00314 -->
00315
00316 '''
00317
00318
00319 showData = self.common.getUrlData(self.pbs_config.find('treeviewUrls'))
00320
00321 if self.config['debug_enabled']:
00322 print "create(%s)" % create
00323 print "showData:"
00324 sys.stdout.write(etree.tostring(showData, encoding='UTF-8', pretty_print=True))
00325 print
00326
00327
00328 showsDir = showData.xpath('//directory')
00329 if len(showsDir):
00330 for dirctory in showsDir:
00331 userPBS+=etree.tostring(dirctory, encoding='UTF-8', pretty_print=True)
00332 userPBS+=u'</userPBS>'
00333 userPBS = etree.XML(userPBS)
00334
00335 if self.config['debug_enabled']:
00336 print "Before any merging userPBS:"
00337 sys.stdout.write(etree.tostring(userPBS, encoding='UTF-8', pretty_print=True))
00338 print
00339
00340
00341 if not create:
00342 userPBS.find('updateDuration').text = self.userPrefs.find('updateDuration').text
00343 for showElement in self.userPrefs.xpath("//sourceURL[@enabled='false']"):
00344 showName = showElement.getparent().attrib['name']
00345 sourceName = showElement.attrib['name']
00346 elements = userPBS.xpath("//sourceURL[@name=$showName]", showName=showName, sourceName=sourceName)
00347 if len(elements):
00348 elements[0].attrib['enabled'] = u'false'
00349
00350 if self.config['debug_enabled']:
00351 print "After any merging userPBS:"
00352 sys.stdout.write(etree.tostring(userPBS, encoding='UTF-8', pretty_print=True))
00353 print
00354
00355
00356 prefDir = self.pbs_config.find('userPreferenceFile').text.replace(u'/pbs.xml', u'')
00357 if not os.path.isdir(prefDir):
00358 os.makedirs(prefDir)
00359 fd = open(self.pbs_config.find('userPreferenceFile').text, 'w')
00360 fd.write(u'<userPBS>\n'+u''.join(etree.tostring(element, encoding='UTF-8', pretty_print=True) for element in userPBS)+u'</userPBS>')
00361 fd.close()
00362
00363
00364 try:
00365 self.userPrefs = etree.parse(self.pbs_config.find('userPreferenceFile').text)
00366 self.mashups_api.userPrefs = self.userPrefs
00367 except Exception, e:
00368 raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
00369 return
00370
00371
00372
00373
00374
00375
00376
00377
00378 def searchForVideos(self, title, pagenumber):
00379 """Common name for a video search. Used to interface with MythTV plugin NetVision
00380 """
00381 self.mashups_api.page_limit = self.page_limit
00382 self.mashups_api.grabber_title = self.grabber_title
00383 self.mashups_api.mashup_title = self.mashup_title
00384 self.mashups_api.channel_icon = self.channel_icon
00385 self.mashups_api.mashup_title = u'pbs'
00386
00387
00388
00389
00390
00391
00392 try:
00393 self.Search = True
00394 self.mashups_api.Search = True
00395 self.mashups_api.searchForVideos(title, pagenumber)
00396 except Exception, e:
00397 sys.stderr.write(u"! Error: During a PBS Video search (%s)\nError(%s)\n" % (title, e))
00398 sys.exit(1)
00399
00400 sys.exit(0)
00401
00402
00403 def displayTreeView(self):
00404 '''Gather all videos for each PBS show
00405 Display the results and exit
00406 '''
00407 self.mashups_api.page_limit = self.page_limit
00408 self.mashups_api.grabber_title = self.grabber_title
00409 self.mashups_api.mashup_title = self.mashup_title
00410 self.mashups_api.channel_icon = self.channel_icon
00411 self.mashups_api.mashup_title = u'pbs'
00412
00413
00414
00415
00416
00417
00418 try:
00419 self.Search = False
00420 self.mashups_api.Search = False
00421 self.mashups_api.displayTreeView()
00422 except Exception, e:
00423 sys.stderr.write(u"! Error: During a PBS Video treeview\nError(%s)\n" % (e))
00424 sys.exit(1)
00425
00426 sys.exit(0)
00427
00428