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__='''
17This python script is intended to perform a data base search of MythNetvision data base tables for
18videos 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
30import os, struct, sys, re, time, datetime, shutil, urllib.request, urllib.parse, urllib.error
31import logging
32from socket import gethostname, gethostbyname
33from threading import Thread
34from copy import deepcopy
35from operator import itemgetter, attrgetter
36
37from .mnvsearch_exceptions import (MNVSQLError, MNVVideoNotFound, )
38import io
39
40class 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
59if 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
65try:
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)
85except 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
90try:
91 from io import StringIO
92 from lxml import etree
93except 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
98class 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
def __init__(self, outstream, encoding=None)
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
def searchForVideos(self, title, pagenumber, feedtitle=False)
def getTreeviewData(self, searchTerms, pagenumber, pagelen, feedtitle=False)
def searchTitle(self, title, pagenumber, pagelen, feedtitle=False)
static void print(const QList< uint > &raw_minimas, const QList< uint > &raw_maximas, const QList< float > &minimas, const QList< float > &maximas)