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