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 
98 class Videos(object):
99  """Main interface to the MNV treeview table search
100  This is done to support a common naming framework for all python Netvision plugins no matter their site
101  target.
102 
103  Supports search methods
104  The apikey is a not required for this grabber
105  """
106  def __init__(self,
107  apikey,
108  mythtv = True,
109  interactive = False,
110  select_first = False,
111  debug = False,
112  custom_ui = None,
113  language = None,
114  search_all_languages = False,
115  ):
116  """apikey (str/unicode):
117  Specify the target site API key. Applications need their own key in some cases
118 
119  mythtv (True/False):
120  When True, the returned meta data is being returned has the key and values massaged to match MythTV
121  When False, the returned meta data is being returned matches what target site returned
122 
123  interactive (True/False): (This option is not supported by all target site apis)
124  When True, uses built-in console UI is used to select the correct show.
125  When False, the first search result is used.
126 
127  select_first (True/False): (This option is not supported currently implemented in any grabbers)
128  Automatically selects the first series search result (rather
129  than showing the user a list of more than one series).
130  Is overridden by interactive = False, or specifying a custom_ui
131 
132  debug (True/False):
133  shows verbose debugging information
134 
135  custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
136  A callable subclass of interactive class (overrides interactive option)
137 
138  language (2 character language abbreviation): (This option is not supported by all target site apis)
139  The language of the returned data. Is also the language search
140  uses. Default is "en" (English). For full list, run..
141 
142  search_all_languages (True/False): (This option is not supported by all target site apis)
143  By default, a Netvision grabber will only search in the language specified using
144  the language option. When this is True, it will search for the
145  show in any language
146 
147  """
148  self.config = {}
149 
150  if apikey is not None:
151  self.config['apikey'] = apikey
152  else:
153  pass # MNV search does not require an apikey
154 
155  self.config['debug_enabled'] = debug # show debugging messages
156  self.common = common
157  self.common.debug = debug # Set the common function debug level
158 
159  self.log_name = 'MNVsearch_Grabber'
160  self.common.logger = self.common.initLogger(path=sys.stderr, log_name=self.log_name)
161  self.logger = self.common.logger # Setups the logger (self.log.debug() etc)
162 
163  self.config['custom_ui'] = custom_ui
164 
165  self.config['interactive'] = interactive
166 
167  self.config['select_first'] = select_first
168 
169  self.config['search_all_languages'] = search_all_languages
170 
171  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", }
172  # Channel details and search results
173  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}
174 
175  self.channel_icon = '%SHAREDIR%/mythnetvision/icons/mnvsearch.png'
176  # end __init__()
177 
178 
179  def searchTitle(self, title, pagenumber, pagelen, feedtitle=False):
180  '''Key word video search of the MNV treeview tables
181  return an array of matching item elements
182  return
183  '''
184 
185  # Usually commented out - Easier for debugging
186 # resultList = self.getTreeviewData(title, pagenumber, pagelen)
187 # print resultList
188 # sys.exit(1)
189 
190  # Perform a search
191  try:
192  resultList = self.getTreeviewData(title, pagenumber, pagelen, feedtitle=feedtitle)
193  except Exception as errormsg:
194  raise MNVSQLError(self.error_messages['MNVSQLError'] % (errormsg))
195 
196  if self.config['debug_enabled']:
197  print("resultList: count(%s)" % len(resultList))
198  print(resultList)
199  print()
200 
201  if not len(resultList):
202  raise MNVVideoNotFound("No treeview Video matches found for search value (%s)" % title)
203 
204  # Check to see if there are more items available to display
205  morePages = False
206  if len(resultList) > pagelen:
207  morePages = True
208  resultList.pop() # Remove the extra item as it was only used detect if there are more pages
209 
210  # Translate the data base search results into MNV RSS item format
211  itemDict = {}
212  itemThumbnail = etree.XPath('.//media:thumbnail', namespaces=self.common.namespaces)
213  itemContent = etree.XPath('.//media:content', namespaces=self.common.namespaces)
214  for result in resultList:
215  if not result['url']:
216  continue
217  mnvsearchItem = etree.XML(self.common.mnvItem)
218  # Insert data into a new item element
219  mnvsearchItem.find('link').text = result['url']
220  if result['title']:
221  mnvsearchItem.find('title').text = result['title']
222  if result['subtitle']:
223  etree.SubElement(mnvsearchItem, "subtitle").text = result['subtitle']
224  if result['description']:
225  mnvsearchItem.find('description').text = result['description']
226  if result['author']:
227  mnvsearchItem.find('author').text = result['author']
228  if result['date']:
229  mnvsearchItem.find('pubDate').text = result['date'].strftime(self.common.pubDateFormat)
230  if result['rating'] != '32576' and result['rating']:
231  mnvsearchItem.find('rating').text = result['rating']
232  if result['thumbnail']:
233  itemThumbnail(mnvsearchItem)[0].attrib['url'] = result['thumbnail']
234  if result['mediaURL']:
235  itemContent(mnvsearchItem)[0].attrib['url'] = result['mediaURL']
236  if result['filesize']:
237  itemContent(mnvsearchItem)[0].attrib['length'] = str(result['filesize'])
238  if result['time']:
239  itemContent(mnvsearchItem)[0].attrib['duration'] = str(result['time'])
240  if result['width']:
241  itemContent(mnvsearchItem)[0].attrib['width'] = str(result['width'])
242  if result['height']:
243  itemContent(mnvsearchItem)[0].attrib['height'] = str(result['height'])
244  if result['language']:
245  itemContent(mnvsearchItem)[0].attrib['lang'] = result['language']
246  if not result['season'] == 0 and not result['episode'] == 0:
247  if result['season']:
248  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = str(result['season'])
249  if result['episode']:
250  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = str(result['episode'])
251  if result['customhtml'] == 1:
252  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}customhtml").text = 'true'
253  if result['countries']:
254  countries = result['countries'].split(' ')
255  for country in countries:
256  etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = country
257  itemDict[result['title'].lower()] = mnvsearchItem
258 
259  if not len(list(itemDict.keys())):
260  raise MNVVideoNotFound("No MNV Video matches found for search value (%s)" % title)
261 
262  # Set the number of search results returned
263  if morePages:
264  self.channel['channel_numresults'] = pagelen
265  else:
266  self.channel['channel_numresults'] = len(itemDict)
267 
268  return [itemDict, morePages]
269  # end searchTitle()
270 
271 
272  def searchForVideos(self, title, pagenumber, feedtitle=False):
273  """Common name for a video search. Used to interface with MythTV plugin NetVision
274  """
275  # Usually commented out - Easier for debugging
276 # print self.searchTitle(title, pagenumber, self.page_limit)
277 # print
278 # sys.exit()
279 
280  try:
281  data = self.searchTitle(title, pagenumber, self.page_limit, feedtitle=feedtitle)
282  except MNVVideoNotFound as msg:
283  if feedtitle:
284  return [{}, '0', '0', '0']
285  sys.stderr.write("%s\n" % msg)
286  sys.exit(0)
287  except MNVSQLError as msg:
288  sys.stderr.write('%s\n' % msg)
289  sys.exit(1)
290  except Exception as e:
291  sys.stderr.write("! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
292  sys.exit(1)
293 
294  if self.config['debug_enabled']:
295  print("data: count(%s)" % len(data[0]))
296  print(data)
297  print()
298 
299  # Create RSS element tree
300  rssTree = etree.XML(self.common.mnvRSS+'</rss>')
301 
302  # Set the paging values
303  itemCount = len(list(data[0].keys()))
304  if data[1] == True:
305  self.channel['channel_returned'] = itemCount
306  self.channel['channel_startindex'] = itemCount+(self.page_limit*(int(pagenumber)-1))
307  self.channel['channel_numresults'] = itemCount+(self.page_limit*(int(pagenumber)-1)+1)
308  else:
309  self.channel['channel_returned'] = itemCount+(self.page_limit*(int(pagenumber)-1))
310  self.channel['channel_startindex'] = self.channel['channel_returned']
311  self.channel['channel_numresults'] = self.channel['channel_returned']
312 
313  # If this was a Mashup search request then just return the elements dictionary a paging info
314  if feedtitle:
315  return [data[0], self.channel['channel_returned'], self.channel['channel_startindex'], self.channel['channel_numresults']]
316 
317  # Add the Channel element tree
318  channelTree = self.common.mnvChannelElement(self.channel)
319  rssTree.append(channelTree)
320 
321  lastKey = None
322  for key in sorted(data[0].keys()):
323  if lastKey != key:
324  channelTree.append(data[0][key])
325  lastKey = key
326 
327  # Output the MNV search results
328  sys.stdout.write('<?xml version="1.0" encoding="UTF-8"?>\n')
329  sys.stdout.write(etree.tostring(rssTree, encoding='UTF-8', pretty_print=True))
330  sys.exit(0)
331  # end searchForVideos()
332 
333  def getTreeviewData(self, searchTerms, pagenumber, pagelen, feedtitle=False):
334  ''' Use a SQL call to get any matching data base entries from the "netvisiontreeitems" and
335  "netvisionrssitems" tables. The search term can contain multiple search words separated
336  by a ";" character.
337  return a list of items found in the search or an empty dictionary if none were found
338  '''
339  if feedtitle:
340  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"
341  else:
342  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'
343  searchTerm = "`title` LIKE '%%SEARCHTERM%%' OR `description` LIKE '%%SEARCHTERM%%'"
344 
345  # Create the query variables search terms and the from/to paging values
346  searchList = searchTerms.split(';')
347  if not len(searchList):
348  return {}
349 
350  dbSearchStatements = ''
351  for aSearch in searchList:
352  tmpTerms = searchTerm.replace('SEARCHTERM', aSearch)
353  if not len(dbSearchStatements):
354  dbSearchStatements+=tmpTerms
355  else:
356  dbSearchStatements+=' OR ' + tmpTerms
357 
358  if pagenumber == 1:
359  fromResults = 0
360  pageLimit = pagelen+1
361  else:
362  fromResults = (int(pagenumber)-1)*int(pagelen)
363  pageLimit = pagelen+1
364 
365  if feedtitle:
366  sqlStatement = sqlStatement.replace('FEEDTITLE', feedtitle)
367 
368  query = sqlStatement % (dbSearchStatements, fromResults, pageLimit,)
369  if self.config['debug_enabled']:
370  print("FromRow(%s) pageLimit(%s)" % (fromResults, pageLimit))
371  print("query:")
372  sys.stdout.write(query)
373  print()
374 
375  # Make the data base call and parse the returned data to extract the matching video item data
376  items = []
377  c = mythdb.cursor()
378  host = gethostname()
379  c.execute(query)
380  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():
381  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})
382  c.close()
383 
384  return items
385  # end getTreeviewData()
386 # end Videos() class
nv_python_libs.mnvsearch.mnvsearch_api.Videos.getTreeviewData
def getTreeviewData(self, searchTerms, pagenumber, pagelen, feedtitle=False)
Definition: mnvsearch_api.py:333
nv_python_libs.mnvsearch.mnvsearch_api.Videos.channel
channel
Definition: mnvsearch_api.py:164
nv_python_libs.mnvsearch.mnvsearch_exceptions.MNVSQLError
Definition: mnvsearch_exceptions.py:22
nv_python_libs.mnvsearch.mnvsearch_api.Videos
Definition: mnvsearch_api.py:98
nv_python_libs.mnvsearch.mnvsearch_api.Videos.common
common
Definition: mnvsearch_api.py:147
nv_python_libs.mnvsearch.mnvsearch_api.Videos.error_messages
error_messages
Definition: mnvsearch_api.py:162
nv_python_libs.mnvsearch.mnvsearch_api.Videos.config
config
Definition: mnvsearch_api.py:139
nv_python_libs.mnvsearch.mnvsearch_api.Videos.channel_icon
channel_icon
Definition: mnvsearch_api.py:166
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:106
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:272
nv_python_libs.mnvsearch.mnvsearch_api.Videos.log_name
log_name
Definition: mnvsearch_api.py:150
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:179
nv_python_libs.mnvsearch.mnvsearch_api.Videos.logger
logger
Definition: mnvsearch_api.py:152