MythTV  0.26-pre
mnvsearch_api.py
Go to the documentation of this file.
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
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends