MythTV master
mainProcess.py
Go to the documentation of this file.
1# -*- coding: UTF-8 -*-
2
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.6"
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# 0.2.6 R. Ernst: Converted to python3, added option to disable a grabber script
36
37import sys, os
38from optparse import OptionParser
39import io
40
41class OutStreamEncoder(object):
42 """Wraps a stream with an encoder"""
43 def __init__(self, outstream, encoding=None):
44 self.out = outstream
45 if not encoding:
46 self.encoding = sys.getfilesystemencoding()
47 else:
48 self.encoding = encoding
49
50 def write(self, obj):
51 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
52 if isinstance(obj, str):
53 obj = obj.encode(self.encoding)
54 self.out.buffer.write(obj)
55
56 def __getattr__(self, attr):
57 """Delegate everything but write to the stream"""
58 return getattr(self.out, attr)
59
60if isinstance(sys.stdout, io.TextIOWrapper):
61 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
62 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
63
64
65try:
66 from io import StringIO
67 from lxml import etree
68except Exception as e:
69 sys.stderr.write('\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
70 sys.exit(1)
71
72
74 '''Methods that quering video Web sites for metadata and outputs the results to stdout any errors are output
75 to stderr.
76 '''
77 def __init__(self,
78 apikey,
79 target,
80 mythtv = False,
81 interactive = False,
82 select_first = False,
83 debug = False,
84 custom_ui = None,
85 language = None,
86 search_all_languages = False,
87 ):
88 """apikey (str/unicode):
89 Specify the themoviedb.org API key. Applications need their own key.
90 See http://api.themoviedb.org/2.1/ to get your own API key
91
92 mythtv (True/False):
93 When True, the movie metadata is being returned has the key and values massaged to match MythTV
94 When False, the movie metadata is being returned matches what TMDB returned
95
96 interactive (True/False):
97 When True, uses built-in console UI is used to select the correct show.
98 When False, the first search result is used.
99
100 select_first (True/False):
101 Automatically selects the first series search result (rather
102 than showing the user a list of more than one series).
103 Is overridden by interactive = False, or specifying a custom_ui
104
105 debug (True/False):
106 shows verbose debugging information
107
108 custom_ui (tvdb_ui.BaseUI subclass):
109 A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
110
111 language (2 character language abbreviation):
112 The language of the returned data. Is also the language search
113 uses. Default is "en" (English). For full list, run..
114
115 >>> MovieDb().config['valid_languages'] #doctest: +ELLIPSIS
116 ['da', 'fi', 'nl', ...]
117
118 search_all_languages (True/False):
119 By default, TMDB 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 self.config['apikey'] = apikey
127 self.config['target'] = target.Videos(apikey, mythtv = mythtv,
128 interactive = interactive,
129 select_first = select_first,
130 debug = debug,
131 custom_ui = custom_ui,
132 language = language,
133 search_all_languages = search_all_languages,)
134 self.searchKeys = ['title', 'releasedate', 'overview', 'url', 'thumbnail', 'video', 'runtime', 'viewcount']
135
136 self.searchXML = {'header': """<?xml version="1.0" encoding="UTF-8"?>""", 'rss': """
137<rss version="2.0"
138xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
139xmlns:content="http://purl.org/rss/1.0/modules/content/"
140xmlns:cnettv="http://cnettv.com/mrss/"
141xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule"
142xmlns:media="http://search.yahoo.com/mrss/"
143xmlns:atom="http://www.w3.org/2005/Atom"
144xmlns:amp="http://www.adobe.com/amp/1.0"
145xmlns:dc="http://purl.org/dc/elements/1.1/"
146xmlns:mythtv="http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format">""", 'channel': """
147 <channel>
148 <title>%(channel_title)s</title>
149 <link>%(channel_link)s</link>
150 <description>%(channel_description)s</description>
151 <numresults>%(channel_numresults)d</numresults>
152 <returned>%(channel_returned)d</returned>
153 <startindex>%(channel_startindex)d</startindex>""", 'item': """
154 <item>
155 <title>%(item_title)s</title>
156 <author>%(item_author)s</author>
157 <pubDate>%(item_pubdate)s</pubDate>
158 <description>%(item_description)s</description>
159 <link>%(item_link)s</link>
160 <media:group>
161 <media:thumbnail url='%(item_thumbnail)s'/>
162 <media:content url='%(item_url)s' duration='%(item_duration)s' width='%(item_width)s' height='%(item_height)s' lang='%(item_lang)s'/>
163 </media:group>
164 <rating>%(item_rating)s</rating>
165 </item>""", 'end_channel': """
166 </channel>""", 'end_rss': """
167</rss>
168""",
169 'nextpagetoken': """
170 <nextpagetoken>%(nextpagetoken)s</nextpagetoken>""",
171 'prevpagetoken': """
172 <prevpagetoken>%(prevpagetoken)s</prevpagetoken>"""
173 }
174
175 self.treeViewXML = {'header': """<?xml version="1.0" encoding="UTF-8"?>""", 'rss': """
176<rss version="2.0"
177xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
178xmlns:content="http://purl.org/rss/1.0/modules/content/"
179xmlns:cnettv="http://cnettv.com/mrss/"
180xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule"
181xmlns:media="http://search.yahoo.com/mrss/"
182xmlns:atom="http://www.w3.org/2005/Atom"
183xmlns:amp="http://www.adobe.com/amp/1.0"
184xmlns:dc="http://purl.org/dc/elements/1.1/"
185xmlns:mythtv="http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format">""", 'start_channel': """
186 <channel>
187 <title>%(channel_title)s</title>
188 <link>%(channel_link)s</link>
189 <description>%(channel_description)s</description>
190 <numresults>%(channel_numresults)d</numresults>
191 <returned>%(channel_returned)d</returned>
192 <startindex>%(channel_startindex)d</startindex>""", "start_directory": """
193 <directory name="%s" thumbnail="%s">""", 'item': """
194 <item>
195 <title>%(item_title)s</title>
196 <author>%(item_author)s</author>
197 <pubDate>%(item_pubdate)s</pubDate>
198 <description>%(item_description)s</description>
199 <link>%(item_link)s</link>
200 <media:group>
201 <media:thumbnail url='%(item_thumbnail)s'/>
202 <media:content url='%(item_url)s' duration='%(item_duration)s' width='%(item_width)s' height='%(item_height)s' lang='%(item_lang)s'/>
203 </media:group>
204 <rating>%(item_rating)s</rating>
205 </item>""", "end_directory": """
206 </directory>""", 'end_channel': """
207 </channel>""", 'end_rss': """
208</rss>
209""", }
210
211
212 # end __init__()
213
214 def searchForVideos(self, search_text, pagenumber):
215 '''Search for vidoes that match the search text and output the results a XML to stdout
216 '''
217 self.firstVideo = True
218 self.config['target'].page_limit = self.page_limit
219 self.config['target'].grabber_title = self.grabber_title
220 self.config['target'].mashup_title = self.mashup_title
221
222 data_sets = self.config['target'].searchForVideos(search_text, pagenumber)
223 if data_sets is None:
224 return
225 if not len(data_sets):
226 return
227
228 for data_set in data_sets:
229 if self.firstVideo:
230 sys.stdout.write(self.searchXML['header'])
231 sys.stdout.write(self.searchXML['rss'])
232 self.firstVideo = False
233 sys.stdout.write(self.searchXML['channel'] % data_set[0])
234 if 'nextpagetoken' in data_set[0]:
235 sys.stdout.write(self.searchXML['nextpagetoken'] % data_set[0])
236 if 'prevpagetoken' in data_set[0]:
237 sys.stdout.write(self.searchXML['prevpagetoken'] % data_set[0])
238 for item in data_set[1]:
239 sys.stdout.write(self.searchXML['item'] % item)
240 sys.stdout.write(self.searchXML['end_channel'])
241 sys.stdout.write(self.searchXML['end_rss'])
242 # end searchForVideos()
243
245 '''Create a Tree View specific to a target site and output the results a XML to stdout. Nested
246 directories are permissable.
247 '''
248 self.firstVideo = True
249 self.config['target'].page_limit = self.page_limit
250 self.config['target'].grabber_title = self.grabber_title
251 self.config['target'].mashup_title = self.mashup_title
252
253 data_sets = self.config['target'].displayTreeView()
254 if data_sets is None:
255 return
256 if not len(data_sets):
257 return
258
259 for data_set in data_sets:
260 if self.firstVideo:
261 sys.stdout.write(self.treeViewXML['header'])
262 sys.stdout.write(self.treeViewXML['rss'])
263 self.firstVideo = False
264 sys.stdout.write(self.treeViewXML['start_channel'] % data_set[0])
265 for directory in data_set[1]:
266 if isinstance(directory, list):
267 if directory[0] == '':
268 sys.stdout.write(self.treeViewXML['end_directory'])
269 continue
270 sys.stdout.write(self.treeViewXML['start_directory'] % (directory[0], directory[1]))
271 continue
272 sys.stdout.write(self.treeViewXML['item'] % directory)
273 sys.stdout.write(self.treeViewXML['end_channel'])
274 sys.stdout.write(self.treeViewXML['end_rss'])
275 # end displayTreeView()
276
277 def displayHTML(self, videocode):
278 '''Request a custom Web page from the grabber and display on stdout
279 '''
280 self.firstVideo = True
281 self.config['target'].page_limit = self.page_limit
282 self.config['target'].grabber_title = self.grabber_title
283 self.config['target'].mashup_title = self.mashup_title
284 self.config['target'].HTMLvideocode = videocode
285
286 sys.stdout.write(self.config['target'].displayCustomHTML())
287 return
288 # end displayHTML()
289
290# end Class siteQueries()
291
293 '''Common processing for all Netvision python grabbers.
294 '''
295 def __init__(self, target, apikey, ):
296 '''
297 '''
298 self.target = target
299 self.apikey = apikey
300 # end __init__()
301
302
303 def main(self):
304 """Gets video details a search term
305 """
306 parser = OptionParser()
307
308 parser.add_option( "-d", "--debug", action="store_true", default=False, dest="debug",
309 help="Show debugging info (URLs, raw XML ... etc, info varies per grabber)")
310 parser.add_option( "-u", "--usage", action="store_true", default=False, dest="usage",
311 help="Display examples for executing the script")
312 parser.add_option( "-v", "--version", action="store_true", default=False, dest="version",
313 help="Display grabber name and supported options")
314 parser.add_option( "-l", "--language", metavar="LANGUAGE", default='', dest="language",
315 help="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.")
316 parser.add_option( "-p", "--pagenumber", metavar="PAGE NUMBER", default=1, dest="pagenumber",
317 help="Display specific page of the search results. Default is page 1.\nPage number is ignored with the Tree View option (-T).")
318 functionality = ''
319 if self.grabberInfo['search']:
320 parser.add_option( "-S", "--search", action="store_true", default=False, dest="search",
321 help="Search for videos")
322 functionality+='S'
323
324 if self.grabberInfo['tree']:
325 parser.add_option( "-T", "--treeview", action="store_true", default=False, dest="treeview",
326 help="Display a Tree View of a sites videos")
327 functionality+='T'
328
329 if self.grabberInfo['html']:
330 parser.add_option( "-H", "--customhtml", action="store_true", default=False,
331 dest="customhtml", help="Return a custom HTML Web page")
332 functionality+='H'
333
334 parser.usage="./%%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'])
335
336 opts, args = parser.parse_args()
337
338 # Make alls command line arguments unicode utf8
339 for index in range(len(args)):
340 args[index] = str(args[index])
341
342 if opts.debug:
343 sys.stdout.write("\nopts: %s\n" % opts)
344 sys.stdout.write("\nargs: %s\n\n" % args)
345
346 # Process version command line requests
347 if opts.version:
348 # grabber scripts must be explicitly enabled
349 if self.grabberInfo.get('enabled', False):
350 version = etree.XML('<grabber></grabber>')
351 else:
352 version = etree.XML('<disabledgrabber></disabledgrabber>')
353 etree.SubElement(version, "name").text = self.grabberInfo['title']
354 etree.SubElement(version, "author").text = self.grabberInfo['author']
355 etree.SubElement(version, "thumbnail").text = self.grabberInfo['thumbnail']
356 etree.SubElement(version, "command").text = self.grabberInfo['command']
357 for t in self.grabberInfo['type']:
358 etree.SubElement(version, "type").text = t
359 etree.SubElement(version, "description").text = self.grabberInfo['desc']
360 etree.SubElement(version, "version").text = self.grabberInfo['version']
361 if self.grabberInfo['search']:
362 etree.SubElement(version, "search").text = 'true'
363 if self.grabberInfo['tree']:
364 etree.SubElement(version, "tree").text = 'true'
365 sys.stdout.write(etree.tostring(version, encoding='UTF-8', pretty_print=True))
366 sys.exit(0)
367
368 # Process usage command line requests
369 if opts.usage:
370 sys.stdout.write(self.grabberInfo['usage'])
371 sys.exit(0)
372
373 if self.grabberInfo['search']:
374 if opts.search and not len(args) == 1:
375 sys.stderr.write("! Error: There must be one value for the search option. Your options are (%s)\n" % (args))
376 sys.exit(1)
377 if opts.search and args[0] == '':
378 sys.stderr.write("! Error: There must be a non-empty search argument, yours is empty.\n")
379 sys.exit(1)
380
381 if self.grabberInfo['html']:
382 if opts.customhtml and not len(args) == 1:
383 sys.stderr.write("! Error: There must be one value for the search option. Your options are (%s)\n" % (args))
384 sys.exit(1)
385 if opts.customhtml and args[0] == '':
386 sys.stderr.write("! Error: There must be a non-empty Videocode argument, yours is empty.\n")
387 sys.exit(1)
388
389 if not self.grabberInfo['search'] and not self.grabberInfo['tree'] and not self.grabberInfo['html']:
390 sys.stderr.write("! Error: You have not selected a valid option.\n")
391 sys.exit(1)
392
393 Queries = siteQueries(self.apikey, self.target,
394 mythtv = True,
395 interactive = False,
396 select_first = False,
397 debug = opts.debug,
398 custom_ui = None,
399 language = opts.language,
400 search_all_languages = False,)
401
402 # Set the maximum number of items to display per Mythtvnetvision search page
403 if self.grabberInfo['search']:
404 if opts.search:
405 if not 'SmaxPage' in list(self.grabberInfo.keys()):
406 Queries.page_limit = 20 # Default items per page
407 else:
408 Queries.page_limit = self.grabberInfo['SmaxPage']
409
410 # Set the maximum number of items to display per Mythtvnetvision tree view page
411 if self.grabberInfo['tree']:
412 if opts.treeview:
413 if not 'TmaxPage' in list(self.grabberInfo.keys()):
414 Queries.page_limit = 20 # Default items per page
415 else:
416 Queries.page_limit = self.grabberInfo['TmaxPage']
417
418 # Set the grabber title
419 Queries.grabber_title = self.grabberInfo['title']
420
421 # Set the mashup title
422 if not 'mashup_title' in list(self.grabberInfo.keys()):
423 Queries.mashup_title = Queries.grabber_title
424 else:
425 if self.grabberInfo['search']:
426 if opts.search:
427 Queries.mashup_title = self.grabberInfo['mashup_title'] + "search"
428 if self.grabberInfo['tree']:
429 if opts.treeview:
430 Queries.mashup_title = self.grabberInfo['mashup_title'] + "treeview"
431 if self.grabberInfo['html']:
432 if opts.customhtml:
433 Queries.mashup_title = self.grabberInfo['mashup_title'] + "customhtml"
434
435 # Process requested option
436 if self.grabberInfo['search']:
437 if opts.search: # Video search -S
438 self.page_limit = Queries.page_limit
439 Queries.searchForVideos(args[0], opts.pagenumber)
440
441 if self.grabberInfo['tree']:
442 if opts.treeview: # Video treeview -T
443 self.page_limit = Queries.page_limit
444 Queries.displayTreeView()
445
446 if self.grabberInfo['html']:
447 if opts.customhtml: # Video treeview -H
448 Queries.displayHTML(args[0])
449
450 sys.exit(0)
451 # end main()
def __init__(self, outstream, encoding=None)
Definition: mainProcess.py:43
def __init__(self, target, apikey)
Definition: mainProcess.py:295
def searchForVideos(self, search_text, pagenumber)
Definition: mainProcess.py:214
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:87