MythTV  0.26-pre
youtube_api.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 # -*- coding: UTF-8 -*-
00003 # ----------------------
00004 # Name: youtube_api - Simple-to-use Python interface to the youtube API (http://www.youtube.com/)
00005 # Python Script
00006 # Author:   R.D. Vaughan
00007 # Purpose:  This python script is intended to perform a variety of utility functions to search and access text
00008 #           metadata, video and image URLs from youtube. These routines are based on the api. Specifications
00009 #           for this api are published at http://developer.youtubenservices.com/docs
00010 #
00011 # License:Creative Commons GNU GPL v2
00012 # (http://creativecommons.org/licenses/GPL/2.0/)
00013 #-------------------------------------
00014 __title__ ="youtube_api - Simple-to-use Python interface to the youtube API (http://developer.youtubenservices.com/docs)"
00015 __author__="R.D. Vaughan"
00016 __purpose__='''
00017 This python script is intended to perform a variety of utility functions to search and access text
00018 meta data, video and image URLs from youtube. These routines are based on the api. Specifications
00019 for this api are published at http://developer.youtubenservices.com/docs
00020 '''
00021 
00022 __version__="v0.2.5"
00023 # 0.1.0 Initial development
00024 # 0.1.1 Added Tree view display option
00025 # 0.1.2 Modified Tree view internals to be consistent in approach and structure.
00026 # 0.1.3 Added images for directories
00027 # 0.1.4 Documentation review
00028 # 0.2.0 Public release
00029 # 0.2.1 New python bindings conversion
00030 #       Better exception error reporting
00031 #       Better handling of invalid unicode data from source
00032 # 0.2.2 Completed exception error reporting improvements
00033 #       Removed the use of the feedparser library
00034 # 0.2.3 Fixed an exception message output code error in two places
00035 # 0.2.4 Removed the need for python MythTV bindings and added "%SHAREDIR%" to icon directory path
00036 # 0.2.5 Fixed the Foreign Film icon file name
00037 
00038 import os, struct, sys, re, time
00039 import urllib, urllib2
00040 import logging
00041 from MythTV import MythXML
00042 
00043 try:
00044     import xml.etree.cElementTree as ElementTree
00045 except ImportError:
00046     import xml.etree.ElementTree as ElementTree
00047 
00048 from youtube_exceptions import (YouTubeUrlError, YouTubeHttpError, YouTubeRssError, YouTubeVideoNotFound, YouTubeInvalidSearchType, YouTubeXmlError, YouTubeVideoDetailError, YouTubeCategoryNotFound)
00049 
00050 class OutStreamEncoder(object):
00051     """Wraps a stream with an encoder"""
00052     def __init__(self, outstream, encoding=None):
00053         self.out = outstream
00054         if not encoding:
00055             self.encoding = sys.getfilesystemencoding()
00056         else:
00057             self.encoding = encoding
00058 
00059     def write(self, obj):
00060         """Wraps the output stream, encoding Unicode strings with the specified encoding"""
00061         if isinstance(obj, unicode):
00062             try:
00063                 self.out.write(obj.encode(self.encoding))
00064             except IOError:
00065                 pass
00066         else:
00067             try:
00068                 self.out.write(obj)
00069             except IOError:
00070                 pass
00071 
00072     def __getattr__(self, attr):
00073         """Delegate everything but write to the stream"""
00074         return getattr(self.out, attr)
00075 sys.stdout = OutStreamEncoder(sys.stdout, 'utf8')
00076 sys.stderr = OutStreamEncoder(sys.stderr, 'utf8')
00077 
00078 
00079 class XmlHandler:
00080     """Deals with retrieval of XML files from API
00081     """
00082     def __init__(self, url):
00083         self.url = url
00084 
00085     def _grabUrl(self, url):
00086         try:
00087             urlhandle = urllib.urlopen(url)
00088         except IOError, errormsg:
00089             raise YouTubeHttpError(errormsg)
00090         return urlhandle.read()
00091 
00092     def getEt(self):
00093         xml = self._grabUrl(self.url)
00094         try:
00095             et = ElementTree.fromstring(xml)
00096         except SyntaxError, errormsg:
00097             raise YouTubeXmlError(errormsg)
00098         return et
00099 
00100 
00101 class Videos(object):
00102     """Main interface to http://www.youtube.com/
00103     This is done to support a common naming framework for all python Netvision plugins no matter their site
00104     target.
00105 
00106     Supports search methods
00107     The apikey is a not required to access http://www.youtube.com/
00108     """
00109     def __init__(self,
00110                 apikey,
00111                 mythtv = True,
00112                 interactive = False,
00113                 select_first = False,
00114                 debug = False,
00115                 custom_ui = None,
00116                 language = None,
00117                 search_all_languages = False,
00118                 ):
00119         """apikey (str/unicode):
00120             Specify the target site API key. Applications need their own key in some cases
00121 
00122         mythtv (True/False):
00123             When True, the returned meta data is being returned has the key and values massaged to match MythTV
00124             When False, the returned meta data  is being returned matches what target site returned
00125 
00126         interactive (True/False): (This option is not supported by all target site apis)
00127             When True, uses built-in console UI is used to select the correct show.
00128             When False, the first search result is used.
00129 
00130         select_first (True/False): (This option is not supported currently implemented in any grabbers)
00131             Automatically selects the first series search result (rather
00132             than showing the user a list of more than one series).
00133             Is overridden by interactive = False, or specifying a custom_ui
00134 
00135         debug (True/False):
00136              shows verbose debugging information
00137 
00138         custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
00139             A callable subclass of interactive class (overrides interactive option)
00140 
00141         language (2 character language abbreviation): (This option is not supported by all target site apis)
00142             The language of the returned data. Is also the language search
00143             uses. Default is "en" (English). For full list, run..
00144 
00145         search_all_languages (True/False): (This option is not supported by all target site apis)
00146             By default, a Netvision grabber will only search in the language specified using
00147             the language option. When this is True, it will search for the
00148             show in any language
00149 
00150         """
00151         self.config = {}
00152         self.mythxml = MythXML()
00153 
00154         if apikey is not None:
00155             self.config['apikey'] = apikey
00156         else:
00157             pass    # YouTube does not require an apikey
00158 
00159         self.config['debug_enabled'] = debug # show debugging messages
00160 
00161         self.log_name = "youtube"
00162         self.log = self._initLogger() # Setups the logger (self.log.debug() etc)
00163 
00164         self.config['custom_ui'] = custom_ui
00165 
00166         self.config['interactive'] = interactive # prompt for correct series?
00167 
00168         self.config['select_first'] = select_first
00169 
00170         self.config['search_all_languages'] = search_all_languages
00171 
00172         self.error_messages = {'YouTubeUrlError': u"! Error: The URL (%s) cause the exception error (%s)\n", 'YouTubeHttpError': u"! Error: An HTTP communications error with YouTube was raised (%s)\n", 'YouTubeRssError': u"! Error: Invalid RSS meta data\nwas received from YouTube error (%s). Skipping item.\n", 'YouTubeVideoNotFound': u"! Error: Video search with YouTube did not return any results (%s)\n", 'YouTubeVideoDetailError': u"! Error: Invalid Video meta data detail\nwas received from YouTube error (%s). Skipping item.\n", }
00173 
00174         # This is an example that must be customized for each target site
00175         self.key_translation = [{'channel_title': 'channel_title', 'channel_link': 'channel_link', 'channel_description': 'channel_description', 'channel_numresults': 'channel_numresults', 'channel_returned': 'channel_returned', 'channel_startindex': 'channel_startindex'}, {'title': 'item_title', 'author': 'item_author', 'published_parsed': 'item_pubdate', 'media_description': 'item_description', 'video': 'item_link', 'thumbnail': 'item_thumbnail', 'link': 'item_url', 'duration': 'item_duration', 'rating': 'item_rating', 'item_width': 'item_width', 'item_height': 'item_height', 'language': 'item_lang'}]
00176 
00177         # Defaulting to no language specified. The YouTube apis does support specifying a language
00178         if language:
00179             self.config['language'] = language
00180         else:
00181             self.config['language'] = u''
00182 
00183         self.config[u'urls'] = {}
00184 
00185         # v2 api calls - An example that must be customized for each target site
00186         self.config[u'urls'][u'video.search'] = 'http://gdata.youtube.com/feeds/api/videos?vq=%s&max-results=%s&start-index=%s&orderby=relevance&Ir=%s'
00187 
00188 
00189         # Functions that parse video data from RSS data
00190         self.config['item_parser'] = {}
00191         self.config['item_parser']['main'] = self.getVideosForURL
00192 
00193         # Tree view url and the function that parses that urls meta data
00194         self.config[u'urls'][u'tree.view'] = {
00195             'standard_feeds': {
00196                 '__all__': ['http://gdata.youtube.com/feeds/api/standardfeeds/%s?v=2', 'main'],
00197                 },
00198             'category': {
00199                 '__all__': ['http://gdata.youtube.com/feeds/api/videos?category=%s&v=2', 'main'],
00200                 },
00201             'local_feeds': {
00202                 '__all__': ['http://gdata.youtube.com/feeds/api/standardfeeds/%s?v=2', 'main'],
00203                 },
00204             'location_feeds': {
00205                 '__all__': ['http://gdata.youtube.com/feeds/api/videos?v=2&q=%s', 'main'],
00206                 },
00207             }
00208         self.config[u'urls'][u'categories_list'] = 'http://gdata.youtube.com/schemas/2007/categories.cat'
00209 
00210         self.config[u'image_extentions'] = ["png", "jpg", "bmp"] # Acceptable image extentions
00211 
00212         self.tree_order = ['standard_feeds', 'location_feeds', 'local_feeds', 'category']
00213         self.tree_org = {
00214             'category': [
00215                 ['Movies', u''],
00216                 ['Trailers and Movies', ['Trailers', 'Movies']],
00217                 ['Movies by Genre', ['Movies_Action_adventure', 'Movies_Drama', 'Movies_Sci_fi_fantasy', 'Movies_Thriller', 'Movies_Comedy', 'Movies_Classics', 'Movies_Horror', 'Movies_Family', 'Movies_Anime_animation', 'Movies_Foreign', 'Movies_Documentary', 'Movies_Shorts',]],
00218                 ['Amateur', ['Shortmov', 'Film']],
00219                 ['',u''],
00220                 ['TV', ['Shows', 'Comedy',]],
00221                 ['', ['Sports']],
00222                 ['Information', ['News', 'Tech', 'Education', 'Howto', ]],
00223                 ['Entertainment', ['Music', 'Games', 'Entertainment', ]],
00224                 ['Other', ['Autos', 'Animals', 'Travel', 'Videoblog', 'People', 'Nonprofit']] ],
00225             'standard_feeds':
00226                 [['Feeds', ['top_rated', 'top_favourites', 'most_viewed', 'most_popular', 'most_recent', 'most_discussed', 'most_responded', 'recently_featured', '']], ],
00227             'local_feeds':
00228                 [['Feeds', ['top_rated', 'top_favourites', 'most_viewed', 'most_popular', 'most_recent', 'most_discussed', 'most_responded', 'recently_featured', '']], ],
00229             'location_feeds':
00230                 [['', ['location']], ]
00231             }
00232 
00233         self.tree_customize = {
00234             'category': {
00235                 '__default__': {'order': 'rating', 'max-results': '20', 'start-index': '1', 'Ir': self.config['language']},
00236                 #'cat name': {'order: '', 'max-results': , 'start-index': , 'restriction: '', 'time': '', 'Ir': ''},
00237                 'Trailers': {'max-results': '40', 'time': 'this_month',},
00238                 'Movies': {'max-results': '40', 'time': 'this_month',},
00239                 'Music': {'max-results': '40', 'time': 'this_month',},
00240                 'Sports': {'max-results': '40', 'time': 'this_month',},
00241             },
00242             'standard_feeds': {
00243                 '__default__': {'order': 'rating', 'max-results': '20', 'start-index': '1', 'Ir': self.config['language'], 'time': 'this_month'},
00244                 #'feed name": {'order: '', 'max-results': , 'start-index': , 'restriction: '', 'time': '', 'Ir': ''}
00245             },
00246             'local_feeds': {
00247                 '__default__': {'order': 'rating', 'max-results': '20', 'start-index': '1', 'Ir': self.config['language'], 'location': '', 'location-radius':'500km'},
00248                 #'feed name": {'order: '', 'max-results': , 'start-index': , 'restriction: '', 'time': '', 'Ir': ''}
00249             },
00250             'location_feeds': {
00251                 '__default__': {'order': 'rating', 'max-results': '20', 'start-index': '1', 'Ir': self.config['language'], },
00252                 #'feed name": {'order: '', 'max-results': , 'start-index': , 'restriction: '', 'time': '', 'Ir': ''}
00253             },
00254             }
00255 
00256         self.feed_names = {
00257             'standard_feeds': {'top_rated': 'Highest Rated', 'top_favourites': 'Most Subscribed', 'most_viewed': 'Most Viewed', 'most_popular': 'Most Popular', 'most_recent': 'Most Recent', 'most_discussed': 'Most Comments', 'most_responded': 'Most Responses', 'recently_featured': 'Featured'}
00258             }
00259 
00260         self.feed_icons = {
00261             'standard_feeds': {'top_rated': 'directories/topics/rated', 'top_favourites': 'directories/topics/most_subscribed', 'most_viewed': 'directories/topics/most_viewed', 'most_popular': None, 'most_recent': 'directories/topics/most_recent', 'most_discussed': 'directories/topics/most_comments', 'most_responded': None, 'recently_featured': 'directories/topics/featured'
00262                 },
00263             'local_feeds': {'top_rated': 'directories/topics/rated', 'top_favourites': 'directories/topics/most_subscribed', 'most_viewed': 'directories/topics/most_viewed', 'most_popular': None, 'most_recent': 'directories/topics/most_recent', 'most_discussed': 'directories/topics/most_comments', 'most_responded': None, 'recently_featured': 'directories/topics/featured'
00264                 },
00265             'category': {
00266                 'Trailers and Movies': 'directories/topics/movies',
00267                 'Movies by Genre': 'directories/topics/movies',
00268                 'Amateur': 'directories/topics/movies',
00269                 'TV': 'directories/topics/tv',
00270                 'Movies': 'directories/topics/movies',
00271                 'Trailers': 'directories/film_genres/trailers',
00272                 'Movies_Action_adventure': 'directories/film_genres/action_adventure', 'Movies_Drama': 'directories/film_genres/drama', 'Movies_Sci_fi_fantasy': 'directories/film_genres/scifi', 'Movies_Thriller': 'directories/film_genres/thriller', 'Movies_Comedy': 'directories/film_genres/comedy', 'Movies_Classics': 'directories/film_genres/classics', 'Movies_Horror': 'directories/film_genres/horror', 'Movies_Family': 'directories/film_genres/family_films', 'Movies_Anime_animation': 'directories/film_genres/animation', 'Movies_Foreign': 'directories/film_genres/foreign_films', 'Movies_Documentary': 'directories/film_genres/documentaries', 'Movies_Shorts': 'directories/film_genres/short_film',
00273                 'Shortmov': 'directories/film_genres/short_film', 'Film': 'directories/film_genres/animation',
00274                 'Shows': 'directories/topics/tv', 'Comedy': 'directories/film_genres/comedy',
00275                 'Sports': 'directories/topics/sports',
00276                 'News': 'directories/topics/news', 'Tech': 'directories/topics/technology', 'Education': 'directories/topics/education', 'Howto': 'directories/topics/howto',
00277                 'Music': 'directories/topics/music', 'Games': 'directories/topics/games', 'Entertainment': 'directories/topics/entertainment',
00278                 'Autos': 'directories/topics/automotive', 'Animals': 'directories/topics/animals', 'Travel': 'directories/topics/travel', 'Videoblog': 'directories/topics/videoblog', 'People': 'directories/topics/people', 'Nonprofit': 'directories/topics/nonprofit',
00279                 },
00280             }
00281 
00282         self.treeview = False
00283         self.channel_icon = u'%SHAREDIR%/mythnetvision/icons/youtube.png'
00284     # end __init__()
00285 
00286 ###########################################################################################################
00287 #
00288 # Start - Utility functions
00289 #
00290 ###########################################################################################################
00291 
00292     def detectUserLocationByIP(self):
00293         '''Get longitude and latitiude to find videos relative to your location. Up to three different
00294         servers will be tried before giving up.
00295         return a dictionary e.g.
00296         {'Latitude': '43.6667', 'Country': 'Canada', 'Longitude': '-79.4167', 'City': 'Toronto'}
00297         return an empty dictionary if there were any errors
00298         Code found at: http://blog.suinova.com/2009/04/from-ip-to-geolocation-country-city.html
00299         '''
00300         def getExternalIP():
00301             '''Find the external IP address of this computer.
00302             '''
00303             url = urllib.URLopener()
00304             try:
00305                 resp = url.open('http://www.whatismyip.com/automation/n09230945.asp')
00306                 return resp.read()
00307             except:
00308                 return None
00309             # end getExternalIP()
00310 
00311         ip = getExternalIP()
00312 
00313         if ip == None:
00314             return {}
00315 
00316         try:
00317             gs = urllib.urlopen('http://blogama.org/ip_query.php?ip=%s&output=xml' % ip)
00318             txt = gs.read()
00319         except:
00320             try:
00321                 gs = urllib.urlopen('http://www.seomoz.org/ip2location/look.php?ip=%s' % ip)
00322                 txt = gs.read()
00323             except:
00324                 try:
00325                     gs = urllib.urlopen('http://api.hostip.info/?ip=%s' % ip)
00326                     txt = gs.read()
00327                 except:
00328                     logging.error('GeoIP servers not available')
00329                     return {}
00330         try:
00331             if txt.find('<Response>') > 0:
00332                 countrys = re.findall(r'<CountryName>([\w ]+)<',txt)[0]
00333                 citys = re.findall(r'<City>([\w ]+)<',txt)[0]
00334                 lats,lons = re.findall(r'<Latitude>([\d\-\.]+)</Latitude>\s*<Longitude>([\d\-\.]+)<',txt)[0]
00335             elif txt.find('GLatLng') > 0:
00336                 citys,countrys = re.findall('<br />\s*([^<]+)<br />\s*([^<]+)<',txt)[0]
00337                 lats,lons = re.findall('LatLng\(([-\d\.]+),([-\d\.]+)',txt)[0]
00338             elif txt.find('<gml:coordinates>') > 0:
00339                 citys = re.findall('<Hostip>\s*<gml:name>(\w+)</gml:name>',txt)[0]
00340                 countrys = re.findall('<countryName>([\w ,\.]+)</countryName>',txt)[0]
00341                 lats,lons = re.findall('gml:coordinates>([-\d\.]+),([-\d\.]+)<',txt)[0]
00342             else:
00343                 logging.error('error parsing IP result %s'%txt)
00344                 return {}
00345             return {'Country':countrys,'City':citys,'Latitude':lats,'Longitude':lons}
00346         except:
00347             logging.error('Error parsing IP result %s'%txt)
00348             return {}
00349     # end detectUserLocationByIP()
00350 
00351 
00352     def massageDescription(self, text):
00353         '''Removes HTML markup from a text string.
00354         @param text The HTML source.
00355         @return The plain text.  If the HTML source contains non-ASCII
00356         entities or character references, this is a Unicode string.
00357         '''
00358         def fixup(m):
00359             text = m.group(0)
00360             if text[:1] == "<":
00361                 return "" # ignore tags
00362             if text[:2] == "&#":
00363                 try:
00364                     if text[:3] == "&#x":
00365                         return unichr(int(text[3:-1], 16))
00366                     else:
00367                         return unichr(int(text[2:-1]))
00368                 except ValueError:
00369                     pass
00370             elif text[:1] == "&":
00371                 import htmlentitydefs
00372                 entity = htmlentitydefs.entitydefs.get(text[1:-1])
00373                 if entity:
00374                     if entity[:2] == "&#":
00375                         try:
00376                             return unichr(int(entity[2:-1]))
00377                         except ValueError:
00378                             pass
00379                     else:
00380                         return unicode(entity, "iso-8859-1")
00381             return text # leave as is
00382         return self.ampReplace(re.sub(u"(?s)<[^>]*>|&#?\w+;", fixup, self.textUtf8(text))).replace(u'\n',u' ')
00383     # end massageDescription()
00384 
00385 
00386     def _initLogger(self):
00387         """Setups a logger using the logging module, returns a log object
00388         """
00389         logger = logging.getLogger(self.log_name)
00390         formatter = logging.Formatter('%(asctime)s) %(levelname)s %(message)s')
00391 
00392         hdlr = logging.StreamHandler(sys.stdout)
00393 
00394         hdlr.setFormatter(formatter)
00395         logger.addHandler(hdlr)
00396 
00397         if self.config['debug_enabled']:
00398             logger.setLevel(logging.DEBUG)
00399         else:
00400             logger.setLevel(logging.WARNING)
00401         return logger
00402     #end initLogger
00403 
00404 
00405     def textUtf8(self, text):
00406         if text == None:
00407             return text
00408         try:
00409             return unicode(text, 'utf8')
00410         except UnicodeDecodeError:
00411             return u''
00412         except (UnicodeEncodeError, TypeError):
00413             return text
00414     # end textUtf8()
00415 
00416 
00417     def ampReplace(self, text):
00418         '''Replace all "&" characters with "&amp;"
00419         '''
00420         text = self.textUtf8(text)
00421         return text.replace(u'&amp;',u'~~~~~').replace(u'&',u'&amp;').replace(u'~~~~~', u'&amp;')
00422     # end ampReplace()
00423 
00424     def setTreeViewIcon(self, dir_icon=None):
00425         '''Check if there is a specific generic tree view icon. If not default to the channel icon.
00426         return self.tree_dir_icon
00427         '''
00428         self.tree_dir_icon = self.channel_icon
00429         if not dir_icon:
00430             if not self.feed_icons.has_key(self.tree_key):
00431                 return self.tree_dir_icon
00432             if not self.feed_icons[self.tree_key].has_key(self.feed):
00433                 return self.tree_dir_icon
00434             dir_icon = self.feed_icons[self.tree_key][self.feed]
00435             if not dir_icon:
00436                 return self.tree_dir_icon
00437         self.tree_dir_icon = u'%%SHAREDIR%%/mythnetvision/icons/%s.png' % (dir_icon, )
00438         return self.tree_dir_icon
00439     # end setTreeViewIcon()
00440 
00441 ###########################################################################################################
00442 #
00443 # End of Utility functions
00444 #
00445 ###########################################################################################################
00446 
00447 
00448     def searchTitle(self, title, pagenumber, pagelen):
00449         '''Key word video search of the YouTube web site
00450         return an array of matching item dictionaries
00451         return
00452         '''
00453         url = self.config[u'urls'][u'video.search'] % (urllib.quote_plus(title.encode("utf-8")), pagelen, pagenumber, self.config['language'], )
00454         if self.config['debug_enabled']:
00455             print url
00456             print
00457 
00458         try:
00459             etree = XmlHandler(url).getEt()
00460         except Exception, errormsg:
00461             raise YouTubeUrlError(self.error_messages['YouTubeUrlError'] % (url, errormsg))
00462 
00463         if etree is None:
00464             raise YouTubeVideoNotFound(u"No YouTube Video matches found for search value (%s)" % title)
00465 
00466         data = []
00467         for entry in etree:
00468             if entry.tag.endswith('totalResults'):
00469                 if entry.text:
00470                     self.channel['channel_numresults'] = int(entry.text)
00471                 else:
00472                     self.channel['channel_numresults'] = 0
00473                 continue
00474             if not entry.tag.endswith('entry'):
00475                 continue
00476             item = {}
00477             cur_size = True
00478             flash = False
00479             for parts in entry:
00480                 if parts.tag.endswith('id'):
00481                     item['id'] = parts.text
00482                     continue
00483                 if parts.tag.endswith('title'):
00484                     item['title'] = parts.text
00485                     continue
00486                 if parts.tag.endswith('author'):
00487                     for e in parts:
00488                         if e.tag.endswith('name'):
00489                             item['author'] = e.text
00490                             break
00491                     continue
00492                 if parts.tag.endswith('published'):
00493                     item['published_parsed'] = parts.text
00494                     continue
00495                 if parts.tag.endswith('content'):
00496                     item['media_description'] = parts.text
00497                     continue
00498                 if parts.tag.endswith(u'rating'):
00499                     item['rating'] = parts.get('average')
00500                     continue
00501                 if not parts.tag.endswith(u'group'):
00502                     continue
00503                 for elem in parts:
00504                     if elem.tag.endswith(u'duration'):
00505                         item['duration'] =  elem.get('seconds')
00506                         continue
00507                     if elem.tag.endswith(u'thumbnail'):
00508                         if cur_size == False:
00509                             continue
00510                         height = elem.get('height')
00511                         width = elem.get('width')
00512                         if int(width) > cur_size:
00513                             item['thumbnail'] = self.ampReplace(elem.get('url'))
00514                             cur_size = int(width)
00515                         if int(width) >= 200:
00516                             cur_size = False
00517                         continue
00518                     if elem.tag.endswith(u'player'):
00519                         item['link'] = self.ampReplace(elem.get('url'))
00520                         continue
00521                     if elem.tag.endswith(u'content') and flash == False:
00522                         for key in elem.keys():
00523                             if not key.endswith(u'format'):
00524                                 continue
00525                             if not elem.get(key) == '5':
00526                                 continue
00527                             self.processVideoUrl(item, elem)
00528                             flash = True
00529                         continue
00530             if not item.has_key('video'):
00531                 item['video'] =  item['link']
00532                 item['duration'] =  u''
00533             else:
00534                 item['link'] =  item['video']
00535             data.append(item)
00536 
00537         # Make sure there are no item elements that are None
00538         for item in data:
00539             for key in item.keys():
00540                 if item[key] == None:
00541                     item[key] = u''
00542 
00543         # Massage each field and eliminate any item without a URL
00544         elements_final = []
00545         for item in data:
00546             if not 'id' in item.keys():
00547                 continue
00548             item['language'] = self.config['language']
00549             for key in item.keys(): # 2010-01-23T08:38:39.000Z
00550                 if key == 'published_parsed':
00551                     if item[key]:
00552                         pub_time = time.strptime(item[key].strip(), "%Y-%m-%dT%H:%M:%S.%fZ")
00553                         item[key] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', pub_time)
00554                     continue
00555                 if key == 'media_description' or key == 'title':
00556                     # Strip the HTML tags
00557                     if item[key]:
00558                         item[key] = self.massageDescription(item[key].strip())
00559                         item[key] = item[key].replace(u'|', u'-')
00560                     continue
00561                 if type(item[key]) == type(u''):
00562                     if item[key]:
00563                         item[key] = self.ampReplace(item[key].replace('"\n',' ').strip())
00564             elements_final.append(item)
00565 
00566         if not len(elements_final):
00567             raise YouTubeVideoNotFound(u"No YouTube Video matches found for search value (%s)" % title)
00568 
00569         return elements_final
00570         # end searchTitle()
00571 
00572 
00573     def searchForVideos(self, title, pagenumber):
00574         """Common name for a video search. Used to interface with MythTV plugin NetVision
00575         """
00576         # Channel details and search results
00577         self.channel = {'channel_title': u'YouTube', 'channel_link': u'http://www.youtube.com/', 'channel_description': u"Share your videos with friends, family, and the world.", 'channel_numresults': 0, 'channel_returned': 1, u'channel_startindex': 0}
00578 
00579         # Easier for debugging
00580 #        print self.searchTitle(title, pagenumber, self.page_limit)
00581 #        print
00582 #        sys.exit()
00583 
00584         startindex = (int(pagenumber) -1) * self.page_limit + 1
00585         try:
00586             data = self.searchTitle(title, startindex, self.page_limit)
00587         except YouTubeVideoNotFound, msg:
00588             sys.stderr.write(u"%s\n" % msg)
00589             return None
00590         except YouTubeUrlError, msg:
00591             sys.stderr.write(u'%s\n' % msg)
00592             sys.exit(1)
00593         except YouTubeHttpError, msg:
00594             sys.stderr.write(self.error_messages['YouTubeHttpError'] % msg)
00595             sys.exit(1)
00596         except YouTubeRssError, msg:
00597             sys.stderr.write(self.error_messages['YouTubeRssError'] % msg)
00598             sys.exit(1)
00599         except Exception, e:
00600             sys.stderr.write(u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
00601             sys.exit(1)
00602 
00603         if data == None:
00604             return None
00605         if not len(data):
00606             return None
00607 
00608         items = []
00609         for match in data:
00610             item_data = {}
00611             for key in self.key_translation[1].keys():
00612                 if key in match.keys():
00613                     item_data[self.key_translation[1][key]] = match[key]
00614                 else:
00615                     item_data[self.key_translation[1][key]] = u''
00616             items.append(item_data)
00617 
00618         self.channel['channel_startindex'] = self.page_limit * int(pagenumber)
00619         self.channel['channel_returned'] = len(items)
00620 
00621         if len(items):
00622             return [[self.channel, items]]
00623         return None
00624     # end searchForVideos()
00625 
00626     def getCategories(self, dir_dict, categories):
00627         '''Parse a dictionary made of subdictionaries and category list and extract all of the categories
00628         return a list of categories
00629         '''
00630         for sets in dir_dict:
00631             if isinstance(sets[1], str):
00632                 continue
00633             for cat in sets[1]:
00634                 categories.append(cat)
00635         return categories
00636     # end getCategories()
00637 
00638     def displayTreeView(self):
00639         '''Gather the Youtube categories/feeds/...etc then get a max page of videos meta data in each of them
00640         return array of directories and their video metadata
00641         '''
00642         # Channel details and search results
00643         self.channel = {'channel_title': u'YouTube', 'channel_link': u'http://www.youtube.com/', 'channel_description': u"Share your videos with friends, family, and the world.", 'channel_numresults': 0, 'channel_returned': 1, u'channel_startindex': 0}
00644 
00645         if self.config['debug_enabled']:
00646             print self.config[u'urls']
00647             print
00648 
00649         if self.config['debug_enabled']:
00650             print self.config[u'urls'][u'categories_list']
00651             print
00652 
00653         try:
00654             etree = XmlHandler(self.config[u'urls'][u'categories_list']).getEt()
00655         except Exception, errormsg:
00656             raise YouTubeUrlError(self.error_messages['YouTubeUrlError'] % (url, errormsg))
00657 
00658         if etree is None:
00659             raise YouTubeCategoryNotFound(u"No YouTube Categories found for Tree view")
00660 
00661         cats = []
00662         for category in etree:
00663             if category.tag.endswith('category'):
00664                 cats.append({'term': category.get('term'), 'label': category.get('label')})
00665         if not len(cats):
00666             raise YouTubeCategoryNotFound(u"No YouTube Category tags found for Tree view")
00667 
00668         self.feed_names['category'] = {}
00669         for category in cats:
00670             self.feed_names['category'][category['term']] = self.ampReplace(category['label'])
00671 
00672         # Verify all categories are already in site tree map add any new ones to 'Other'
00673         categories = []
00674         categories = self.getCategories(self.tree_org['category'], categories)
00675 
00676         # Add any categories that are not in the preset tree map
00677         new_category = []
00678         for category in self.feed_names['category'].keys():
00679             if category in categories:
00680                 continue
00681             new_category.append(category)
00682         if len(new_category):
00683             self.tree_org['category'].append(['New', new_category])
00684             self.tree_org['category'].append(['', u''])
00685 
00686         # Add local feed details
00687         # {'Latitude': '43.6667', 'Country': 'Canada', 'Longitude': '-79.4167', 'City': 'Toronto'}
00688         longitude_latitude = self.detectUserLocationByIP()
00689         if len(longitude_latitude):
00690             self.feed_names['local_feeds'] = dict(self.feed_names['standard_feeds'])
00691             self.tree_customize['local_feeds']['__default__']['location'] = u"%s,%s" % (longitude_latitude['Latitude'], longitude_latitude['Longitude'])
00692             self.tree_org['local_feeds'][0][0] = u'Youtube Feeds within %s of %s, %s' % (self.tree_customize['local_feeds']['__default__']['location-radius'], longitude_latitude['City'], longitude_latitude['Country'])
00693         else:
00694             self.tree_order.remove('local_feeds')
00695         # Set location search parameters
00696         if len(longitude_latitude):
00697             city_country = u'%s+%s' % (longitude_latitude['City'], longitude_latitude['Country'])
00698             self.tree_org['location_feeds'][0][1][0] = city_country
00699             self.feed_names['location_feeds'] = dict({u'%s' % city_country: u'Youtube Videos for %s, %s' % (longitude_latitude['City'], longitude_latitude['Country'])})
00700         else:
00701             self.tree_order.remove('location_feeds')
00702 
00703         # Set the default videos per page limit for all feeds/categories/... etc
00704         for key in self.tree_customize.keys():
00705             if '__default__' in self.tree_customize[key].keys():
00706                 if 'max-results' in self.tree_customize[key]['__default__'].keys():
00707                     self.tree_customize[key]['__default__']['max-results'] = unicode(self.page_limit)
00708 
00709         # Get videos within each category
00710         dictionaries = []
00711 
00712         # Process the various video feeds/categories/... etc
00713         for key in self.tree_order:
00714             self.tree_key = key
00715             dictionaries = self.getVideos(self.tree_org[key], dictionaries)
00716 
00717         return [[self.channel, dictionaries]]
00718     # end displayTreeView()
00719 
00720     def processVideoUrl(self, item, elem):
00721         '''Processes elem.get('url') to either use a custom HTML page served by
00722         the backend, or include '&autoplay=1'
00723         '''
00724         m = re.search('/v/([^?]+)', elem.get('url'))
00725         if m:
00726             url = self.mythxml.getInternetContentUrl("nv_python_libs/configs/HTML/youtube.html", \
00727                                                      m.group(1))
00728             item['video'] = self.ampReplace(url)
00729         else:
00730             item['video'] = self.ampReplace((elem.get('url')+'&autoplay=1'))
00731 
00732     def makeURL(self, URL):
00733         '''Form a URL to search for videos
00734         return a URL
00735         '''
00736         additions = dict(self.tree_customize[self.tree_key]['__default__']) # Set defaults
00737 
00738         # Add customizations
00739         if self.feed in self.tree_customize[self.tree_key].keys():
00740             for element in self.tree_customize[self.tree_key][self.feed].keys():
00741                 additions[element] = self.tree_customize[self.tree_key][self.feed][element]
00742 
00743         # Make the search extension string that is added to the URL
00744         addition = u''
00745         for ky in additions.keys():
00746             if ky.startswith('add_'):
00747                 addition+=u'/%s' %  additions[ky]
00748             else:
00749                 addition+=u'&%s=%s' %  (ky, additions[ky])
00750         index = URL.find('%')
00751         if index == -1:
00752             return (URL+addition)
00753         else:
00754             return (URL+addition) % self.feed
00755     # end makeURL()
00756 
00757 
00758     def getVideos(self, dir_dict, dictionaries):
00759         '''Parse a list made of category lists and retrieve video meta data
00760         return a dictionary of directory names and categories video metadata
00761         '''
00762         for sets in dir_dict:
00763             if not isinstance(sets[1], list):
00764                 if sets[0] != '': # Add the nested dictionaries display name
00765                     try:
00766                         dictionaries.append([self.massageDescription(sets[0]), self.setTreeViewIcon(self.feed_icons[self.tree_key][sets[0]])])
00767                     except KeyError:
00768                         dictionaries.append([self.massageDescription(sets[0]), self.channel_icon])
00769                 else:
00770                     dictionaries.append(['', u'']) # Add the nested dictionary indicator
00771                 continue
00772             temp_dictionary = []
00773             for self.feed in sets[1]:
00774                 if self.config[u'urls'][u'tree.view'][self.tree_key].has_key('__all__'):
00775                     URL = self.config[u'urls'][u'tree.view'][self.tree_key]['__all__']
00776                 else:
00777                     URL = self.config[u'urls'][u'tree.view'][self.tree_key][self.feed]
00778                 temp_dictionary = self.config['item_parser'][URL[1]](self.makeURL(URL[0]), temp_dictionary)
00779             if len(temp_dictionary):
00780                 if len(sets[0]): # Add the nested dictionaries display name
00781                     try:
00782                         dictionaries.append([self.massageDescription(sets[0]), self.setTreeViewIcon(self.feed_icons[self.tree_key][sets[0]])])
00783                     except KeyError:
00784                         dictionaries.append([self.massageDescription(sets[0]), self.channel_icon])
00785                 for element in temp_dictionary:
00786                     dictionaries.append(element)
00787                 if len(sets[0]):
00788                     dictionaries.append(['', u'']) # Add the nested dictionary indicator
00789         return dictionaries
00790     # end getVideos()
00791 
00792     def getVideosForURL(self, url, dictionaries):
00793         '''Get the video metadata for url search
00794         return the video dictionary of directories and their video mata data
00795         '''
00796         initial_length = len(dictionaries)
00797 
00798         if self.config['debug_enabled']:
00799             print "Category URL:"
00800             print url
00801             print
00802 
00803         try:
00804             etree = XmlHandler(url).getEt()
00805         except Exception, errormsg:
00806             sys.stderr.write(self.error_messages['YouTubeUrlError'] % (url, errormsg))
00807             return dictionaries
00808 
00809         if etree is None:
00810             sys.stderr.write(u'1-No Videos for (%s)\n' % self.feed)
00811             return dictionaries
00812 
00813         dictionary_first = False
00814         for elements in etree:
00815             if elements.tag.endswith(u'totalResults'):
00816                 self.channel['channel_numresults'] += int(elements.text)
00817                 self.channel['channel_startindex'] = self.page_limit
00818                 self.channel['channel_returned'] = self.page_limit # False value CHANGE later
00819                 continue
00820 
00821             if not elements.tag.endswith(u'entry'):
00822                 continue
00823 
00824             metadata = {}
00825             cur_size = True
00826             flash = False
00827             for e in elements:
00828                 metadata['language'] = self.config['language']
00829                 if e.tag.endswith(u'published'): # '2009-02-13T04:54:28.000Z'
00830                     if e.text:
00831                         pub_time = time.strptime(e.text.strip(), "%Y-%m-%dT%H:%M:%S.000Z")
00832                         metadata['published_parsed'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', pub_time)
00833                     continue
00834                 if e.tag.endswith(u'rating'):
00835                     if e.get('average'):
00836                         metadata['rating'] = e.get('average')
00837                     continue
00838                 if e.tag.endswith(u'title'):
00839                     if e.text:
00840                         metadata['title'] = self.massageDescription(e.text.strip())
00841                     continue
00842                 if e.tag.endswith(u'author'):
00843                     for a in e:
00844                         if a.tag.endswith(u'name'):
00845                             if a.text:
00846                                 metadata['author'] = self.massageDescription(a.text.strip())
00847                             break
00848                     continue
00849                 if not e.tag.endswith(u'group'):
00850                     continue
00851                 for elem in e:
00852                     if elem.tag.endswith(u'description'):
00853                         if elem.text != None:
00854                             metadata['media_description'] = self.massageDescription(elem.text.strip())
00855                         else:
00856                             metadata['media_description'] = u''
00857                         continue
00858                     if elem.tag.endswith(u'duration'):
00859                         if elem.get('seconds'):
00860                             metadata['duration'] =  elem.get('seconds').strip()
00861                         continue
00862                     if elem.tag.endswith(u'thumbnail'):
00863                         if cur_size == False:
00864                             continue
00865                         height = elem.get('height')
00866                         width = elem.get('width')
00867                         if int(width) > cur_size:
00868                             if elem.get('url'):
00869                                 metadata['thumbnail'] = self.ampReplace(elem.get('url'))
00870                                 cur_size = int(width)
00871                         if int(width) >= 200:
00872                             cur_size = False
00873                         continue
00874                     if elem.tag.endswith(u'player'):
00875                         if elem.get('url'):
00876                             metadata['link'] = self.ampReplace(elem.get('url'))
00877                         continue
00878                     if elem.tag.endswith(u'content') and flash == False:
00879                         for key in elem.keys():
00880                             if not key.endswith(u'format'):
00881                                 continue
00882                             if not elem.get(key) == '5':
00883                                 continue
00884                             if elem.get('url'):
00885                                 self.processVideoUrl(metadata, elem)
00886                                 flash = True
00887                         continue
00888 
00889             if not metadata.has_key('video') and not metadata.has_key('link'):
00890                 continue
00891 
00892             if not metadata.has_key('video'):
00893                 metadata['video'] = metadata['link']
00894             else:
00895                 metadata['link'] =  metadata['video']
00896 
00897             if not dictionary_first:  # Add the dictionaries display name
00898                 dictionaries.append([self.massageDescription(self.feed_names[self.tree_key][self.feed]), self.setTreeViewIcon()])
00899                 dictionary_first = True
00900 
00901             final_item = {}
00902             for key in self.key_translation[1].keys():
00903                 if not metadata.has_key(key):
00904                     final_item[self.key_translation[1][key]] = u''
00905                 else:
00906                     final_item[self.key_translation[1][key]] = metadata[key]
00907             dictionaries.append(final_item)
00908 
00909         if initial_length < len(dictionaries): # Need to check if there was any items for this Category
00910             dictionaries.append(['', u'']) # Add the nested dictionary indicator
00911         return dictionaries
00912     # end getVideosForURL()
00913 # end Videos() class
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends