MythTV  master
mnvsearch_api.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3 # ----------------------
4 # Name: mnvsearch_api - Simple-to-use Python interface to search the MythNetvision data base tables
5 #
6 # Python Script
7 # Author: R.D. Vaughan
8 # This python script is intended to perform a data base search of MythNetvision data base tables for
9 # videos based on a command line search term.
10 #
11 # License:Creative Commons GNU GPL v2
12 # (http://creativecommons.org/licenses/GPL/2.0/)
13 #-------------------------------------
14 __title__ ="mnvsearch_api - Simple-to-use Python interface to search the MythNetvision data base tables"
15 __author__="R.D. Vaughan"
16 __purpose__='''
17 This python script is intended to perform a data base search of MythNetvision data base tables for
18 videos based on a command line search term.
19 '''
20 
21 __version__="0.1.4"
22 # 0.1.0 Initial development
23 # 0.1.1 Changed the logger to only output to stderr rather than a file
24 # 0.1.2 Changed the SQL query to the new "internetcontentarticles" table format and new fields
25 # Added "%SHAREDIR%" to icon directory path
26 # 0.1.3 Video duration value was being erroneously multiplied by 60.
27 # 0.1.4 Add the ability to search within a specific "feedtitle". Used mainly for searching large mashups
28 # Fixed a paging bug
29 
30 import os, struct, sys, re, time, datetime, shutil, urllib
31 import logging
32 from socket import gethostname, gethostbyname
33 from threading import Thread
34 from copy import deepcopy
35 from operator import itemgetter, attrgetter
36 
37 from mnvsearch_exceptions import (MNVSQLError, MNVVideoNotFound, )
38 
39 class OutStreamEncoder(object):
40  """Wraps a stream with an encoder"""
41  def __init__(self, outstream, encoding=None):
42  self.out = outstream
43  if not encoding:
44  self.encoding = sys.getfilesystemencoding()
45  else:
46  self.encoding = encoding
47 
48  def write(self, obj):
49  """Wraps the output stream, encoding Unicode strings with the specified encoding"""
50  if isinstance(obj, unicode):
51  try:
52  self.out.write(obj.encode(self.encoding))
53  except IOError:
54  pass
55  else:
56  try:
57  self.out.write(obj)
58  except IOError:
59  pass
60 
61  def __getattr__(self, attr):
62  """Delegate everything but write to the stream"""
63  return getattr(self.out, attr)
64 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
65 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
66 
67 
68 # Find out if the MythTV python bindings can be accessed and instances can created
69 try:
70  '''If the MythTV python interface is found, required to access Netvision icon directory settings
71  '''
72  from MythTV import MythDB, MythLog
73  try:
74  '''Create an instance of each: MythDB
75  '''
76  MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout
77  mythdb = MythDB()
78  except MythError, e:
79  sys.stderr.write(u'\n! Error - %s\n' % e.args[0])
80  filename = os.path.expanduser("~")+'/.mythtv/config.xml'
81  if not os.path.isfile(filename):
82  sys.stderr.write(u'\n! Error - A correctly configured (%s) file must exist\n' % filename)
83  else:
84  sys.stderr.write(u'\n! Error - Check that (%s) is correctly configured\n' % filename)
85  sys.exit(1)
86  except Exception, e:
87  sys.stderr.write(u"\n! Error - Creating an instance caused an error for one of: MythDB. error(%s)\n" % e)
88  sys.exit(1)
89 except Exception, e:
90  sys.stderr.write(u"\n! Error - MythTV python bindings could not be imported. error(%s)\n" % e)
91  sys.exit(1)
92 
93 
94 try:
95  from StringIO import StringIO
96  from lxml import etree
97 except Exception, e:
98  sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e)
99  sys.exit(1)
100 
101 # Check that the lxml library is current enough
102 # From the lxml documents it states: (http://codespeak.net/lxml/installation.html)
103 # "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later"
104 # Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package
105 version = ''
106 for digit in etree.LIBXML_VERSION:
107  version+=str(digit)+'.'
108 version = version[:-1]
109 if version < '2.7.2':
110  sys.stderr.write(u'''
111 ! Error - The installed version of the "lxml" python library "libxml" version is too old.
112  At least "libxml" version 2.7.2 must be installed. Your version is (%s).
113 ''' % version)
114  sys.exit(1)
115 
116 
117 class Videos(object):
118  """Main interface to the MNV treeview table search
119  This is done to support a common naming framework for all python Netvision plugins no matter their site
120  target.
121 
122  Supports search methods
123  The apikey is a not required for this grabber
124  """
125  def __init__(self,
126  apikey,
127  mythtv = True,
128  interactive = False,
129  select_first = False,
130  debug = False,
131  custom_ui = None,
132  language = None,
133  search_all_languages = False,
134  ):
135  """apikey (str/unicode):
136  Specify the target site API key. Applications need their own key in some cases
137 
138  mythtv (True/False):
139  When True, the returned meta data is being returned has the key and values massaged to match MythTV
140  When False, the returned meta data is being returned matches what target site returned
141 
142  interactive (True/False): (This option is not supported by all target site apis)
143  When True, uses built-in console UI is used to select the correct show.
144  When False, the first search result is used.
145 
146  select_first (True/False): (This option is not supported currently implemented in any grabbers)
147  Automatically selects the first series search result (rather
148  than showing the user a list of more than one series).
149  Is overridden by interactive = False, or specifying a custom_ui
150 
151  debug (True/False):
152  shows verbose debugging information
153 
154  custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
155  A callable subclass of interactive class (overrides interactive option)
156 
157  language (2 character language abbreviation): (This option is not supported by all target site apis)
158  The language of the returned data. Is also the language search
159  uses. Default is "en" (English). For full list, run..
160 
161  search_all_languages (True/False): (This option is not supported by all target site apis)
162  By default, a Netvision grabber will only search in the language specified using
163  the language option. When this is True, it will search for the
164  show in any language
165 
166  """
167  self.config = {}
168 
169  if apikey is not None:
170  self.config['apikey'] = apikey
171  else:
172  pass # MNV search does not require an apikey
173 
174  self.config['debug_enabled'] = debug # show debugging messages
175  self.common = common
176  self.common.debug = debug # Set the common function debug level
177 
178  self.log_name = u'MNVsearch_Grabber'
179  self.common.logger = self.common.initLogger(path=sys.stderr, log_name=self.log_name)
180  self.logger = self.common.logger # Setups the logger (self.log.debug() etc)
182  self.config['custom_ui'] = custom_ui
184  self.config['interactive'] = interactive
186  self.config['select_first'] = select_first
187 
188  self.config['search_all_languages'] = search_all_languages
189 
190  self.error_messages = {'MNVSQLError': u"! Error: A SQL call cause the exception error (%s)\n", 'MNVVideoNotFound': u"! Error: Video search did not return any results (%s)\n", }
191  # Channel details and search results
192  self.channel = {'channel_title': u'Search all tree views', 'channel_link': u'http://www.mythtv.org/wiki/MythNetvision', 'channel_description': u"MythNetvision treeview data base search", 'channel_numresults': 0, 'channel_returned': 1, u'channel_startindex': 0}
193 
194  self.channel_icon = u'%SHAREDIR%/mythnetvision/icons/mnvsearch.png'
195  # end __init__()
196 
197 
198  def searchTitle(self, title, pagenumber, pagelen, feedtitle=False):
199  '''Key word video search of the MNV treeview tables
200  return an array of matching item elements
201  return
202  '''
203 
204  # Usually commented out - Easier for debugging
205 # resultList = self.getTreeviewData(title, pagenumber, pagelen)
206 # print resultList
207 # sys.exit(1)
208 
209  # Perform a search
210  try:
211  resultList = self.getTreeviewData(title, pagenumber, pagelen, feedtitle=feedtitle)
212  except Exception, errormsg:
213  raise MNVSQLError(self.error_messages['MNVSQLError'] % (errormsg))
214 
215  if self.config['debug_enabled']:
216  print "resultList: count(%s)" % len(resultList)
217  print resultList
218  print
219 
220  if not len(resultList):
221  raise MNVVideoNotFound(u"No treeview Video matches found for search value (%s)" % title)
222 
223  # Check to see if there are more items available to display
224  morePages = False
225  if len(resultList) > pagelen:
226  morePages = True
227  resultList.pop() # Remove the extra item as it was only used detect if there are more pages
228 
229  # Translate the data base search results into MNV RSS item format
230  itemDict = {}
231  itemThumbnail = etree.XPath('.//media:thumbnail', namespaces=self.common.namespaces)
232  itemContent = etree.XPath('.//media:content', namespaces=self.common.namespaces)
233  for result in resultList:
234  if not result['url']:
235  continue
236  mnvsearchItem = etree.XML(self.common.mnvItem)
237  # Insert data into a new item element
238  mnvsearchItem.find('link').text = result['url']
239  if result['title']:
240  mnvsearchItem.find('title').text = result['title']
241  if result['subtitle']:
242  etree.SubElement(mnvsearchItem, "subtitle").text = result['subtitle']
243  if result['description']:
244  mnvsearchItem.find('description').text = result['description']
245  if result['author']:
246  mnvsearchItem.find('author').text = result['author']
247  if result['date']:
248  mnvsearchItem.find('pubDate').text = result['date'].strftime(self.common.pubDateFormat)
249  if result['rating'] != '32576' and result['rating']:
250  mnvsearchItem.find('rating').text = result['rating']
251  if result['thumbnail']:
252  itemThumbnail(mnvsearchItem)[0].attrib['url'] = result['thumbnail']
253  if result['mediaURL']:
254  itemContent(mnvsearchItem)[0].attrib['url'] = result['mediaURL']
255  if result['filesize']:
256  itemContent(mnvsearchItem)[0].attrib['length'] = unicode(result['filesize'])
257  if result['time']:
258  itemContent(mnvsearchItem)[0].attrib['duration'] = unicode(result['time'])
259  if result['width']:
260  itemContent(mnvsearchItem)[0].attrib['width'] = unicode(result['width'])
261  if result['height']:
262  itemContent(mnvsearchItem)[0].attrib['height'] = unicode(result['height'])
263  if result['language']:
264  itemContent(mnvsearchItem)[0].attrib['lang'] = result['language']
265  if not result['season'] == 0 and not result['episode'] == 0:
266  if result['season']:
267  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = unicode(result['season'])
268  if result['episode']:
269  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = unicode(result['episode'])
270  if result['customhtml'] == 1:
271  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}customhtml").text = 'true'
272  if result['countries']:
273  countries = result['countries'].split(u' ')
274  for country in countries:
275  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = country
276  itemDict[result['title'].lower()] = mnvsearchItem
277 
278  if not len(itemDict.keys()):
279  raise MNVVideoNotFound(u"No MNV Video matches found for search value (%s)" % title)
280 
281  # Set the number of search results returned
282  if morePages:
283  self.channel['channel_numresults'] = pagelen
284  else:
285  self.channel['channel_numresults'] = len(itemDict)
286 
287  return [itemDict, morePages]
288  # end searchTitle()
289 
290 
291  def searchForVideos(self, title, pagenumber, feedtitle=False):
292  """Common name for a video search. Used to interface with MythTV plugin NetVision
293  """
294  # Usually commented out - Easier for debugging
295 # print self.searchTitle(title, pagenumber, self.page_limit)
296 # print
297 # sys.exit()
298 
299  try:
300  data = self.searchTitle(title, pagenumber, self.page_limit, feedtitle=feedtitle)
301  except MNVVideoNotFound, msg:
302  if feedtitle:
303  return [{}, '0', '0', '0']
304  sys.stderr.write(u"%s\n" % msg)
305  sys.exit(0)
306  except MNVSQLError, msg:
307  sys.stderr.write(u'%s\n' % msg)
308  sys.exit(1)
309  except Exception, e:
310  sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
311  sys.exit(1)
312 
313  if self.config['debug_enabled']:
314  print "data: count(%s)" % len(data[0])
315  print data
316  print
317 
318  # Create RSS element tree
319  rssTree = etree.XML(self.common.mnvRSS+u'</rss>')
320 
321  # Set the paging values
322  itemCount = len(data[0].keys())
323  if data[1] == True:
324  self.channel['channel_returned'] = itemCount
325  self.channel['channel_startindex'] = itemCount+(self.page_limit*(int(pagenumber)-1))
326  self.channel['channel_numresults'] = itemCount+(self.page_limit*(int(pagenumber)-1)+1)
327  else:
328  self.channel['channel_returned'] = itemCount+(self.page_limit*(int(pagenumber)-1))
329  self.channel['channel_startindex'] = self.channel['channel_returned']
330  self.channel['channel_numresults'] = self.channel['channel_returned']
331 
332  # If this was a Mashup search request then just return the elements dictionary a paging info
333  if feedtitle:
334  return [data[0], self.channel['channel_returned'], self.channel['channel_startindex'], self.channel['channel_numresults']]
335 
336  # Add the Channel element tree
337  channelTree = self.common.mnvChannelElement(self.channel)
338  rssTree.append(channelTree)
339 
340  lastKey = None
341  for key in sorted(data[0].keys()):
342  if lastKey != key:
343  channelTree.append(data[0][key])
344  lastKey = key
345 
346  # Output the MNV search results
347  sys.stdout.write(u'<?xml version="1.0" encoding="UTF-8"?>\n')
348  sys.stdout.write(etree.tostring(rssTree, encoding='UTF-8', pretty_print=True))
349  sys.exit(0)
350  # end searchForVideos()
351 
352  def getTreeviewData(self, searchTerms, pagenumber, pagelen, feedtitle=False):
353  ''' Use a SQL call to get any matching data base entries from the "netvisiontreeitems" and
354  "netvisionrssitems" tables. The search term can contain multiple search words separated
355  by a ";" character.
356  return a list of items found in the search or an empty dictionary if none were found
357  '''
358  if feedtitle:
359  sqlStatement = u"(SELECT title, description, subtitle, season, episode, url, type, thumbnail, mediaURL, author, date, rating, filesize, player, playerargs, download, downloadargs, time, width, height, language, customhtml, countries FROM `internetcontentarticles` WHERE `feedtitle` LIKE '%%%%FEEDTITLE%%%%' AND (%s)) ORDER BY title ASC LIMIT %s , %s"
360  else:
361  sqlStatement = u'(SELECT title, description, subtitle, season, episode, url, type, thumbnail, mediaURL, author, date, rating, filesize, player, playerargs, download, downloadargs, time, width, height, language, customhtml, countries FROM `internetcontentarticles` WHERE %s) ORDER BY title ASC LIMIT %s , %s'
362  searchTerm = u"`title` LIKE '%%SEARCHTERM%%' OR `description` LIKE '%%SEARCHTERM%%'"
363 
364  # Create the query variables search terms and the from/to paging values
365  searchList = searchTerms.split(u';')
366  if not len(searchList):
367  return {}
368 
369  dbSearchStatements = u''
370  for aSearch in searchList:
371  tmpTerms = searchTerm.replace(u'SEARCHTERM', aSearch)
372  if not len(dbSearchStatements):
373  dbSearchStatements+=tmpTerms
374  else:
375  dbSearchStatements+=u' OR ' + tmpTerms
376 
377  if pagenumber == 1:
378  fromResults = 0
379  pageLimit = pagelen+1
380  else:
381  fromResults = (int(pagenumber)-1)*int(pagelen)
382  pageLimit = pagelen+1
383 
384  if feedtitle:
385  sqlStatement = sqlStatement.replace(u'FEEDTITLE', feedtitle)
386 
387  query = sqlStatement % (dbSearchStatements, fromResults, pageLimit,)
388  if self.config['debug_enabled']:
389  print "FromRow(%s) pageLimit(%s)" % (fromResults, pageLimit)
390  print "query:"
391  sys.stdout.write(query)
392  print
393 
394  # Make the data base call and parse the returned data to extract the matching video item data
395  items = []
396  c = mythdb.cursor()
397  host = gethostname()
398  c.execute(query)
399  for title, description, subtitle, season, episode, url, media_type, thumbnail, mediaURL, author, date, rating, filesize, player, playerargs, download, downloadargs, time, width, height, language, customhtml, countries in c.fetchall():
400  items.append({'title': title, 'description': description, 'subtitle': subtitle, 'season': season, 'episode': episode, 'url': url, 'media_type': media_type, 'thumbnail': thumbnail, 'mediaURL': mediaURL, 'author': author, 'date': date, 'rating': rating, 'filesize': filesize, 'player': player, 'playerargs': playerargs, 'download': download, 'downloadargs': downloadargs, 'time': time, 'width': width, 'height': height, 'language': language, 'customhtml': customhtml, 'countries': countries})
401  c.close()
402 
403  return items
404  # end getTreeviewData()
405 # end Videos() class
def __init__(self, outstream, encoding=None)
def searchTitle(self, title, pagenumber, pagelen, feedtitle=False)
Definition: mythdb.h:14
def getTreeviewData(self, searchTerms, pagenumber, pagelen, feedtitle=False)
def searchForVideos(self, title, pagenumber, feedtitle=False)
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)