MythTV  master
mainProcess.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3 # ----------------------
4 # Name: mainProcess.py
5 # Python Script
6 # Author: R.D. Vaughan
7 # Purpose:
8 # This python script is the common process for all python MythTV Netvision plugin grabber processing.
9 # It follows the MythTV standards set for Netvision grabbers.
10 # Command example:
11 # See help (-u and -h) options
12 #
13 # Design:
14 # 1) Verify the command line options (display help or version and exit)
15 # 2) Call the target site with an information request according to the selected options
16 # 3) Prepare and display XML results from the information request
17 #
18 #
19 # License:Creative Commons GNU GPL v2
20 # (http://creativecommons.org/licenses/GPL/2.0/)
21 #-------------------------------------
22 __title__ ="Netvision Common Query Processing";
23 __author__="R.D.Vaughan"
24 __version__="v0.2.5"
25 # 0.1.0 Initial development
26 # 0.1.1 Refining the code like the additional of a grabber specifing the maximum number of items to return
27 # 0.1.2 Added the Tree view option
28 # 0.1.3 Updated deocumentation
29 # 0.2.0 Public release
30 # 0.2.1 Added the ability to have a mashup name independant of the mashup title
31 # 0.2.2 Added support of the -H option
32 # 0.2.3 Added support of the XML version information
33 # 0.2.4 Added the "command" tag to the XML version information
34 # 0.2.5 Fixed a bug in the setting up of a search mashup page item maximum
35 
36 import sys, os
37 from optparse import OptionParser
38 import re
39 from string import capitalize
40 
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, unicode):
54  try:
55  self.out.write(obj.encode(self.encoding))
56  except IOError:
57  pass
58  else:
59  try:
60  self.out.write(obj)
61  except IOError:
62  pass
63 
64  def __getattr__(self, attr):
65  """Delegate everything but write to the stream"""
66  return getattr(self.out, attr)
67 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
68 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
69 
70 
71 try:
72  from StringIO import StringIO
73  from lxml import etree
74 except Exception, e:
75  sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
76  sys.exit(1)
77 
78 # Check that the lxml library is current enough
79 # From the lxml documents it states: (http://codespeak.net/lxml/installation.html)
80 # "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later"
81 # Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package
82 version = ''
83 for digit in etree.LIBXML_VERSION:
84  version+=str(digit)+'.'
85 version = version[:-1]
86 if version < '2.7.2':
87  sys.stderr.write(u'''
88 ! Error - The installed version of the "lxml" python library "libxml" version is too old.
89  At least "libxml" version 2.7.2 must be installed. Your version is (%s).
90 ''' % version)
91  sys.exit(1)
92 
93 
94 class siteQueries():
95  '''Methods that quering video Web sites for metadata and outputs the results to stdout any errors are output
96  to stderr.
97  '''
98  def __init__(self,
99  apikey,
100  target,
101  mythtv = False,
102  interactive = False,
103  select_first = False,
104  debug = False,
105  custom_ui = None,
106  language = None,
107  search_all_languages = False,
108  ):
109  """apikey (str/unicode):
110  Specify the themoviedb.org API key. Applications need their own key.
111  See http://api.themoviedb.org/2.1/ to get your own API key
112 
113  mythtv (True/False):
114  When True, the movie metadata is being returned has the key and values massaged to match MythTV
115  When False, the movie metadata is being returned matches what TMDB returned
116 
117  interactive (True/False):
118  When True, uses built-in console UI is used to select the correct show.
119  When False, the first search result is used.
120 
121  select_first (True/False):
122  Automatically selects the first series search result (rather
123  than showing the user a list of more than one series).
124  Is overridden by interactive = False, or specifying a custom_ui
125 
126  debug (True/False):
127  shows verbose debugging information
128 
129  custom_ui (tvdb_ui.BaseUI subclass):
130  A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
131 
132  language (2 character language abbreviation):
133  The language of the returned data. Is also the language search
134  uses. Default is "en" (English). For full list, run..
136  >>> MovieDb().config['valid_languages'] #doctest: +ELLIPSIS
137  ['da', 'fi', 'nl', ...]
138 
139  search_all_languages (True/False):
140  By default, TMDB will only search in the language specified using
141  the language option. When this is True, it will search for the
142  show in any language
143 
144  """
145  self.config = {}
146 
147  self.config['apikey'] = apikey
148  self.config['target'] = target.Videos(apikey, mythtv = mythtv,
149  interactive = interactive,
150  select_first = select_first,
151  debug = debug,
152  custom_ui = custom_ui,
153  language = language,
154  search_all_languages = search_all_languages,)
155  self.searchKeys = [u'title', u'releasedate', u'overview', u'url', u'thumbnail', u'video', u'runtime', u'viewcount']
156 
157  self.searchXML = {'header': """<?xml version="1.0" encoding="UTF-8"?>""", 'rss': """
158 <rss version="2.0"
159 xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
160 xmlns:content="http://purl.org/rss/1.0/modules/content/"
161 xmlns:cnettv="http://cnettv.com/mrss/"
162 xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule"
163 xmlns:media="http://search.yahoo.com/mrss/"
164 xmlns:atom="http://www.w3.org/2005/Atom"
165 xmlns:amp="http://www.adobe.com/amp/1.0"
166 xmlns:dc="http://purl.org/dc/elements/1.1/"
167 xmlns:mythtv="http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format">""", 'channel': """
168  <channel>
169  <title>%(channel_title)s</title>
170  <link>%(channel_link)s</link>
171  <description>%(channel_description)s</description>
172  <numresults>%(channel_numresults)d</numresults>
173  <returned>%(channel_returned)d</returned>
174  <startindex>%(channel_startindex)d</startindex>""", 'item': """
175  <item>
176  <title>%(item_title)s</title>
177  <author>%(item_author)s</author>
178  <pubDate>%(item_pubdate)s</pubDate>
179  <description>%(item_description)s</description>
180  <link>%(item_link)s</link>
181  <media:group>
182  <media:thumbnail url='%(item_thumbnail)s'/>
183  <media:content url='%(item_url)s' duration='%(item_duration)s' width='%(item_width)s' height='%(item_height)s' lang='%(item_lang)s'/>
184  </media:group>
185  <rating>%(item_rating)s</rating>
186  </item>""", 'end_channel': """
187  </channel>""", 'end_rss': """
188 </rss>
189 """,
190  'nextpagetoken': """
191  <nextpagetoken>%(nextpagetoken)s</nextpagetoken>""",
192  'prevpagetoken': """
193  <prevpagetoken>%(prevpagetoken)s</prevpagetoken>"""
194  }
195 
196  self.treeViewXML = {'header': """<?xml version="1.0" encoding="UTF-8"?>""", 'rss': """
197 <rss version="2.0"
198 xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
199 xmlns:content="http://purl.org/rss/1.0/modules/content/"
200 xmlns:cnettv="http://cnettv.com/mrss/"
201 xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule"
202 xmlns:media="http://search.yahoo.com/mrss/"
203 xmlns:atom="http://www.w3.org/2005/Atom"
204 xmlns:amp="http://www.adobe.com/amp/1.0"
205 xmlns:dc="http://purl.org/dc/elements/1.1/"
206 xmlns:mythtv="http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format">""", 'start_channel': """
207  <channel>
208  <title>%(channel_title)s</title>
209  <link>%(channel_link)s</link>
210  <description>%(channel_description)s</description>
211  <numresults>%(channel_numresults)d</numresults>
212  <returned>%(channel_returned)d</returned>
213  <startindex>%(channel_startindex)d</startindex>""", "start_directory": """
214  <directory name="%s" thumbnail="%s">""", 'item': """
215  <item>
216  <title>%(item_title)s</title>
217  <author>%(item_author)s</author>
218  <pubDate>%(item_pubdate)s</pubDate>
219  <description>%(item_description)s</description>
220  <link>%(item_link)s</link>
221  <media:group>
222  <media:thumbnail url='%(item_thumbnail)s'/>
223  <media:content url='%(item_url)s' duration='%(item_duration)s' width='%(item_width)s' height='%(item_height)s' lang='%(item_lang)s'/>
224  </media:group>
225  <rating>%(item_rating)s</rating>
226  </item>""", "end_directory": """
227  </directory>""", 'end_channel': """
228  </channel>""", 'end_rss': """
229 </rss>
230 """, }
231 
232 
233  # end __init__()
234 
235  def searchForVideos(self, search_text, pagenumber):
236  '''Search for vidoes that match the search text and output the results a XML to stdout
237  '''
238  self.firstVideo = True
239  self.config['target'].page_limit = self.page_limit
240  self.config['target'].grabber_title = self.grabber_title
241  self.config['target'].mashup_title = self.mashup_title
242 
243  data_sets = self.config['target'].searchForVideos(search_text, pagenumber)
244  if data_sets == None:
245  return
246  if not len(data_sets):
247  return
248 
249  for data_set in data_sets:
250  if self.firstVideo:
251  sys.stdout.write(self.searchXML['header'])
252  sys.stdout.write(self.searchXML['rss'])
253  self.firstVideo = False
254  sys.stdout.write(self.searchXML['channel'] % data_set[0])
255  if 'nextpagetoken' in data_set[0]:
256  sys.stdout.write(self.searchXML['nextpagetoken'] % data_set[0])
257  if 'prevpagetoken' in data_set[0]:
258  sys.stdout.write(self.searchXML['prevpagetoken'] % data_set[0])
259  for item in data_set[1]:
260  sys.stdout.write(self.searchXML['item'] % item)
261  sys.stdout.write(self.searchXML['end_channel'])
262  sys.stdout.write(self.searchXML['end_rss'])
263  # end searchForVideos()
264 
265  def displayTreeView(self):
266  '''Create a Tree View specific to a target site and output the results a XML to stdout. Nested
267  directories are permissable.
268  '''
269  self.firstVideo = True
270  self.config['target'].page_limit = self.page_limit
271  self.config['target'].grabber_title = self.grabber_title
272  self.config['target'].mashup_title = self.mashup_title
273 
274  data_sets = self.config['target'].displayTreeView()
275  if data_sets == None:
276  return
277  if not len(data_sets):
278  return
279 
280  for data_set in data_sets:
281  if self.firstVideo:
282  sys.stdout.write(self.treeViewXML['header'])
283  sys.stdout.write(self.treeViewXML['rss'])
284  self.firstVideo = False
285  sys.stdout.write(self.treeViewXML['start_channel'] % data_set[0])
286  for directory in data_set[1]:
287  if isinstance(directory, list):
288  if directory[0] == '':
289  sys.stdout.write(self.treeViewXML['end_directory'])
290  continue
291  sys.stdout.write(self.treeViewXML['start_directory'] % (directory[0], directory[1]))
292  continue
293  sys.stdout.write(self.treeViewXML['item'] % directory)
294  sys.stdout.write(self.treeViewXML['end_channel'])
295  sys.stdout.write(self.treeViewXML['end_rss'])
296  # end displayTreeView()
297 
298  def displayHTML(self, videocode):
299  '''Request a custom Web page from the grabber and display on stdout
300  '''
301  self.firstVideo = True
302  self.config['target'].page_limit = self.page_limit
303  self.config['target'].grabber_title = self.grabber_title
304  self.config['target'].mashup_title = self.mashup_title
305  self.config['target'].HTMLvideocode = videocode
306 
307  sys.stdout.write(self.config['target'].displayCustomHTML())
308  return
309  # end displayHTML()
310 
311 # end Class siteQueries()
312 
314  '''Common processing for all Netvision python grabbers.
315  '''
316  def __init__(self, target, apikey, ):
317  '''
318  '''
319  self.target = target
320  self.apikey = apikey
321  # end __init__()
322 
323 
324  def main(self):
325  """Gets video details a search term
326  """
327  parser = OptionParser()
328 
329  parser.add_option( "-d", "--debug", action="store_true", default=False, dest="debug",
330  help=u"Show debugging info (URLs, raw XML ... etc, info varies per grabber)")
331  parser.add_option( "-u", "--usage", action="store_true", default=False, dest="usage",
332  help=u"Display examples for executing the script")
333  parser.add_option( "-v", "--version", action="store_true", default=False, dest="version",
334  help=u"Display grabber name and supported options")
335  parser.add_option( "-l", "--language", metavar="LANGUAGE", default=u'', dest="language",
336  help=u"Select data that matches the specified language fall back to English if nothing found (e.g. 'es' EspaƱol, 'de' Deutsch ... etc).\nNot all sites or grabbers support this option.")
337  parser.add_option( "-p", "--pagenumber", metavar="PAGE NUMBER", default=1, dest="pagenumber",
338  help=u"Display specific page of the search results. Default is page 1.\nPage number is ignored with the Tree View option (-T).")
339  functionality = u''
340  if self.grabberInfo['search']:
341  parser.add_option( "-S", "--search", action="store_true", default=False, dest="search",
342  help=u"Search for videos")
343  functionality+='S'
344 
345  if self.grabberInfo['tree']:
346  parser.add_option( "-T", "--treeview", action="store_true", default=False, dest="treeview",
347  help=u"Display a Tree View of a sites videos")
348  functionality+='T'
349 
350  if self.grabberInfo['html']:
351  parser.add_option( "-H", "--customhtml", action="store_true", default=False,
352  dest="customhtml", help=u"Return a custom HTML Web page")
353  functionality+='H'
354 
355  parser.usage=u"./%%prog -hduvl%s [parameters] <search text>\nVersion: %s Author: %s\n\nFor details on the MythTV Netvision plugin see the wiki page at:\nhttp://www.mythtv.org/wiki/MythNetvision" % (functionality, self.grabberInfo['version'], self.grabberInfo['author'])
356 
357  opts, args = parser.parse_args()
358 
359  # Make alls command line arguments unicode utf8
360  for index in range(len(args)):
361  args[index] = unicode(args[index], 'utf8')
362 
363  if opts.debug:
364  sys.stdout.write("\nopts: %s\n" % opts)
365  sys.stdout.write("\nargs: %s\n\n" % args)
366 
367  # Process version command line requests
368  if opts.version:
369  version = etree.XML(u'<grabber></grabber>')
370  etree.SubElement(version, "name").text = self.grabberInfo['title']
371  etree.SubElement(version, "author").text = self.grabberInfo['author']
372  etree.SubElement(version, "thumbnail").text = self.grabberInfo['thumbnail']
373  etree.SubElement(version, "command").text = self.grabberInfo['command']
374  for t in self.grabberInfo['type']:
375  etree.SubElement(version, "type").text = t
376  etree.SubElement(version, "description").text = self.grabberInfo['desc']
377  etree.SubElement(version, "version").text = self.grabberInfo['version']
378  if self.grabberInfo['search']:
379  etree.SubElement(version, "search").text = 'true'
380  if self.grabberInfo['tree']:
381  etree.SubElement(version, "tree").text = 'true'
382  sys.stdout.write(etree.tostring(version, encoding='UTF-8', pretty_print=True))
383  sys.exit(0)
384 
385  # Process usage command line requests
386  if opts.usage:
387  sys.stdout.write(self.grabberInfo['usage'])
388  sys.exit(0)
389 
390  if self.grabberInfo['search']:
391  if opts.search and not len(args) == 1:
392  sys.stderr.write("! Error: There must be one value for the search option. Your options are (%s)\n" % (args))
393  sys.exit(1)
394  if opts.search and args[0] == u'':
395  sys.stderr.write("! Error: There must be a non-empty search argument, yours is empty.\n")
396  sys.exit(1)
397 
398  if self.grabberInfo['html']:
399  if opts.customhtml and not len(args) == 1:
400  sys.stderr.write("! Error: There must be one value for the search option. Your options are (%s)\n" % (args))
401  sys.exit(1)
402  if opts.customhtml and args[0] == u'':
403  sys.stderr.write("! Error: There must be a non-empty Videocode argument, yours is empty.\n")
404  sys.exit(1)
405 
406  if not self.grabberInfo['search'] and not self.grabberInfo['tree'] and not self.grabberInfo['html']:
407  sys.stderr.write("! Error: You have not selected a valid option.\n")
408  sys.exit(1)
409 
410  Queries = siteQueries(self.apikey, self.target,
411  mythtv = True,
412  interactive = False,
413  select_first = False,
414  debug = opts.debug,
415  custom_ui = None,
416  language = opts.language,
417  search_all_languages = False,)
418 
419  # Set the maximum number of items to display per Mythtvnetvision search page
420  if self.grabberInfo['search']:
421  if opts.search:
422  if not 'SmaxPage' in self.grabberInfo.keys():
423  Queries.page_limit = 20 # Default items per page
424  else:
425  Queries.page_limit = self.grabberInfo['SmaxPage']
426 
427  # Set the maximum number of items to display per Mythtvnetvision tree view page
428  if self.grabberInfo['tree']:
429  if opts.treeview:
430  if not 'TmaxPage' in self.grabberInfo.keys():
431  Queries.page_limit = 20 # Default items per page
432  else:
433  Queries.page_limit = self.grabberInfo['TmaxPage']
434 
435  # Set the grabber title
436  Queries.grabber_title = self.grabberInfo['title']
437 
438  # Set the mashup title
439  if not 'mashup_title' in self.grabberInfo.keys():
440  Queries.mashup_title = Queries.grabber_title
441  else:
442  if self.grabberInfo['search']:
443  if opts.search:
444  Queries.mashup_title = self.grabberInfo['mashup_title'] + "search"
445  if self.grabberInfo['tree']:
446  if opts.treeview:
447  Queries.mashup_title = self.grabberInfo['mashup_title'] + "treeview"
448  if self.grabberInfo['html']:
449  if opts.customhtml:
450  Queries.mashup_title = self.grabberInfo['mashup_title'] + "customhtml"
451 
452  # Process requested option
453  if self.grabberInfo['search']:
454  if opts.search: # Video search -S
455  self.page_limit = Queries.page_limit
456  Queries.searchForVideos(args[0], opts.pagenumber)
457 
458  if self.grabberInfo['tree']:
459  if opts.treeview: # Video treeview -T
460  self.page_limit = Queries.page_limit
461  Queries.displayTreeView()
462 
463  if self.grabberInfo['html']:
464  if opts.customhtml: # Video treeview -H
465  Queries.displayHTML(args[0])
466 
467  sys.exit(0)
468  # end main()
def __init__(self, outstream, encoding=None)
Definition: mainProcess.py:44
def searchForVideos(self, search_text, pagenumber)
Definition: mainProcess.py:235
def __init__(self, apikey, target, mythtv=False, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
Definition: mainProcess.py:98
def __init__(self, target, apikey)
Definition: mainProcess.py:316