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