|
MythTV
0.26-pre
|
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 "&" 00419 ''' 00420 text = self.textUtf8(text) 00421 return text.replace(u'&',u'~~~~~').replace(u'&',u'&').replace(u'~~~~~', u'&') 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
1.7.6.1