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__='''
17This python script is intended to perform a variety of utility functions to search and access text
18meta 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
20a 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
33import os, struct, sys, re, time, datetime, urllib.request, urllib.parse, urllib.error, re
34import logging
35from socket import gethostname, gethostbyname
36from threading import Thread
37from copy import deepcopy
38
39from .rev3_exceptions import (Rev3UrlError, Rev3HttpError, Rev3RssError, Rev3VideoNotFound, Rev3ConfigFileError, Rev3UrlDownloadError)
40import io
41
42class 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
61if isinstance(sys.stdout, io.TextIOWrapper):
62 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
63 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
64
65
66try:
67 from io import StringIO
68 from lxml import etree
69except 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
74class 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
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
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
def __init__(self, outstream, encoding=None)
Definition: rev3_api.py:44
def getRev3Config(self)
Start - Utility functions.
Definition: rev3_api.py:177
def searchTitle(self, title, pagenumber, pagelen)
End of Utility functions.
Definition: rev3_api.py:483
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:91
def getSeasonEpisode(self, title, link=None)
Definition: rev3_api.py:418
def updateRev3(self, create=False)
Definition: rev3_api.py:235
def searchForVideos(self, title, pagenumber)
Definition: rev3_api.py:562
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)
MBASE_PUBLIC long long copy(QFile &dst, QFile &src, uint block_size=0)
Copies src file to dst file.
static void print(const QList< uint > &raw_minimas, const QList< uint > &raw_maximas, const QList< float > &minimas, const QList< float > &maximas)