MythTV  master
rev3_api.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3 # ----------------------
4 # Name: rev3_api - Simple-to-use Python interface to the Revision3 RSS feeds (http://revision3.com/)
5 # Python Script
6 # Author: R.D. Vaughan
7 # Purpose: This python script is intended to perform a variety of utility functions to search and access text
8 # metadata, video and image URLs from rev3. These routines are based on the api. Specifications
9 # for this api are published at http://developer.rev3nservices.com/docs
10 #
11 # License:Creative Commons GNU GPL v2
12 # (http://creativecommons.org/licenses/GPL/2.0/)
13 #-------------------------------------
14 __title__ ="rev3_api - Simple-to-use Python interface to the Revision3 RSS feeds (http://revision3.com/)"
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 rev3. These routines process RSS feeds provided by Revision3
19 (http://revision3.com/). The specific Revision3 RSS feeds that are processed are controled through
20 a user XML preference file usually found at "~/.mythtv/MythNetvision/userGrabberPrefs/rev3.xml"
21 '''
22 
23 __version__="v0.1.4"
24 # 0.1.0 Initial development
25 # 0.1.1 Changed the search functionality to be "Videos" only rather than the site search.
26 # Added support for Revision3's Personal RSS feed
27 # Changed the logger to only output to stderr rather than a file
28 # 0.1.2 Fixed an abort when no RSS feed data was returned
29 # 0.1.3 Removed the need for python MythTV bindings and added "%SHAREDIR%" to icon directory path
30 # 0.1.4 Fixed missing shows from the creation of the user default preference due to Web site changes
31 # Fixed two incorrect variable names in debug messages
32 
33 import os, struct, sys, re, time, datetime, urllib, re
34 import logging
35 from socket import gethostname, gethostbyname
36 from threading import Thread
37 from copy import deepcopy
38 
39 from rev3_exceptions import (Rev3UrlError, Rev3HttpError, Rev3RssError, Rev3VideoNotFound, Rev3ConfigFileError, Rev3UrlDownloadError)
40 
41 class OutStreamEncoder(object):
42  """Wraps a stream with an encoder"""
43  def __init__(self, outstream, encoding=None):
44  self.out = outstream
45  if not encoding:
46  self.encoding = sys.getfilesystemencoding()
47  else:
48  self.encoding = encoding
49 
50  def write(self, obj):
51  """Wraps the output stream, encoding Unicode strings with the specified encoding"""
52  if isinstance(obj, unicode):
53  try:
54  self.out.write(obj.encode(self.encoding))
55  except IOError:
56  pass
57  else:
58  try:
59  self.out.write(obj)
60  except IOError:
61  pass
62 
63  def __getattr__(self, attr):
64  """Delegate everything but write to the stream"""
65  return getattr(self.out, attr)
66 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
67 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
68 
69 
70 try:
71  from StringIO import StringIO
72  from lxml import etree
73 except Exception, e:
74  sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
75  sys.exit(1)
76 
77 # Check that the lxml library is current enough
78 # From the lxml documents it states: (http://codespeak.net/lxml/installation.html)
79 # "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later"
80 # Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package
81 version = ''
82 for digit in etree.LIBXML_VERSION:
83  version+=str(digit)+'.'
84 version = version[:-1]
85 if version < '2.7.2':
86  sys.stderr.write(u'''
87 ! Error - The installed version of the "lxml" python library "libxml" version is too old.
88  At least "libxml" version 2.7.2 must be installed. Your version is (%s).
89 ''' % version)
90  sys.exit(1)
91 
92 
93 class Videos(object):
94  """Main interface to http://www.rev3.com/
95  This is done to support a common naming framework for all python Netvision plugins no matter their site
96  target.
97 
98  Supports search methods
99  The apikey is a not required to access http://www.rev3.com/
100  """
101  def __init__(self,
102  apikey,
103  mythtv = True,
104  interactive = False,
105  select_first = False,
106  debug = False,
107  custom_ui = None,
108  language = None,
109  search_all_languages = False,
110  ):
111  """apikey (str/unicode):
112  Specify the target site API key. Applications need their own key in some cases
113 
114  mythtv (True/False):
115  When True, the returned meta data is being returned has the key and values massaged to match MythTV
116  When False, the returned meta data is being returned matches what target site returned
117 
118  interactive (True/False): (This option is not supported by all target site apis)
119  When True, uses built-in console UI is used to select the correct show.
120  When False, the first search result is used.
121 
122  select_first (True/False): (This option is not supported currently implemented in any grabbers)
123  Automatically selects the first series search result (rather
124  than showing the user a list of more than one series).
125  Is overridden by interactive = False, or specifying a custom_ui
126 
127  debug (True/False):
128  shows verbose debugging information
129 
130  custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
131  A callable subclass of interactive class (overrides interactive option)
132 
133  language (2 character language abbreviation): (This option is not supported by all target site apis)
134  The language of the returned data. Is also the language search
135  uses. Default is "en" (English). For full list, run..
136 
137  search_all_languages (True/False): (This option is not supported by all target site apis)
138  By default, a Netvision grabber will only search in the language specified using
139  the language option. When this is True, it will search for the
140  show in any language
141 
142  """
143  self.config = {}
144 
145  if apikey is not None:
146  self.config['apikey'] = apikey
147  else:
148  pass # Rev3 does not require an apikey
149 
150  self.config['debug_enabled'] = debug # show debugging messages
151  self.common = common
152  self.common.debug = debug # Set the common function debug level
153 
154  self.log_name = u'Rev3_Grabber'
155  self.common.logger = self.common.initLogger(path=sys.stderr, log_name=self.log_name)
156  self.logger = self.common.logger # Setups the logger (self.log.debug() etc)
158  self.config['custom_ui'] = custom_ui
159 
160  self.config['interactive'] = interactive
161 
162  self.config['select_first'] = select_first
164  self.config['search_all_languages'] = search_all_languages
165 
166  self.error_messages = {'Rev3UrlError': u"! Error: The URL (%s) cause the exception error (%s)\n", 'Rev3HttpError': u"! Error: An HTTP communications error with Rev3 was raised (%s)\n", 'Rev3RssError': u"! Error: Invalid RSS meta data\nwas received from Rev3 error (%s). Skipping item.\n", 'Rev3VideoNotFound': u"! Error: Video search with Rev3 did not return any results (%s)\n", 'Rev3ConfigFileError': u"! Error: rev3_config.xml file missing\nit should be located in and named as (%s).\n", 'Rev3UrlDownloadError': u"! Error: Downloading a RSS feed or Web page (%s).\n", }
167 
168  # Channel details and search results
169  self.channel = {'channel_title': u'Revision3', 'channel_link': u'http://revision3.com/', 'channel_description': u"Revision3 is the leading television network for the internet generation.", 'channel_numresults': 0, 'channel_returned': 1, u'channel_startindex': 0}
170 
171  # Season and/or Episode detection regex patterns
172  self.s_e_Patterns = [
173  # Season 3, Episode 8
174  re.compile(u'''^.+?Season\\ (?P<seasno>[0-9]+).*.+?Episode\\ (?P<epno>[0-9]+).*$''', re.UNICODE),
175  # "Episode 1" anywhere in text
176  re.compile(u'''^.+?Episode\\ (?P<seasno>[0-9]+).*$''', re.UNICODE),
177  # "Episode 1" at the start of the text
178  re.compile(u'''Episode\\ (?P<seasno>[0-9]+).*$''', re.UNICODE),
179  # "--0027--" when the episode is in the URl link
180  re.compile(u'''^.+?--(?P<seasno>[0-9]+)--.*$''', re.UNICODE),
181  ]
182 
183  self.FullScreen = u'http://revision3.com/show/popupPlayer?video_id=%s&quality=high&offset=0'
184  self.FullScreenParser = self.common.parsers['html'].copy()
185  self.FullScreenVidIDxPath = etree.XPath('//object', namespaces=self.common.namespaces )
186 
187  self.channel_icon = u'%SHAREDIR%/mythnetvision/icons/rev3.png'
188  # end __init__()
189 
190 
195 
196  def getRev3Config(self):
197  ''' Read the MNV Revision3 grabber "rev3_config.xml" configuration file
198  return nothing
199  '''
200  # Read the grabber rev3_config.xml configuration file
201  url = u'file://%s/nv_python_libs/configs/XML/rev3_config.xml' % (baseProcessingDir, )
202  if not os.path.isfile(url[7:]):
203  raise Rev3ConfigFileError(self.error_messages['Rev3ConfigFileError'] % (url[7:], ))
204 
205  if self.config['debug_enabled']:
206  print url
207  print
208  try:
209  self.rev3_config = etree.parse(url)
210  except Exception, e:
211  raise Rev3UrlError(self.error_messages['Rev3UrlError'] % (url, errormsg))
212  return
213  # end getRev3Config()
214 
215 
217  '''Read the rev3_config.xml and user preference rev3.xml file.
218  If the rev3.xml file does not exist then create it.
219  If the rev3.xml file is too old then update it.
220  return nothing
221  '''
222  # Get rev3_config.xml
223  self.getRev3Config()
224 
225  # Check if the rev3.xml file exists
226  userPreferenceFile = self.rev3_config.find('userPreferenceFile').text
227  if userPreferenceFile[0] == '~':
228  self.rev3_config.find('userPreferenceFile').text = u"%s%s" % (os.path.expanduser(u"~"), userPreferenceFile[1:])
229  if os.path.isfile(self.rev3_config.find('userPreferenceFile').text):
230  # Read the grabber rev3_config.xml configuration file
231  url = u'file://%s' % (self.rev3_config.find('userPreferenceFile').text, )
232  if self.config['debug_enabled']:
233  print url
234  print
235  try:
236  self.userPrefs = etree.parse(url)
237  except Exception, e:
238  raise Rev3UrlError(self.error_messages['Rev3UrlError'] % (url, errormsg))
239  # Check if the rev3.xml file is too old
240  nextUpdateSecs = int(self.userPrefs.find('updateDuration').text)*86400 # seconds in a day
241  nextUpdate = time.localtime(os.path.getmtime(self.rev3_config.find('userPreferenceFile').text)+nextUpdateSecs)
242  now = time.localtime()
243  if nextUpdate > now:
244  return
245  create = False
246  else:
247  create = True
248 
249  # If required create/update the rev3.xml file
250  self.updateRev3(create)
251  return
252  # end getUserPreferences()
253 
254  def updateRev3(self, create=False):
255  ''' Create or update the rev3.xml user preferences file
256  return nothing
257  '''
258  userRev3 = etree.XML(u'''
259 <userRev3>
260 <!--
261  The shows are split into three top level directories which represent how Rev3 categories
262  their videos. Each top level directory has one or more shows. Each show has one or more
263  MP4 formats that can be downloaded. The formats represent various video quality levels.
264  Initially only three shows are enabled. You must change a show's "mp4Format" element's
265  "enabled" attribute to 'true'. When you change the attribute to 'true' that show's RSS feed
266  will be added to the Rev3 tree view. You could activate more than one MP4 format but that
267  would result in duplicates. With the exception of "Tekzilla" which is a show that has
268  both a weekly and daily video RSS feed within the same show element.
269  When the Rev3 Tree view is created it will have links to the video's web page but also a
270  link to the MP4 file that you can download through the MythNetvision interface.
271  If a show moves from one top level directory to another your show sections will be
272  preserved as long as that format is available in the new top level directory.
273  Updates to the "rev3.xml" file is made every X number of days as determined by the value of
274  the "updateDuration" element in this file. The default is every 3 days.
275 -->
276 <!-- Number of days between updates to the config file -->
277 <updateDuration>3</updateDuration>
278 
279 <!--
280  Personal RSS feed.
281  "globalmax" (optional) Is a way to limit the number of items processed per RSS feed for all
282  treeview URLs. A value of zero (0) means there are no limitions.
283  "max" (optional) Is a way to limit the number of items processed for an individual RSS feed.
284  This value will override any "globalmax" setting. A value of zero (0) means
285  there are no limitions and would be the same if the attribute was no included at all.
286  "enabled" If you want to remove a RSS feed then change the "enabled" attribute to "false".
287 
288  See details: http://revision3.com/blog/2010/03/11/pick-your-favorite-shows-create-a-personalized-feed/
289  Once you sign up and create your personal RSS feed replace the url in the example below with the
290  Revision3 "Your RSS Feed Address" URL and change the "enabled" element attribute to "true".
291 -->
292 <treeviewURLS globalmax="0">
293  <url enabled="false">http://revision3.com/feed/user/EXAMPLE</url>
294 </treeviewURLS>
295 </userRev3>
296 ''')
297 
298  # Get the current show links from the Rev3 web site
299  linksTree = self.common.getUrlData(self.rev3_config.find('treeviewUrls'))
300 
301  if self.config['debug_enabled']:
302  print "create(%s)" % create
303  print "linksTree:"
304  sys.stdout.write(etree.tostring(linksTree, encoding='UTF-8', pretty_print=True))
305  print
306 
307  # Extract the show name and Web page links
308  showData = etree.XML(u'<xml></xml>')
309  complexFilter = u"//div[@class='subscribe_rss']//div//p[.='MP4']/..//a"
310  for result in linksTree.xpath('//results'):
311  tmpDirectory = etree.XML(u'<directory></directory>')
312  dirName = result.find('name').text
313  tmpDirectory.attrib['name'] = dirName
314 
315  if self.config['debug_enabled']:
316  print "Results: #Items(%s) for (%s)" % (len(result.xpath('.//a')), dirName)
317  print
318 
319  for anchor in result.xpath('.//a'):
320  showURL = None
321  if dirName == 'Shows':
322  showURL = anchor.attrib.get('href')
323  showFilter = complexFilter
324  tmpName = anchor.text
325  elif dirName == 'Revision3 Beta':
326  tmpName = etree.tostring(anchor, method="text", encoding=unicode)
327  showURL = u'http://revision3beta.com%sfeed/' % anchor.attrib.get('href')
328  showFilter = None
329  elif dirName == 'Archived Shows':
330  showURL = u'http://revision3.com%s' % anchor.attrib.get('href')
331  showFilter = complexFilter
332  tmpName = anchor.text
333  if tmpName == u'Revision3 Beta':
334  continue
335  if showURL != None:
336  url = etree.SubElement(tmpDirectory, "url")
337  etree.SubElement(url, "name").text = tmpName
338  etree.SubElement(url, "href").text = showURL
339  etree.SubElement(url, "filter").text = showFilter
340  etree.SubElement(url, "parserType").text = u'html'
341  if tmpDirectory.find('url') != None:
342  showData.append(tmpDirectory)
343 
344  if self.config['debug_enabled']:
345  print "showData:"
346  sys.stdout.write(etree.tostring(showData, encoding='UTF-8', pretty_print=True))
347  print
348 
349  # Assemble the feeds and formats
350  for directory in showData.findall('directory'):
351  if create:
352  firstEnabled = True
353  else:
354  firstEnabled = False
355  tmpDirectory = etree.XML(u'<directory></directory>')
356  tmpDirectory.attrib['name'] = directory.attrib['name']
357  if directory.attrib['name'] == u'Revision3 Beta':
358  for show in directory.findall('url'):
359  tmpShow = etree.XML(u'<show></show>')
360  tmpShow.attrib['name'] = show.find('name').text
361  mp4Format = etree.SubElement(tmpShow, u"mp4Format")
362  if firstEnabled:
363  mp4Format.attrib['enabled'] = u'true'
364  firstEnabled = False
365  else:
366  mp4Format.attrib['enabled'] = u'false'
367  mp4Format.attrib['name'] = u'Web Only'
368  mp4Format.attrib['rss'] = show.find('href').text
369  tmpDirectory.append(tmpShow)
370  else:
371  showResults = self.common.getUrlData(directory)
372  for show in showResults.xpath('//results'):
373  tmpShow = etree.XML(u'<show></show>')
374  tmpShow.attrib['name'] = show.find('name').text
375 
376  if self.config['debug_enabled']:
377  print "Results: #Items(%s) for (%s)" % (len(show.xpath('.//a')), tmpShow.attrib['name'])
378  print
379 
380  for format in show.xpath('.//a'):
381  link = u'http://revision3.com%s' % format.attrib['href']
382  # If this is a "tekzilla" link without extra parameters that skip show
383  # This forces the Tekzilla weekly show to be separate from the daily show
384  if link.find('/tekzilla/') != -1 and link.find('?subshow=false') == -1:
385  continue
386  mp4Format = etree.SubElement(tmpShow, "mp4Format")
387  if firstEnabled:
388  mp4Format.attrib['enabled'] = u'true'
389  firstEnabled = False
390  else:
391  mp4Format.attrib['enabled'] = u'false'
392  mp4Format.attrib['name'] = format.text
393  mp4Format.attrib['rss'] = link
394  if tmpShow.find('mp4Format') != None:
395  tmpDirectory.append(tmpShow)
396 
397  # If there is any data then add to new rev3.xml element tree
398  if tmpDirectory.find('show') != None:
399  userRev3.append(tmpDirectory)
400 
401  if self.config['debug_enabled']:
402  print "Before any merging userRev3:"
403  sys.stdout.write(etree.tostring(userRev3, encoding='UTF-8', pretty_print=True))
404  print
405 
406  # If there was an existing rev3.xml file then add any relevant user settings to this new rev3.xml
407  if not create:
408  userRev3.find('updateDuration').text = self.userPrefs.find('updateDuration').text
409  for mp4Format in self.userPrefs.xpath("//mp4Format[@enabled='true']"):
410  showName = mp4Format.getparent().attrib['name']
411  mp4name = mp4Format.attrib['name']
412  elements = userRev3.xpath("//show[@name=$showName]/mp4Format[@name=$mp4name]", showName=showName, mp4name=mp4name)
413  if len(elements):
414  elements[0].attrib['enabled'] = u'true'
415 
416  if self.config['debug_enabled']:
417  print "After any merging userRev3:"
418  sys.stdout.write(etree.tostring(userRev3, encoding='UTF-8', pretty_print=True))
419  print
420 
421  # Save the rev3.xml file
422  prefDir = self.rev3_config.find('userPreferenceFile').text.replace(u'/rev3.xml', u'')
423  if not os.path.isdir(prefDir):
424  os.makedirs(prefDir)
425  fd = open(self.rev3_config.find('userPreferenceFile').text, 'w')
426  fd.write(u'<userRev3>\n'+u''.join(etree.tostring(element, encoding='UTF-8', pretty_print=True) for element in userRev3)+u'</userRev3>')
427  fd.close()
428 
429  # Read the refreshed user config file
430  try:
431  self.userPrefs = etree.parse(self.rev3_config.find('userPreferenceFile').text)
432  except Exception, e:
433  raise Rev3UrlError(self.error_messages['Rev3UrlError'] % (url, errormsg))
434  return
435  # end updateRev3()
436 
437  def getSeasonEpisode(self, title, link=None):
438  ''' Check is there is any season or episode number information in an item's title
439  return array of season and/or episode numbers
440  return array with None values
441  '''
442  s_e = [None, None]
443  for index in range(len(self.s_e_Patterns)):
444  match = self.s_e_Patterns[index].match(title)
445  if not match:
446  if link:
447  match = self.s_e_Patterns[index].match(link)
448  if not match:
449  continue
450  else:
451  continue
452  if index < 2:
453  s_e[0], s_e[1] = match.groups()
454  break
455  else:
456  s_e[1] = u'%s' % int(match.groups()[0])
457  break
458  return s_e
459  # end getSeasonEpisode()
460 
461  def getVideoID(self, link):
462  ''' Read the Web page to find the video ID number used for fullscreen autoplay
463  return the video ID number
464  return None if the number cannot be found
465  '''
466  videoID = None
467  try:
468  eTree = etree.parse(link, self.FullScreenParser)
469  except Exception, errormsg:
470  sys.stderr.write(u"! Error: The URL (%s) cause the exception error (%s)\n" % (link, errormsg))
471  return videoID
472 
473  if self.config['debug_enabled']:
474  print "Raw unfiltered URL imput:"
475  sys.stdout.write(etree.tostring(eTree, encoding='UTF-8', pretty_print=True))
476  print
477 
478  if not eTree:
479  return videoID
480 
481  # Filter out the video id
482  try:
483  tmpVideoID = self.FullScreenVidIDxPath(eTree)
484  except AssertionError, e:
485  sys.stderr.write("No filter results for VideoID from url(%s)\n" % link)
486  sys.stderr.write("! Error:(%s)\n" % e)
487  return videoID
488 
489  if len(tmpVideoID):
490  if tmpVideoID[0].get('id'):
491  videoID = tmpVideoID[0].attrib['id'].strip().replace(u'player-', u'')
492 
493  return videoID
494 
495 
500 
501 
502  def searchTitle(self, title, pagenumber, pagelen):
503  '''Key word video search of the Rev3 web site
504  return an array of matching item elements
505  return
506  '''
507  try:
508  searchVar = u'?q=%s' % (urllib.quote(title.encode("utf-8")).replace(u' ', u'+'))
509  except UnicodeDecodeError:
510  searchVar = u'?q=%s' % (urllib.quote(title).replace(u' ', u'+'))
511  url = self.rev3_config.find('searchURLS').xpath(".//href")[0].text+searchVar
512 
513  if self.config['debug_enabled']:
514  print url
515  print
516 
517  self.rev3_config.find('searchURLS').xpath(".//href")[0].text = url
518 
519  # Perform a search
520  try:
521  resultTree = self.common.getUrlData(self.rev3_config.find('searchURLS'), pageFilter=self.rev3_config.find('searchURLS').xpath(".//pageFilter")[0].text)
522  except Exception, errormsg:
523  raise Rev3UrlDownloadError(self.error_messages['Rev3UrlDownloadError'] % (errormsg))
524 
525  if resultTree is None:
526  raise Rev3VideoNotFound(u"No Rev3 Video matches found for search value (%s)" % title)
527 
528  searchResults = resultTree.xpath('//result//li[@class="video"]')
529  if not len(searchResults):
530  raise Rev3VideoNotFound(u"No Rev3 Video matches found for search value (%s)" % title)
531 
532  # Set the number of search results returned
533  self.channel['channel_numresults'] = len(searchResults)
534 
535  # Rev3 search results fo not have a pubDate so use the current data time
536  # e.g. "Sun, 06 Jan 2008 21:44:36 GMT"
537  pubDate = datetime.datetime.now().strftime(self.common.pubDateFormat)
538 
539  # Translate the search results into MNV RSS item format
540  thumbnailFilter = etree.XPath('.//a[@class="thumbnail"]/img/@src')
541  titleFilter = etree.XPath('.//a[@class="title"]')
542  descFilter = etree.XPath('.//div[@class="description"]')
543  itemThumbNail = etree.XPath('.//media:thumbnail', namespaces=self.common.namespaces)
544  itemDwnLink = etree.XPath('.//media:content', namespaces=self.common.namespaces)
545  itemDict = {}
546  for result in searchResults:
547  if len(titleFilter(result)): # Make sure that this result actually has a video
548  rev3Item = etree.XML(self.common.mnvItem)
549  # Extract and massage data
550  thumbNail = self.common.ampReplace(thumbnailFilter(result)[0])
551  title = self.common.massageText(titleFilter(result)[0].text.strip())
552  tmpDesc = etree.tostring(descFilter(result)[0], method="text", encoding=unicode).strip()
553  index = tmpDesc.find(u'¬ñ')
554  if index != -1:
555  tmpDesc = tmpDesc[index+1:].strip()
556  description = self.common.massageText(tmpDesc)
557  link = self.common.ampReplace(titleFilter(result)[0].attrib['href'])
558  author = u'Revision3'
559  # Insert data into a new item element
560  rev3Item.find('title').text = title
561  rev3Item.find('author').text = author
562  rev3Item.find('pubDate').text = pubDate
563  rev3Item.find('description').text = description
564  rev3Item.find('link').text = link
565  itemThumbNail(rev3Item)[0].attrib['url'] = thumbNail
566  itemDwnLink(rev3Item)[0].attrib['url'] = link
567  s_e = self.getSeasonEpisode(title, None)
568  if s_e[0]:
569  etree.SubElement(rev3Item, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = s_e[0]
570  if s_e[1]:
571  etree.SubElement(rev3Item, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = s_e[1]
572  itemDict[title.lower()] = rev3Item
573 
574  if not len(itemDict.keys()):
575  raise Rev3VideoNotFound(u"No Rev3 Video matches found for search value (%s)" % title)
576 
577  return [itemDict, resultTree.xpath('//pageInfo')[0].text]
578  # end searchTitle()
579 
580 
581  def searchForVideos(self, title, pagenumber):
582  """Common name for a video search. Used to interface with MythTV plugin NetVision
583  """
584  # Get rev3_config.xml
585  self.getRev3Config()
586 
587  if self.config['debug_enabled']:
588  print "self.rev3_config:"
589  sys.stdout.write(etree.tostring(self.rev3_config, encoding='UTF-8', pretty_print=True))
590  print
591 
592  # Easier for debugging
593 # print self.searchTitle(title, pagenumber, self.page_limit)
594 # print
595 # sys.exit()
596 
597  try:
598  data = self.searchTitle(title, pagenumber, self.page_limit)
599  except Rev3VideoNotFound, msg:
600  sys.stderr.write(u"%s\n" % msg)
601  sys.exit(0)
602  except Rev3UrlError, msg:
603  sys.stderr.write(u'%s\n' % msg)
604  sys.exit(1)
605  except Rev3HttpError, msg:
606  sys.stderr.write(self.error_messages['Rev3HttpError'] % msg)
607  sys.exit(1)
608  except Rev3RssError, msg:
609  sys.stderr.write(self.error_messages['Rev3RssError'] % msg)
610  sys.exit(1)
611  except Exception, e:
612  sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
613  sys.exit(1)
614 
615  # Create RSS element tree
616  rssTree = etree.XML(self.common.mnvRSS+u'</rss>')
617 
618  # Set the paging values
619  itemCount = len(data[0].keys())
620  if data[1] == 'true':
621  self.channel['channel_returned'] = itemCount
622  self.channel['channel_startindex'] = itemCount
623  self.channel['channel_numresults'] = itemCount+(self.page_limit*(int(pagenumber)-1)+1)
624  else:
625  self.channel['channel_returned'] = itemCount+(self.page_limit*(int(pagenumber)-1))
626  self.channel['channel_startindex'] = self.channel['channel_returned']
627  self.channel['channel_numresults'] = self.channel['channel_returned']
628 
629  # Add the Channel element tree
630  channelTree = self.common.mnvChannelElement(self.channel)
631  rssTree.append(channelTree)
632 
633  lastKey = None
634  for key in sorted(data[0].keys()):
635  if lastKey != key:
636  channelTree.append(data[0][key])
637  lastKey = key
638 
639  # Output the MNV search results
640  sys.stdout.write(u'<?xml version="1.0" encoding="UTF-8"?>\n')
641  sys.stdout.write(etree.tostring(rssTree, encoding='UTF-8', pretty_print=True))
642  sys.exit(0)
643  # end searchForVideos()
644 
645  def displayTreeView(self):
646  '''Gather the Revision3 feeds then get a max page of videos meta data in each of them
647  Display the results and exit
648  '''
649  personalFeed = u"Personal Feed" # A label used to identify processing of a personal RSS feed
650 
651  # Get the user preferences that specify which shows and formats they want to be in the treeview
652  try:
653  self.getUserPreferences()
654  except Exception, e:
655  sys.stderr.write(u'%s' % e)
656  sys.exit(1)
657 
658  if self.config['debug_enabled']:
659  print "self.userPrefs:"
660  sys.stdout.write(etree.tostring(self.userPrefs, encoding='UTF-8', pretty_print=True))
661  print
662 
663  # Verify that there is at least one RSS feed that user wants to download
664  rssFeeds = self.userPrefs.xpath("//mp4Format[@enabled='true']")
665  personalFeeds = self.userPrefs.xpath("//treeviewURLS//url[@enabled='true']")
666  if not len(rssFeeds) and not len(personalFeeds):
667  sys.stderr.write(u'There are no mp4Format or personal RSS feed elements "enabled" in your "rev3.xml" user preferences\nfile (%s)\n' % self.rev3_config.find('userPreferenceFile').text)
668  sys.exit(1)
669 
670  # Create a structure of feeds that can be concurrently downloaded
671  showData = etree.XML(u'<xml></xml>')
672  for feed in personalFeeds:
673  rssFeeds.append(feed)
674  count = 0
675  for rssFeed in rssFeeds:
676  if rssFeed.getparent().tag == 'treeviewURLS':
677  uniqueName = u'%s:%s' % (personalFeed, count)
678  count+=1
679  else:
680  uniqueName = u'%s:%s:%s' % (rssFeed.getparent().getparent().attrib['name'], rssFeed.getparent().attrib['name'], rssFeed.attrib['name'])
681  url = etree.XML(u'<url></url>')
682  etree.SubElement(url, "name").text = uniqueName
683  if uniqueName.startswith(personalFeed):
684  etree.SubElement(url, "href").text = rssFeed.text
685  else:
686  etree.SubElement(url, "href").text = rssFeed.attrib['rss']
687  etree.SubElement(url, "filter").text = u"//channel"
688  etree.SubElement(url, "parserType").text = u'xml'
689  showData.append(url)
690 
691  if self.config['debug_enabled']:
692  print "showData:"
693  sys.stdout.write(etree.tostring(showData, encoding='UTF-8', pretty_print=True))
694  print
695 
696  # Get the RSS Feed data
697  try:
698  resultTree = self.common.getUrlData(showData)
699  except Exception, errormsg:
700  raise Rev3UrlDownloadError(self.error_messages['Rev3UrlDownloadError'] % (errormsg))
701 
702  if resultTree is None:
703  sys.exit(0)
704 
705  if self.config['debug_enabled']:
706  print "resultTree:"
707  sys.stdout.write(etree.tostring(resultTree, encoding='UTF-8', pretty_print=True))
708  print
709 
710  # Create RSS element tree
711  rssTree = etree.XML(self.common.mnvRSS+u'</rss>')
712 
713  # Add the Channel element tree
714  channelTree = self.common.mnvChannelElement(self.channel)
715  rssTree.append(channelTree)
716 
717  # Process each directory of the user preferences that have an enabled rss feed
718  itemFilter = etree.XPath('.//item')
719  channelFilter = etree.XPath('./result/channel')
720  imageFilter = etree.XPath('.//image/url')
721  itemDwnLink = './/media:content'
722  itemThumbNail = './/media:thumbnail'
723  itemDuration = './/media:content'
724  itemLanguage = './/media:content'
725  categoryDir = None
726  showDir = None
727  categoryElement = etree.XML(u'<directory></directory>')
728  itemAuthor = u'Revision3'
729  for result in resultTree.findall('results'):
730  names = result.find('name').text.split(':')
731  for index in range(len(names)):
732  names[index] = self.common.massageText(names[index])
733  channel = channelFilter(result)[0]
734  if channel.find('image') != None:
735  channelThumbnail = self.common.ampReplace(imageFilter(channel)[0].text)
736  else:
737  channelThumbnail = self.common.ampReplace(channel.find('link').text.replace(u'/watch/', u'/images/')+u'100.jpg')
738  channelLanguage = u'en'
739  if channel.find('language') != None:
740  channelLanguage = channel.find('language').text[:2]
741  # Create a new directory and/or subdirectory if required
742  if names[0] != categoryDir:
743  if categoryDir != None:
744  channelTree.append(categoryElement)
745  categoryElement = etree.XML(u'<directory></directory>')
746  if names[0] == personalFeed:
747  categoryElement.attrib['name'] = channel.find('title').text
748  else:
749  categoryElement.attrib['name'] = names[0]
750  categoryElement.attrib['thumbnail'] = self.channel_icon
751  categoryDir = names[0]
752  if names[1] != showDir:
753  if names[0] == personalFeed:
754  showElement = categoryElement
755  else:
756  showElement = etree.SubElement(categoryElement, u"directory")
757  if names[2] == 'Web Only':
758  showElement.attrib['name'] = u'%s' % (names[1])
759  else:
760  showElement.attrib['name'] = u'%s: %s' % (names[1], names[2])
761  showElement.attrib['thumbnail'] = channelThumbnail
762  showDir = names[1]
763 
764  if self.config['debug_enabled']:
765  print "Results: #Items(%s) for (%s)" % (len(itemFilter(result)), names)
766  print
767 
768  # Convert each RSS item into a MNV item
769  for itemData in itemFilter(result):
770  rev3Item = etree.XML(self.common.mnvItem)
771  # Extract and massage data also insert data into a new item element
772  rev3Item.find('title').text = self.common.massageText(itemData.find('title').text.strip())
773  rev3Item.find('author').text = itemAuthor
774  rev3Item.find('pubDate').text = self.common.massageText(itemData.find('pubDate').text)
775  rev3Item.find('description').text = self.common.massageText(itemData.find('description').text.strip())
776  link = self.common.ampReplace(itemData.find('link').text)
777  downLoadLink = self.common.ampReplace(itemData.xpath(itemDwnLink, namespaces=self.common.namespaces)[0].attrib['url'])
778 
779  # If this is one of the main shows or from the personal RSS feed
780  # then get a full screen video id
781  if names[0] == 'Shows' or names[0] == personalFeed:
782  fullScreenVideoID = self.getVideoID(itemData.find('link').text)
783  if fullScreenVideoID:
784  if link == downLoadLink:
785  downLoadLink = self.common.ampReplace(self.FullScreen % fullScreenVideoID)
786  link = self.common.ampReplace(self.FullScreen % fullScreenVideoID)
787 
788  rev3Item.find('link').text = self.common.ampReplace(link)
789  rev3Item.xpath(itemDwnLink, namespaces=self.common.namespaces)[0].attrib['url'] = downLoadLink
790  try:
791  rev3Item.xpath(itemThumbNail, namespaces=self.common.namespaces)[0].attrib['url'] = self.common.ampReplace(itemData.xpath(itemThumbNail, namespaces=self.common.namespaces)[0].attrib['url'].replace(u'--mini', u'--medium'))
792  except IndexError:
793  pass
794  try:
795  rev3Item.xpath(itemDuration, namespaces=self.common.namespaces)[0].attrib['duration'] = itemData.xpath(itemDuration, namespaces=self.common.namespaces)[0].attrib['duration']
796  except KeyError:
797  pass
798  rev3Item.xpath(itemLanguage, namespaces=self.common.namespaces)[0].attrib['lang'] = channelLanguage
799  if rev3Item.xpath(itemThumbNail, namespaces=self.common.namespaces)[0].get('url'):
800  s_e = self.getSeasonEpisode(rev3Item.find('title').text, rev3Item.xpath(itemThumbNail, namespaces=self.common.namespaces)[0].attrib['url'])
801  else:
802  s_e = self.getSeasonEpisode(rev3Item.find('title').text, rev3Item.xpath(itemDwnLink, namespaces=self.common.namespaces)[0].attrib['url'])
803  if s_e[0]:
804  etree.SubElement(rev3Item, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = s_e[0]
805  if s_e[1]:
806  etree.SubElement(rev3Item, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = s_e[1]
807  if s_e[0] and s_e[1]:
808  rev3Item.find('title').text = u'S%02dE%02d: %s' % (int(s_e[0]), int (s_e[1]), rev3Item.find('title').text)
809  elif s_e[0]:
810  rev3Item.find('title').text = u'S%02d: %s' % (int(s_e[0]), rev3Item.find('title').text)
811  elif s_e[1]:
812  rev3Item.find('title').text = u'Ep%03d: %s' % (int(s_e[1]), rev3Item.find('title').text)
813  showElement.append(rev3Item)
814 
815  # Add the last directory processed
816  if categoryElement.xpath('.//item') != None:
817  channelTree.append(categoryElement)
818 
819  # Check that there was at least some items
820  if len(rssTree.xpath('//item')):
821  # Output the MNV search results
822  sys.stdout.write(u'<?xml version="1.0" encoding="UTF-8"?>\n')
823  sys.stdout.write(etree.tostring(rssTree, encoding='UTF-8', pretty_print=True))
824 
825  sys.exit(0)
826  # end displayTreeView()
827 # end Videos() class
def getSeasonEpisode(self, title, link=None)
Definition: rev3_api.py:437
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 __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
Definition: rev3_api.py:101
long long copy(QFile &dst, QFile &src, uint block_size)
Copies src file to dst file.
def searchForVideos(self, title, pagenumber)
Definition: rev3_api.py:581
def getRev3Config(self)
Start - Utility functions.
Definition: rev3_api.py:196
def updateRev3(self, create=False)
Definition: rev3_api.py:254
def __init__(self, outstream, encoding=None)
Definition: rev3_api.py:43
def searchTitle(self, title, pagenumber, pagelen)
End of Utility functions.
Definition: rev3_api.py:502