14 __title__ =
"mnvsearch_api - Simple-to-use Python interface to search the MythNetvision data base tables"
15 __author__=
"R.D. Vaughan"
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.
30 import os, struct, sys, re, time, datetime, shutil, urllib.request, urllib.parse, urllib.error
32 from socket
import gethostname, gethostbyname
33 from threading
import Thread
34 from copy
import deepcopy
35 from operator
import itemgetter, attrgetter
37 from .mnvsearch_exceptions
import (MNVSQLError, MNVVideoNotFound, )
41 """Wraps a stream with an encoder"""
50 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
51 if isinstance(obj, str):
53 self.
out.buffer.write(obj)
56 """Delegate everything but write to the stream"""
57 return getattr(self.
out, attr)
59 if isinstance(sys.stdout, io.TextIOWrapper):
66 '''If the MythTV python interface is found, required to access Netvision icon directory settings
68 from MythTV
import MythDB, MythLog
70 '''Create an instance of each: MythDB
72 MythLog._setlevel(
'none')
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)
80 sys.stderr.write(
'\n! Error - Check that (%s) is correctly configured\n' % filename)
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)
85 except Exception
as e:
86 sys.stderr.write(
"\n! Error - MythTV python bindings could not be imported. error(%s)\n" % e)
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)
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
103 Supports search methods
104 The apikey is a not required for this grabber
110 select_first = False,
114 search_all_languages = False,
116 """apikey (str/unicode):
117 Specify the target site API key. Applications need their own key in some cases
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
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.
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
133 shows verbose debugging information
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)
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..
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
150 if apikey
is not None:
151 self.
config[
'apikey'] = apikey
155 self.
config[
'debug_enabled'] = debug
163 self.
config[
'custom_ui'] = custom_ui
165 self.
config[
'interactive'] = interactive
167 self.
config[
'select_first'] = select_first
169 self.
config[
'search_all_languages'] = search_all_languages
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", }
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}
175 self.
channel_icon =
'%SHAREDIR%/mythnetvision/icons/mnvsearch.png'
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
192 resultList = self.
getTreeviewData(title, pagenumber, pagelen, feedtitle=feedtitle)
193 except Exception
as errormsg:
196 if self.
config[
'debug_enabled']:
197 print(
"resultList: count(%s)" % len(resultList))
201 if not len(resultList):
202 raise MNVVideoNotFound(
"No treeview Video matches found for search value (%s)" % title)
206 if len(resultList) > pagelen:
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']:
217 mnvsearchItem = etree.XML(self.
common.mnvItem)
219 mnvsearchItem.find(
'link').text = result[
'url']
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']
227 mnvsearchItem.find(
'author').text = result[
'author']
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'])
239 itemContent(mnvsearchItem)[0].attrib[
'duration'] = str(result[
'time'])
241 itemContent(mnvsearchItem)[0].attrib[
'width'] = str(result[
'width'])
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:
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
259 if not len(list(itemDict.keys())):
260 raise MNVVideoNotFound(
"No MNV Video matches found for search value (%s)" % title)
264 self.
channel[
'channel_numresults'] = pagelen
266 self.
channel[
'channel_numresults'] = len(itemDict)
268 return [itemDict, morePages]
273 """Common name for a video search. Used to interface with MythTV plugin NetVision
281 data = self.
searchTitle(title, pagenumber, self.page_limit, feedtitle=feedtitle)
282 except MNVVideoNotFound
as msg:
284 return [{},
'0',
'0',
'0']
285 sys.stderr.write(
"%s\n" % msg)
287 except MNVSQLError
as msg:
288 sys.stderr.write(
'%s\n' % msg)
290 except Exception
as e:
291 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
294 if self.
config[
'debug_enabled']:
295 print(
"data: count(%s)" % len(data[0]))
300 rssTree = etree.XML(self.
common.mnvRSS+
'</rss>')
303 itemCount = len(list(data[0].keys()))
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)
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']
315 return [data[0], self.
channel[
'channel_returned'], self.
channel[
'channel_startindex'], self.
channel[
'channel_numresults']]
319 rssTree.append(channelTree)
322 for key
in sorted(data[0].keys()):
324 channelTree.append(data[0][key])
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))
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
337 return a list of items found in the search or an empty dictionary if none were found
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"
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%%'"
346 searchList = searchTerms.split(
';')
347 if not len(searchList):
350 dbSearchStatements =
''
351 for aSearch
in searchList:
352 tmpTerms = searchTerm.replace(
'SEARCHTERM', aSearch)
353 if not len(dbSearchStatements):
354 dbSearchStatements+=tmpTerms
356 dbSearchStatements+=
' OR ' + tmpTerms
360 pageLimit = pagelen+1
362 fromResults = (int(pagenumber)-1)*int(pagelen)
363 pageLimit = pagelen+1
366 sqlStatement = sqlStatement.replace(
'FEEDTITLE', feedtitle)
368 query = sqlStatement % (dbSearchStatements, fromResults, pageLimit,)
369 if self.
config[
'debug_enabled']:
370 print(
"FromRow(%s) pageLimit(%s)" % (fromResults, pageLimit))
372 sys.stdout.write(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})