MythTV  master
pbs_api.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3 # ----------------------
4 # Name: pbs_api - Simple-to-use Python interface to the PBS RSS feeds
5 # (http://video.pbs.org/)
6 # Python Script
7 # Author: R.D. Vaughan
8 # Purpose: This python script is intended to perform a variety of utility functions to
9 # search and access text metadata, video and image URLs from PBS Web site.
10 #
11 # License:Creative Commons GNU GPL v2
12 # (http://creativecommons.org/licenses/GPL/2.0/)
13 #-------------------------------------
14 __title__ ="pbs_api - Simple-to-use Python interface to the PBS videos (http://video.pbs.org/)"
15 __author__="R.D. Vaughan"
16 __purpose__='''
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 the PBS Web site. These routines process videos
19 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
20 "~/.mythtv/MythNetvision/userGrabberPrefs/pbs.xml"
21 '''
22 
23 __version__="v0.1.1"
24 # 0.1.0 Initial development
25 # 0.1.1 Debug code was should have been commented out. This has been corrected.
26 
27 import os, struct, sys, re, time, datetime, shutil, urllib
28 from string import capitalize
29 import logging
30 from threading import Thread
31 from copy import deepcopy
32 from operator import itemgetter, attrgetter
33 
34 from pbs_exceptions import (PBSUrlError, PBSHttpError, PBSRssError, PBSVideoNotFound, PBSConfigFileError, PBSUrlDownloadError)
35 
36 class OutStreamEncoder(object):
37  """Wraps a stream with an encoder"""
38  def __init__(self, outstream, encoding=None):
39  self.out = outstream
40  if not encoding:
41  self.encoding = sys.getfilesystemencoding()
42  else:
43  self.encoding = encoding
44 
45  def write(self, obj):
46  """Wraps the output stream, encoding Unicode strings with the specified encoding"""
47  if isinstance(obj, unicode):
48  try:
49  self.out.write(obj.encode(self.encoding))
50  except IOError:
51  pass
52  else:
53  try:
54  self.out.write(obj)
55  except IOError:
56  pass
57 
58  def __getattr__(self, attr):
59  """Delegate everything but write to the stream"""
60  return getattr(self.out, attr)
61 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
62 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
63 
64 
65 try:
66  from StringIO import StringIO
67  from lxml import etree
68 except Exception, e:
69  sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
70  sys.exit(1)
71 
72 # Check that the lxml library is current enough
73 # From the lxml documents it states: (http://codespeak.net/lxml/installation.html)
74 # "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later"
75 # Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package
76 version = ''
77 for digit in etree.LIBXML_VERSION:
78  version+=str(digit)+'.'
79 version = version[:-1]
80 if version < '2.7.2':
81  sys.stderr.write(u'''
82 ! Error - The installed version of the "lxml" python library "libxml" version is too old.
83  At least "libxml" version 2.7.2 must be installed. Your version is (%s).
84 ''' % version)
85  sys.exit(1)
86 
87 # Used for debugging
88 #import nv_python_libs.mashups.mashups_api as target
89 try:
90  '''Import the python mashups support classes
91  '''
92  import nv_python_libs.mashups.mashups_api as mashups_api
93 except Exception, e:
94  sys.stderr.write('''
95 The subdirectory "nv_python_libs/mashups" containing the modules mashups_api and
96 mashups_exceptions.py (v0.1.0 or greater),
97 They should have been included with the distribution of pbs.py.
98 Error(%s)
99 ''' % e)
100  sys.exit(1)
101 if mashups_api.__version__ < '0.1.0':
102  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__)
103  sys.exit(1)
104 
105 
106 class Videos(object):
107  """Main interface to http://video.pbs.org/
108  This is done to support a common naming framework for all python Netvision plugins no matter their
109  site target.
110 
111  Supports search methods
112  The apikey is a not required to access http://video.pbs.org/
113  """
114  def __init__(self,
115  apikey,
116  mythtv = True,
117  interactive = False,
118  select_first = False,
119  debug = False,
120  custom_ui = None,
121  language = None,
122  search_all_languages = False,
123  ):
124  """apikey (str/unicode):
125  Specify the target site API key. Applications need their own key in some cases
126 
127  mythtv (True/False):
128  When True, the returned meta data is being returned has the key and values massaged to match MythTV
129  When False, the returned meta data is being returned matches what target site returned
130 
131  interactive (True/False): (This option is not supported by all target site apis)
132  When True, uses built-in console UI is used to select the correct show.
133  When False, the first search result is used.
134 
135  select_first (True/False): (This option is not supported currently implemented in any grabbers)
136  Automatically selects the first series search result (rather
137  than showing the user a list of more than one series).
138  Is overridden by interactive = False, or specifying a custom_ui
139 
140  debug (True/False):
141  shows verbose debugging information
142 
143  custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
144  A callable subclass of interactive class (overrides interactive option)
145 
146  language (2 character language abbreviation): (This option is not supported by all target site apis)
147  The language of the returned data. Is also the language search
148  uses. Default is "en" (English). For full list, run..
149 
150  search_all_languages (True/False): (This option is not supported by all target site apis)
151  By default, a Netvision grabber will only search in the language specified using
152  the language option. When this is True, it will search for the
153  show in any language
154 
155  """
156  self.config = {}
157 
158  if apikey is not None:
159  self.config['apikey'] = apikey
160  else:
161  pass # PBS does not require an apikey
162 
163  self.config['debug_enabled'] = debug # show debugging messages
164  self.common = common
165  self.common.debug = debug # Set the common function debug level
166 
167  self.log_name = u'PBS_Grabber'
168  self.common.logger = self.common.initLogger(path=sys.stderr, log_name=self.log_name)
169  self.logger = self.common.logger # Setups the logger (self.log.debug() etc)
171  self.config['custom_ui'] = custom_ui
172 
173  self.config['interactive'] = interactive
174 
175  self.config['select_first'] = select_first
176 
177  self.config['search_all_languages'] = search_all_languages
178 
179  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", }
180 
181  # Channel details and search results
182  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}
183 
184  self.channel_icon = u'%SHAREDIR%/mythnetvision/icons/pbs.png'
185 
186  self.config[u'image_extentions'] = ["png", "jpg", "bmp"] # Acceptable image extentions
187 
188  # Initialize Mashups api variables
189  mashups_api.common = self.common
190  self.mashups_api = mashups_api.Videos(u'')
191  self.mashups_api.channel = self.channel
192  if language:
193  self.mashups_api.config['language'] = self.config['language']
194  self.mashups_api.config['debug_enabled'] = self.config['debug_enabled']
195  self.mashups_api.getUserPreferences = self.getUserPreferences
196  # end __init__()
197 
198 
203 
204  def getPBSConfig(self):
205  ''' Read the MNV PBS grabber "pbs_config.xml" configuration file
206  return nothing
207  '''
208  # Read the grabber pbs_config.xml configuration file
209  url = u'file://%s/nv_python_libs/configs/XML/pbs_config.xml' % (baseProcessingDir, )
210  if not os.path.isfile(url[7:]):
211  raise PBSConfigFileError(self.error_messages['PBSConfigFileError'] % (url[7:], ))
212 
213  if self.config['debug_enabled']:
214  print url
215  print
216  try:
217  self.pbs_config = etree.parse(url)
218  except Exception, e:
219  raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
220  return
221  # end getPBSConfig()
222 
223 
225  '''Read the pbs_config.xml and user preference pbs.xml file.
226  If the pbs.xml file does not exist then create it.
227  If the pbs.xml file is too old then update it.
228  return nothing
229  '''
230  # Get pbs_config.xml
231  self.getPBSConfig()
232 
233  # Check if the pbs.xml file exists
234  userPreferenceFile = self.pbs_config.find('userPreferenceFile').text
235  if userPreferenceFile[0] == '~':
236  self.pbs_config.find('userPreferenceFile').text = u"%s%s" % (os.path.expanduser(u"~"), userPreferenceFile[1:])
237  if os.path.isfile(self.pbs_config.find('userPreferenceFile').text):
238  # Read the grabber pbs_config.xml configuration file
239  url = u'file://%s' % (self.pbs_config.find('userPreferenceFile').text, )
240  if self.config['debug_enabled']:
241  print url
242  print
243  try:
244  self.userPrefs = etree.parse(url)
245  except Exception, e:
246  raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
247  # Check if the pbs.xml file is too old
248  nextUpdateSecs = int(self.userPrefs.find('updateDuration').text)*86400 # seconds in a day
249  nextUpdate = time.localtime(os.path.getmtime(self.pbs_config.find('userPreferenceFile').text)+nextUpdateSecs)
250  now = time.localtime()
251  if nextUpdate > now or self.Search:
252  self.mashups_api.userPrefs = self.userPrefs
253  return
254  create = False
255  else:
256  create = True
257 
258  # If required create/update the pbs.xml file
259  self.updatePBS(create)
260  return
261  # end getUserPreferences()
262 
263  def updatePBS(self, create=False):
264  ''' Create or update the pbs.xml user preferences file
265  return nothing
266  '''
267  userPBS = u'''
268 <userPBS>
269 <!--
270  All PBS shows that have represented on the http://video.pbs.org/ web page are included
271  in as directories. A user may enable it disable an individual show so that it will be
272  included in the treeview. By default ALL shows are enabled.
273  NOTE: As the search is based on the treeview data disabling shows will also reduve the
274  number of possible search results .
275  Updates to the "pbs.xml" file is made every X number of days as determined by the value of
276  the "updateDuration" element in this file. The default is every 3 days.
277 -->
278 <!-- Number of days between updates to the config file -->
279 <updateDuration>3</updateDuration>
280 
281 <!--
282  The PBS Search
283  "enabled" If you want to remove a source URL then change the "enabled" attribute to "false".
284  "xsltFile" The XSLT file name that is used to translate data into MNV item format
285  "type" The source type "xml", "html" and "xhtml"
286  "url" The link that is used to retrieve the information from the Internet
287  "pageFunction" Identifies a XPath extension function that returns the start page/index for the
288  specific source.
289  "mnvsearch" (optional) Identifies that search items are to include items from the MNV table using the
290  mnvsearch_api.py functions. This attributes value must match the "feedtitle" value
291  as it is in the "internetcontentarticles" table. When present the "xsltFile",
292  "url" and "pageFunction" attributes are left empty as they will be ignored.
293 -->
294 <search name="PBS Search">
295  <subDirectory name="PBS">
296  <sourceURL enabled="true" name="PBS" xsltFile="" type="xml" url="" pageFunction="" mnvsearch="PBS"/>
297  </subDirectory>
298 </search>
299 
300 <!--
301  The PBS Video RSS feed and HTML URLs.
302  "globalmax" (optional) Is a way to limit the number of items processed per source for all
303  treeview URLs. A value of zero (0) means there are no limitations.
304  "max" (optional) Is a way to limit the number of items processed for an individual sourceURL.
305  This value will override any "globalmax" setting. A value of zero (0) means
306  there are no limitations and would be the same if the attribute was no included at all.
307  "enabled" If you want to remove a source URL then change the "enabled" attribute to "false".
308  "xsltFile" The XSLT file name that is used to translate data into MNV item format
309  "type" The source type "xml", "html" and "xhtml"
310  "url" The link that is used to retrieve the information from the Internet
311  "parameter" (optional) Specifies source specific parameter that are passed to the XSLT stylesheet.
312  Multiple parameters require the use of key/value pairs. Example:
313  parameter="key1:value1;key2:value2" with the ";" as the separator value.
314 -->
315 
316 '''
317 
318  # Get the current show links from the PBS web site
319  showData = self.common.getUrlData(self.pbs_config.find('treeviewUrls'))
320 
321  if self.config['debug_enabled']:
322  print "create(%s)" % create
323  print "showData:"
324  sys.stdout.write(etree.tostring(showData, encoding='UTF-8', pretty_print=True))
325  print
326 
327  # If there is any data then add to new pbs.xml element tree
328  showsDir = showData.xpath('//directory')
329  if len(showsDir):
330  for dirctory in showsDir:
331  userPBS+=etree.tostring(dirctory, encoding='UTF-8', pretty_print=True)
332  userPBS+=u'</userPBS>'
333  userPBS = etree.XML(userPBS)
334 
335  if self.config['debug_enabled']:
336  print "Before any merging userPBS:"
337  sys.stdout.write(etree.tostring(userPBS, encoding='UTF-8', pretty_print=True))
338  print
339 
340  # If there was an existing pbs.xml file then add any relevant user settings to this new pbs.xml
341  if not create:
342  userPBS.find('updateDuration').text = self.userPrefs.find('updateDuration').text
343  for showElement in self.userPrefs.xpath("//sourceURL[@enabled='false']"):
344  showName = showElement.getparent().attrib['name']
345  sourceName = showElement.attrib['name']
346  elements = userPBS.xpath("//sourceURL[@name=$showName]", showName=showName, sourceName=sourceName)
347  if len(elements):
348  elements[0].attrib['enabled'] = u'false'
349 
350  if self.config['debug_enabled']:
351  print "After any merging userPBS:"
352  sys.stdout.write(etree.tostring(userPBS, encoding='UTF-8', pretty_print=True))
353  print
354 
355  # Save the pbs.xml file
356  prefDir = self.pbs_config.find('userPreferenceFile').text.replace(u'/pbs.xml', u'')
357  if not os.path.isdir(prefDir):
358  os.makedirs(prefDir)
359  fd = open(self.pbs_config.find('userPreferenceFile').text, 'w')
360  fd.write(u'<userPBS>\n'+u''.join(etree.tostring(element, encoding='UTF-8', pretty_print=True) for element in userPBS)+u'</userPBS>')
361  fd.close()
362 
363  # Read the refreshed user config file
364  try:
365  self.userPrefs = etree.parse(self.pbs_config.find('userPreferenceFile').text)
366  self.mashups_api.userPrefs = self.userPrefs
367  except Exception, e:
368  raise PBSUrlError(self.error_messages['PBSUrlError'] % (url, errormsg))
369  return
370  # end updatePBS()
371 
372 
377 
378  def searchForVideos(self, title, pagenumber):
379  """Common name for a video search. Used to interface with MythTV plugin NetVision
380  """
381  self.mashups_api.page_limit = self.page_limit
382  self.mashups_api.grabber_title = self.grabber_title
383  self.mashups_api.mashup_title = self.mashup_title
384  self.mashups_api.channel_icon = self.channel_icon
385  self.mashups_api.mashup_title = u'pbs'
386 
387  # Easier for debugging
388 # self.mashups_api.searchForVideos(title, pagenumber)
389 # print
390 # sys.exit()
391 
392  try:
393  self.Search = True
394  self.mashups_api.Search = True
395  self.mashups_api.searchForVideos(title, pagenumber)
396  except Exception, e:
397  sys.stderr.write(u"! Error: During a PBS Video search (%s)\nError(%s)\n" % (title, e))
398  sys.exit(1)
399 
400  sys.exit(0)
401  # end searchForVideos()
402 
403  def displayTreeView(self):
404  '''Gather all videos for each PBS show
405  Display the results and exit
406  '''
407  self.mashups_api.page_limit = self.page_limit
408  self.mashups_api.grabber_title = self.grabber_title
409  self.mashups_api.mashup_title = self.mashup_title
410  self.mashups_api.channel_icon = self.channel_icon
411  self.mashups_api.mashup_title = u'pbs'
412 
413  # Easier for debugging
414 # self.mashups_api.displayTreeView()
415 # print
416 # sys.exit(1)
417 
418  try:
419  self.Search = False
420  self.mashups_api.Search = False
422  except Exception, e:
423  sys.stderr.write(u"! Error: During a PBS Video treeview\nError(%s)\n" % (e))
424  sys.exit(1)
425 
426  sys.exit(0)
427  # end displayTreeView()
428 # end Videos() class
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)
def searchForVideos(self, title, pagenumber)
End of Utility functions.
Definition: pbs_api.py:378
def updatePBS(self, create=False)
Definition: pbs_api.py:263
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
Definition: pbs_api.py:114
def __init__(self, outstream, encoding=None)
Definition: pbs_api.py:38
def getPBSConfig(self)
Start - Utility functions.
Definition: pbs_api.py:204