|
MythTV
0.26-pre
|
00001 #!/usr/bin/env python 00002 # -*- coding: UTF-8 -*- 00003 # ---------------------- 00004 # Name: mnvsearch_api - Simple-to-use Python interface to search the MythNetvision data base tables 00005 # 00006 # Python Script 00007 # Author: R.D. Vaughan 00008 # This python script is intended to perform a data base search of MythNetvision data base tables for 00009 # videos based on a command line search term. 00010 # 00011 # License:Creative Commons GNU GPL v2 00012 # (http://creativecommons.org/licenses/GPL/2.0/) 00013 #------------------------------------- 00014 __title__ ="mnvsearch_api - Simple-to-use Python interface to search the MythNetvision data base tables" 00015 __author__="R.D. Vaughan" 00016 __purpose__=''' 00017 This python script is intended to perform a data base search of MythNetvision data base tables for 00018 videos based on a command line search term. 00019 ''' 00020 00021 __version__="0.1.4" 00022 # 0.1.0 Initial development 00023 # 0.1.1 Changed the logger to only output to stderr rather than a file 00024 # 0.1.2 Changed the SQL query to the new "internetcontentarticles" table format and new fields 00025 # Added "%SHAREDIR%" to icon directory path 00026 # 0.1.3 Video duration value was being erroneously multiplied by 60. 00027 # 0.1.4 Add the ability to search within a specific "feedtitle". Used mainly for searching large mashups 00028 # Fixed a paging bug 00029 00030 import os, struct, sys, re, time, datetime, shutil, urllib 00031 import logging 00032 from socket import gethostname, gethostbyname 00033 from threading import Thread 00034 from copy import deepcopy 00035 from operator import itemgetter, attrgetter 00036 00037 from mnvsearch_exceptions import (MNVSQLError, MNVVideoNotFound, ) 00038 00039 class OutStreamEncoder(object): 00040 """Wraps a stream with an encoder""" 00041 def __init__(self, outstream, encoding=None): 00042 self.out = outstream 00043 if not encoding: 00044 self.encoding = sys.getfilesystemencoding() 00045 else: 00046 self.encoding = encoding 00047 00048 def write(self, obj): 00049 """Wraps the output stream, encoding Unicode strings with the specified encoding""" 00050 if isinstance(obj, unicode): 00051 try: 00052 self.out.write(obj.encode(self.encoding)) 00053 except IOError: 00054 pass 00055 else: 00056 try: 00057 self.out.write(obj) 00058 except IOError: 00059 pass 00060 00061 def __getattr__(self, attr): 00062 """Delegate everything but write to the stream""" 00063 return getattr(self.out, attr) 00064 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8') 00065 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8') 00066 00067 00068 # Find out if the MythTV python bindings can be accessed and instances can created 00069 try: 00070 '''If the MythTV python interface is found, required to access Netvision icon directory settings 00071 ''' 00072 from MythTV import MythDB, MythLog 00073 try: 00074 '''Create an instance of each: MythDB 00075 ''' 00076 MythLog._setlevel('none') # Some non option -M cannot have any logging on stdout 00077 mythdb = MythDB() 00078 except MythError, e: 00079 sys.stderr.write(u'\n! Error - %s\n' % e.args[0]) 00080 filename = os.path.expanduser("~")+'/.mythtv/config.xml' 00081 if not os.path.isfile(filename): 00082 sys.stderr.write(u'\n! Error - A correctly configured (%s) file must exist\n' % filename) 00083 else: 00084 sys.stderr.write(u'\n! Error - Check that (%s) is correctly configured\n' % filename) 00085 sys.exit(1) 00086 except Exception, e: 00087 sys.stderr.write(u"\n! Error - Creating an instance caused an error for one of: MythDB. error(%s)\n" % e) 00088 sys.exit(1) 00089 except Exception, e: 00090 sys.stderr.write(u"\n! Error - MythTV python bindings could not be imported. error(%s)\n" % e) 00091 sys.exit(1) 00092 00093 00094 try: 00095 from StringIO import StringIO 00096 from lxml import etree 00097 except Exception, e: 00098 sys.stderr.write(u'\n! Error - Importing the "lxml" and "StringIO" python libraries failed on error(%s)\n' % e) 00099 sys.exit(1) 00100 00101 # Check that the lxml library is current enough 00102 # From the lxml documents it states: (http://codespeak.net/lxml/installation.html) 00103 # "If you want to use XPath, do not use libxml2 2.6.27. We recommend libxml2 2.7.2 or later" 00104 # Testing was performed with the Ubuntu 9.10 "python-lxml" version "2.1.5-1ubuntu2" repository package 00105 version = '' 00106 for digit in etree.LIBXML_VERSION: 00107 version+=str(digit)+'.' 00108 version = version[:-1] 00109 if version < '2.7.2': 00110 sys.stderr.write(u''' 00111 ! Error - The installed version of the "lxml" python library "libxml" version is too old. 00112 At least "libxml" version 2.7.2 must be installed. Your version is (%s). 00113 ''' % version) 00114 sys.exit(1) 00115 00116 00117 class Videos(object): 00118 """Main interface to the MNV treeview table search 00119 This is done to support a common naming framework for all python Netvision plugins no matter their site 00120 target. 00121 00122 Supports search methods 00123 The apikey is a not required for this grabber 00124 """ 00125 def __init__(self, 00126 apikey, 00127 mythtv = True, 00128 interactive = False, 00129 select_first = False, 00130 debug = False, 00131 custom_ui = None, 00132 language = None, 00133 search_all_languages = False, 00134 ): 00135 """apikey (str/unicode): 00136 Specify the target site API key. Applications need their own key in some cases 00137 00138 mythtv (True/False): 00139 When True, the returned meta data is being returned has the key and values massaged to match MythTV 00140 When False, the returned meta data is being returned matches what target site returned 00141 00142 interactive (True/False): (This option is not supported by all target site apis) 00143 When True, uses built-in console UI is used to select the correct show. 00144 When False, the first search result is used. 00145 00146 select_first (True/False): (This option is not supported currently implemented in any grabbers) 00147 Automatically selects the first series search result (rather 00148 than showing the user a list of more than one series). 00149 Is overridden by interactive = False, or specifying a custom_ui 00150 00151 debug (True/False): 00152 shows verbose debugging information 00153 00154 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers) 00155 A callable subclass of interactive class (overrides interactive option) 00156 00157 language (2 character language abbreviation): (This option is not supported by all target site apis) 00158 The language of the returned data. Is also the language search 00159 uses. Default is "en" (English). For full list, run.. 00160 00161 search_all_languages (True/False): (This option is not supported by all target site apis) 00162 By default, a Netvision grabber will only search in the language specified using 00163 the language option. When this is True, it will search for the 00164 show in any language 00165 00166 """ 00167 self.config = {} 00168 00169 if apikey is not None: 00170 self.config['apikey'] = apikey 00171 else: 00172 pass # MNV search does not require an apikey 00173 00174 self.config['debug_enabled'] = debug # show debugging messages 00175 self.common = common 00176 self.common.debug = debug # Set the common function debug level 00177 00178 self.log_name = u'MNVsearch_Grabber' 00179 self.common.logger = self.common.initLogger(path=sys.stderr, log_name=self.log_name) 00180 self.logger = self.common.logger # Setups the logger (self.log.debug() etc) 00181 00182 self.config['custom_ui'] = custom_ui 00183 00184 self.config['interactive'] = interactive 00185 00186 self.config['select_first'] = select_first 00187 00188 self.config['search_all_languages'] = search_all_languages 00189 00190 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", } 00191 # Channel details and search results 00192 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} 00193 00194 self.channel_icon = u'%SHAREDIR%/mythnetvision/icons/mnvsearch.png' 00195 # end __init__() 00196 00197 00198 def searchTitle(self, title, pagenumber, pagelen, feedtitle=False): 00199 '''Key word video search of the MNV treeview tables 00200 return an array of matching item elements 00201 return 00202 ''' 00203 00204 # Usually commented out - Easier for debugging 00205 # resultList = self.getTreeviewData(title, pagenumber, pagelen) 00206 # print resultList 00207 # sys.exit(1) 00208 00209 # Perform a search 00210 try: 00211 resultList = self.getTreeviewData(title, pagenumber, pagelen, feedtitle=feedtitle) 00212 except Exception, errormsg: 00213 raise MNVSQLError(self.error_messages['MNVSQLError'] % (errormsg)) 00214 00215 if self.config['debug_enabled']: 00216 print "resultList: count(%s)" % len(resultList) 00217 print resultList 00218 print 00219 00220 if not len(resultList): 00221 raise MNVVideoNotFound(u"No treeview Video matches found for search value (%s)" % title) 00222 00223 # Check to see if there are more items available to display 00224 morePages = False 00225 if len(resultList) > pagelen: 00226 morePages = True 00227 resultList.pop() # Remove the extra item as it was only used detect if there are more pages 00228 00229 # Translate the data base search results into MNV RSS item format 00230 itemDict = {} 00231 itemThumbnail = etree.XPath('.//media:thumbnail', namespaces=self.common.namespaces) 00232 itemContent = etree.XPath('.//media:content', namespaces=self.common.namespaces) 00233 for result in resultList: 00234 if not result['url']: 00235 continue 00236 mnvsearchItem = etree.XML(self.common.mnvItem) 00237 # Insert data into a new item element 00238 mnvsearchItem.find('link').text = result['url'] 00239 if result['title']: 00240 mnvsearchItem.find('title').text = result['title'] 00241 if result['subtitle']: 00242 etree.SubElement(mnvsearchItem, "subtitle").text = result['subtitle'] 00243 if result['description']: 00244 mnvsearchItem.find('description').text = result['description'] 00245 if result['author']: 00246 mnvsearchItem.find('author').text = result['author'] 00247 if result['date']: 00248 mnvsearchItem.find('pubDate').text = result['date'].strftime(self.common.pubDateFormat) 00249 if result['rating'] != '32576' and result['rating']: 00250 mnvsearchItem.find('rating').text = result['rating'] 00251 if result['thumbnail']: 00252 itemThumbnail(mnvsearchItem)[0].attrib['url'] = result['thumbnail'] 00253 if result['mediaURL']: 00254 itemContent(mnvsearchItem)[0].attrib['url'] = result['mediaURL'] 00255 if result['filesize']: 00256 itemContent(mnvsearchItem)[0].attrib['length'] = unicode(result['filesize']) 00257 if result['time']: 00258 itemContent(mnvsearchItem)[0].attrib['duration'] = unicode(result['time']) 00259 if result['width']: 00260 itemContent(mnvsearchItem)[0].attrib['width'] = unicode(result['width']) 00261 if result['height']: 00262 itemContent(mnvsearchItem)[0].attrib['height'] = unicode(result['height']) 00263 if result['language']: 00264 itemContent(mnvsearchItem)[0].attrib['lang'] = result['language'] 00265 if not result['season'] == 0 and not result['episode'] == 0: 00266 if result['season']: 00267 etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}season").text = unicode(result['season']) 00268 if result['episode']: 00269 etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}episode").text = unicode(result['episode']) 00270 if result['customhtml'] == 1: 00271 etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}customhtml").text = 'true' 00272 if result['countries']: 00273 countries = result['countries'].split(u' ') 00274 for country in countries: 00275 etree.SubElement(mnvsearchItem, "{http://www.mythtv.org/wiki/MythNetvision_Grabber_Script_Format}country").text = country 00276 itemDict[result['title'].lower()] = mnvsearchItem 00277 00278 if not len(itemDict.keys()): 00279 raise MNVVideoNotFound(u"No MNV Video matches found for search value (%s)" % title) 00280 00281 # Set the number of search results returned 00282 if morePages: 00283 self.channel['channel_numresults'] = pagelen 00284 else: 00285 self.channel['channel_numresults'] = len(itemDict) 00286 00287 return [itemDict, morePages] 00288 # end searchTitle() 00289 00290 00291 def searchForVideos(self, title, pagenumber, feedtitle=False): 00292 """Common name for a video search. Used to interface with MythTV plugin NetVision 00293 """ 00294 # Usually commented out - Easier for debugging 00295 # print self.searchTitle(title, pagenumber, self.page_limit) 00296 # print 00297 # sys.exit() 00298 00299 try: 00300 data = self.searchTitle(title, pagenumber, self.page_limit, feedtitle=feedtitle) 00301 except MNVVideoNotFound, msg: 00302 if feedtitle: 00303 return [{}, '0', '0', '0'] 00304 sys.stderr.write(u"%s\n" % msg) 00305 sys.exit(0) 00306 except MNVSQLError, msg: 00307 sys.stderr.write(u'%s\n' % msg) 00308 sys.exit(1) 00309 except Exception, e: 00310 sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e)) 00311 sys.exit(1) 00312 00313 if self.config['debug_enabled']: 00314 print "data: count(%s)" % len(data[0]) 00315 print data 00316 print 00317 00318 # Create RSS element tree 00319 rssTree = etree.XML(self.common.mnvRSS+u'</rss>') 00320 00321 # Set the paging values 00322 itemCount = len(data[0].keys()) 00323 if data[1] == True: 00324 self.channel['channel_returned'] = itemCount 00325 self.channel['channel_startindex'] = itemCount+(self.page_limit*(int(pagenumber)-1)) 00326 self.channel['channel_numresults'] = itemCount+(self.page_limit*(int(pagenumber)-1)+1) 00327 else: 00328 self.channel['channel_returned'] = itemCount+(self.page_limit*(int(pagenumber)-1)) 00329 self.channel['channel_startindex'] = self.channel['channel_returned'] 00330 self.channel['channel_numresults'] = self.channel['channel_returned'] 00331 00332 # If this was a Mashup search request then just return the elements dictionary a paging info 00333 if feedtitle: 00334 return [data[0], self.channel['channel_returned'], self.channel['channel_startindex'], self.channel['channel_numresults']] 00335 00336 # Add the Channel element tree 00337 channelTree = self.common.mnvChannelElement(self.channel) 00338 rssTree.append(channelTree) 00339 00340 lastKey = None 00341 for key in sorted(data[0].keys()): 00342 if lastKey != key: 00343 channelTree.append(data[0][key]) 00344 lastKey = key 00345 00346 # Output the MNV search results 00347 sys.stdout.write(u'<?xml version="1.0" encoding="UTF-8"?>\n') 00348 sys.stdout.write(etree.tostring(rssTree, encoding='UTF-8', pretty_print=True)) 00349 sys.exit(0) 00350 # end searchForVideos() 00351 00352 def getTreeviewData(self, searchTerms, pagenumber, pagelen, feedtitle=False): 00353 ''' Use a SQL call to get any matching data base entries from the "netvisiontreeitems" and 00354 "netvisionrssitems" tables. The search term can contain multiple search words separated 00355 by a ";" character. 00356 return a list of items found in the search or an empty dictionary if none were found 00357 ''' 00358 if feedtitle: 00359 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" 00360 else: 00361 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' 00362 searchTerm = u"`title` LIKE '%%SEARCHTERM%%' OR `description` LIKE '%%SEARCHTERM%%'" 00363 00364 # Create the query variables search terms and the from/to paging values 00365 searchList = searchTerms.split(u';') 00366 if not len(searchList): 00367 return {} 00368 00369 dbSearchStatements = u'' 00370 for aSearch in searchList: 00371 tmpTerms = searchTerm.replace(u'SEARCHTERM', aSearch) 00372 if not len(dbSearchStatements): 00373 dbSearchStatements+=tmpTerms 00374 else: 00375 dbSearchStatements+=u' OR ' + tmpTerms 00376 00377 if pagenumber == 1: 00378 fromResults = 0 00379 pageLimit = pagelen+1 00380 else: 00381 fromResults = (int(pagenumber)-1)*int(pagelen) 00382 pageLimit = pagelen+1 00383 00384 if feedtitle: 00385 sqlStatement = sqlStatement.replace(u'FEEDTITLE', feedtitle) 00386 00387 query = sqlStatement % (dbSearchStatements, fromResults, pageLimit,) 00388 if self.config['debug_enabled']: 00389 print "FromRow(%s) pageLimit(%s)" % (fromResults, pageLimit) 00390 print "query:" 00391 sys.stdout.write(query) 00392 print 00393 00394 # Make the data base call and parse the returned data to extract the matching video item data 00395 items = [] 00396 c = mythdb.cursor() 00397 host = gethostname() 00398 c.execute(query) 00399 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(): 00400 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}) 00401 c.close() 00402 00403 return items 00404 # end getTreeviewData() 00405 # end Videos() class
1.7.6.1