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