14 __title__ =
"mtv_api - Simple-to-use Python interface to the MTV API (http://developer.mtvnservices.com/docs)" 15 __author__=
"R.D. Vaughan" 17 This python script is intended to perform a variety of utility functions to search and access text 18 metadata, video and image URLs from MTV. These routines are based on the api. Specifications 19 for this api are published at http://developer.mtvnservices.com/docs 38 import os, struct, sys, re, time
39 from datetime
import datetime, timedelta
40 import urllib, urllib2
44 import xml.etree.cElementTree
as ElementTree
46 import xml.etree.ElementTree
as ElementTree
48 from mtv_exceptions
import (MtvUrlError, MtvHttpError, MtvRssError, MtvVideoNotFound, MtvInvalidSearchType, MtvXmlError, MtvVideoDetailError)
51 """Wraps a stream with an encoder""" 60 """Wraps the output stream, encoding Unicode strings with the specified encoding""" 61 if isinstance(obj, unicode):
73 """Delegate everything but write to the stream""" 74 return getattr(self.
out, attr)
80 """Deals with retrieval of XML files from API 87 urlhandle = urllib.urlopen(url)
88 except IOError, errormsg:
89 raise MtvHttpError(errormsg)
90 return urlhandle.read()
95 et = ElementTree.fromstring(xml)
96 except SyntaxError, errormsg:
97 raise MtvXmlError(errormsg)
102 """Main interface to http://www.mtv.com/ 103 This is done to support a common naming framework for all python Netvision plugins no matter their site 106 Supports search methods 107 The apikey is a not required to access http://www.mtv.com/ 113 select_first = False,
117 search_all_languages = False,
119 """apikey (str/unicode): 120 Specify the target site API key. Applications need their own key in some cases 123 When True, the returned meta data is being returned has the key and values massaged to match MythTV 124 When False, the returned meta data is being returned matches what target site returned 126 interactive (True/False): (This option is not supported by all target site apis) 127 When True, uses built-in console UI is used to select the correct show. 128 When False, the first search result is used. 130 select_first (True/False): (This option is not supported currently implemented in any grabbers) 131 Automatically selects the first series search result (rather 132 than showing the user a list of more than one series). 133 Is overridden by interactive = False, or specifying a custom_ui 136 shows verbose debugging information 138 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers) 139 A callable subclass of interactive class (overrides interactive option) 141 language (2 character language abbreviation): (This option is not supported by all target site apis) 142 The language of the returned data. Is also the language search 143 uses. Default is "en" (English). For full list, run.. 145 search_all_languages (True/False): (This option is not supported by all target site apis) 146 By default, a Netvision grabber will only search in the language specified using 147 the language option. When this is True, it will search for the 153 if apikey
is not None:
154 self.
config[
'apikey'] = apikey
158 self.
config[
'debug_enabled'] = debug
163 self.
config[
'custom_ui'] = custom_ui
167 self.
config[
'select_first'] = select_first
169 self.
config[
'search_all_languages'] = search_all_languages
172 self.
config[
'language'] =
"en" 174 self.
error_messages = {
'MtvUrlError':
u"! Error: The URL (%s) cause the exception error (%s)\n",
'MtvHttpError':
u"! Error: An HTTP communications error with MTV was raised (%s)\n",
'MtvRssError':
u"! Error: Invalid RSS metadata\nwas received from MTV error (%s). Skipping item.\n",
'MtvVideoNotFound':
u"! Error: Video search with MTV did not return any results (%s)\n",
'MtvVideoDetailError':
u"! Error: Invalid Video metadata detail\nwas received from MTV error (%s). Skipping item.\n", }
177 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',
'media_credit':
'item_author',
'published_parsed':
'item_pubdate',
'media_description':
'item_description',
'video':
'item_link',
'thumbnail':
'item_thumbnail',
'link':
'item_url',
'duration':
'item_duration',
'item_rating':
'item_rating',
'item_width':
'item_width',
'item_height':
'item_height',
'language':
'item_lang'}]
181 self.
config[
u'image_extentions'] = [
"png",
"jpg",
"bmp"]
185 self.
config[
'item_parser'] = {}
191 '__all__': [
'http://api.mtvnservices.com/1/genre/%s/videos/?',
'main'],
194 '__all__': [
'http://api.mtvnservices.com/1/genre/%s/videos/?',
'main'],
200 'new_genres': [[
'New over the last 3 months', [
'pop',
'rock',
'metal',
'randb',
'jazz',
'blues_folk',
'country',
'latin',
'hip_hop',
'world_reggae',
'electronic_dance',
'easy_listening',
'classical',
'soundtracks_musicals',
'alternative',
'environmental', ]],
202 'genres': [[
'All Genres', [
'pop',
'rock',
'metal',
'randb',
'jazz',
'blues_folk',
'country',
'latin',
'hip_hop',
'world_reggae',
'electronic_dance',
'easy_listening',
'classical',
'soundtracks_musicals',
'alternative',
'environmental', ]],
208 yr = d1 - timedelta(weeks=52)
209 mts = d1 - timedelta(days=93)
210 last_3_months =
u'%s-%s' % (mts.strftime(
'%m%d%Y'), d1.strftime(
'%m%d%Y'))
211 last_year =
u'%s-%s' % (yr.strftime(
'%m%d%Y'), d1.strftime(
'%m%d%Y'))
216 '__default__': {
'max-results':
'20',
'start-index':
'1',
'date': last_3_months,
'sort':
'date_descending'},
220 '__default__': {
'max-results':
'20',
'start-index':
'1',
'sort':
'date_descending'},
222 'rock': {
'date': last_year, },
223 'R&B': {
'date': last_year, },
224 'country': {
'date': last_year, },
225 'hip_hop': {
'date': last_year, },
230 'new_genres': {
'world_reggae':
'World/Reggae',
'pop':
'Pop',
'metal':
'Metal',
'environmental':
'Environmental',
'latin':
'Latin',
'randb':
'R&B',
'rock':
'Rock',
'easy_listening':
'Easy Listening',
'jazz':
'Jazz',
'country':
'Country',
'hip_hop':
'Hip-Hop',
'classical':
'Classical',
'electronic_dance':
'Electro / Dance',
'blues_folk':
'Blues / Folk',
'alternative':
'Alternative',
'soundtracks_musicals':
'Soundtracks / Musicals',
'New over the last 3 months':
'directories/topics/month' 232 'genres': {
'world_reggae':
'World/Reggae',
'pop':
'Pop',
'metal':
'Metal',
'environmental':
'Environmental',
'latin':
'Latin',
'randb':
'R&B',
'rock':
'Rock',
'easy_listening':
'Easy Listening',
'jazz':
'Jazz',
'country':
'Country',
'hip_hop':
'Hip-Hop',
'classical':
'Classical',
'electronic_dance':
'Electro / Dance',
'blues_folk':
'Blues / Folk',
'alternative':
'Alternative',
'soundtracks_musicals':
'Soundtracks / Musicals',
237 'new_genres': {
'New over the last 3 months':
'directories/topics/recent',
'world_reggae':
'directories/music_genres/world_reggae',
'pop':
'directories/music_genres/pop',
'metal':
'directories/music_genres/metal',
'environmental':
'directories/music_genres/environmental',
'latin':
'directories/music_genres/latino',
'randb':
'directories/music_genres/rnb',
'rock':
'directories/music_genres/rock',
'easy_listening':
'directories/music_genres/easy_listening',
'jazz':
'directories/music_genres/jazz',
'country':
'directories/music_genres/country',
'hip_hop':
'directories/music_genres/hiphop',
'classical':
'directories/music_genres/classical',
'electronic_dance':
'directories/music_genres/electronic_dance',
'blues_folk':
'directories/music_genres/blues_folk',
'alternative':
'directories/music_genres/alternative',
'soundtracks_musicals':
'directories/music_genres/soundtracks_musicals',
239 'genres': {
'Genres':
'directories/topics/music',
'world_reggae':
'directories/music_genres/world_reggae',
'pop':
'directories/music_genres/pop',
'metal':
'directories/music_genres/metal',
'environmental':
'directories/music_genres/environmental',
'latin':
'directories/music_genres/latino',
'randb':
'directories/music_genres/rnb',
'rock':
'directories/music_genres/rock',
'easy_listening':
'directories/music_genres/easy_listening',
'jazz':
'directories/music_genres/jazz',
'country':
'directories/music_genres/country',
'hip_hop':
'directories/music_genres/hiphop',
'classical':
'directories/music_genres/classical',
'electronic_dance':
'directories/music_genres/electronic_dance',
'blues_folk':
'directories/music_genres/blues_folk',
'alternative':
'directories/music_genres/alternative',
'soundtracks_musicals':
'directories/music_genres/soundtracks_musicals',
243 self.
mtvHtmlPath =
u'file://'+os.path.dirname( os.path.realpath( __file__ )).replace(
u'/nv_python_libs/mtv',
u'/nv_python_libs/configs/HTML/mtv.html?title=%s&videocode=%s')
247 self.
channel_icon =
u'%SHAREDIR%/mythnetvision/icons/mtv.png' 257 '''Removes HTML markup from a text string. 258 @param text The HTML source. 259 @return The plain text. If the HTML source contains non-ASCII 260 entities or character references, this is a Unicode string. 268 if text[:3] ==
"&#x":
269 return unichr(int(text[3:-1], 16))
271 return unichr(int(text[2:-1]))
274 elif text[:1] ==
"&":
275 import htmlentitydefs
276 entity = htmlentitydefs.entitydefs.get(text[1:-1])
278 if entity[:2] ==
"&#":
280 return unichr(int(entity[2:-1]))
284 return unicode(entity,
"iso-8859-1")
286 return self.
ampReplace(re.sub(
u"(?s)<[^>]*>|&#?\w+;", fixup, self.
textUtf8(text))).replace(
u'\n',
u' ')
291 """Setups a logger using the logging module, returns a log object 293 logger = logging.getLogger(self.
log_name)
294 formatter = logging.Formatter(
'%(asctime)s) %(levelname)s %(message)s')
296 hdlr = logging.StreamHandler(sys.stdout)
298 hdlr.setFormatter(formatter)
299 logger.addHandler(hdlr)
301 if self.
config[
'debug_enabled']:
302 logger.setLevel(logging.DEBUG)
304 logger.setLevel(logging.WARNING)
314 except UnicodeDecodeError:
316 except (UnicodeEncodeError, TypeError):
322 '''Replace all "&" characters with "&" 325 return text.replace(
u'&',
u'~~~~~').replace(
u'&',
u'&').replace(
u'~~~~~',
u'&')
330 '''Check if there is a specific generic tree view icon. If not default to the channel icon. 331 return self.tree_dir_icon 342 self.
tree_dir_icon =
u'%%SHAREDIR%%/mythnetvision/icons/%s.png' % (dir_icon, )
354 '''Key word video search of the MTV web site 355 return an array of matching item dictionaries 358 url = self.
config[
u'urls'][
u'video.search'] % (urllib.quote_plus(title.encode(
"utf-8")), pagenumber , pagelen,)
359 if self.
config[
'debug_enabled']:
365 except Exception, errormsg:
366 raise MtvUrlError(self.
error_messages[
'MtvUrlError'] % (url, errormsg))
369 raise MtvVideoNotFound(
u"No MTV Video matches found for search value (%s)" % title)
373 if not entry.tag.endswith(
'entry'):
377 if parts.tag.endswith(
'id'):
378 item[
'id'] = parts.text
379 if parts.tag.endswith(
'title'):
380 item[
'title'] = parts.text
381 if parts.tag.endswith(
'author'):
383 if e.tag.endswith(
'name'):
384 item[
'media_credit'] = e.text
386 if parts.tag.endswith(
'published'):
387 item[
'published_parsed'] = parts.text
388 if parts.tag.endswith(
'description'):
389 item[
'media_description'] = parts.text
394 for key
in item.keys():
395 if item[key] ==
None:
401 if not 'id' in item.keys():
406 video_details = self.
videoDetails(item[
'id'], urllib.quote(item[
'title'].encode(
"utf-8")))
407 except MtvUrlError, msg:
409 except MtvVideoDetailError, msg:
410 sys.stderr.write(self.
error_messages[
'MtvVideoDetailError'] % msg)
412 sys.stderr.write(
u"! Error: Unknown error while retrieving a Video's meta data. Skipping video.' (%s)\nError(%s)\n" % (title, e))
415 for key
in video_details.keys():
416 item[key] = video_details[key]
418 item[
'language'] =
u'' 419 for key
in item.keys():
422 if item[key][0].has_key(
'language'):
423 if item[key][0][
'language'] !=
None:
424 item[
'language'] = item[key][0][
'language']
425 if key ==
'published_parsed':
427 pub_time = time.strptime(item[key].strip(),
"%Y-%m-%dT%H:%M:%SZ")
428 item[key] = time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', pub_time)
430 if key ==
'media_description' or key ==
'title':
434 item[key] = item[key].replace(
u'|',
u'-')
438 item[key] = item[key].replace(
'"\n',
' ').strip()
439 elements_final.append(item)
441 if not len(elements_final):
442 raise MtvVideoNotFound(
u"No MTV Video matches found for search value (%s)" % title)
444 return elements_final
449 '''Using the passed URL retrieve the video meta data details 450 return a dictionary of video metadata details 453 if self.
config[
'debug_enabled']:
459 except Exception, errormsg:
460 raise MtvUrlError(self.
error_messages[
'MtvUrlError'] % (url, errormsg))
463 raise MtvVideoDetailError(
u'1-No Video meta data for (%s)' % url)
468 if e.tag.endswith(
u'content')
and e.text ==
None:
469 index = e.get(
'url').rindex(
u':')
470 metadata[
'video'] = self.
mtvHtmlPath % (title, e.get(
'url')[index+1:])
473 metadata[
'duration'] = e.get(
'duration')
474 if e.tag.endswith(
u'player'):
475 metadata[
'link'] = e.get(
'url')
476 if e.tag.endswith(
u'thumbnail'):
477 if cur_size ==
False:
479 height = e.get(
'height')
480 width = e.get(
'width')
481 if int(width) > cur_size:
482 metadata[
'thumbnail'] = e.get(
'url')
483 cur_size = int(width)
484 if int(width) >= 200:
488 if not len(metadata):
489 raise MtvVideoDetailError(
u'2-No Video meta data for (%s)' % url)
491 if not metadata.has_key(
'video'):
492 metadata[
'video'] = metadata[
'link']
493 metadata[
'duration'] =
u'' 495 metadata[
'link'] = metadata[
'video']
502 """Common name for a video search. Used to interface with MythTV plugin NetVision 506 self.
config[
u'urls'][
u'video.search'] =
"http://api.mtvnservices.com/1/video/search/?term=%s&start-index=%s&max-results=%s" 508 self.
config[
u'urls'][
u'video.search'] =
"http://api.mtvnservices.com/1/artist/search/?term=%s&start-index=%s&max-results=%s" 510 sys.stderr.write(
u"! Error: MtvInvalidSearchType - The grabber name (%s) is invalid \n" % self.
grabber_title)
520 startindex = (int(pagenumber) -1) * self.page_limit + 1
522 data = self.
searchTitle(title, startindex, self.page_limit)
523 except MtvVideoNotFound, msg:
524 sys.stderr.write(
u"%s\n" % msg)
526 except MtvUrlError, msg:
527 sys.stderr.write(
u'%s\n' % msg)
529 except MtvHttpError, msg:
532 except MtvRssError, msg:
536 sys.stderr.write(
u"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
548 if key
in match.keys():
552 items.append(item_data)
555 channel = {
'channel_title':
u'MTV',
'channel_link':
u'http://www.mtv.com',
'channel_description':
u"Visit MTV (Music Television) for TV shows, music videos, celebrity photos, news.",
'channel_numresults': 0,
'channel_returned': 1,
u'channel_startindex': 0}
557 if len(items) == self.page_limit:
558 channel[
'channel_numresults'] = self.page_limit * int(pagenumber) + 1
559 elif len(items) < self.page_limit:
560 channel[
'channel_numresults'] = self.page_limit * (int(pagenumber)-1) + len(items)
562 channel[
'channel_numresults'] = self.page_limit * int(pagenumber)
563 channel[
'channel_startindex'] = self.page_limit * int(pagenumber)
564 channel[
'channel_returned'] = len(items)
567 return [[channel, items]]
573 '''Gather the MTV Genres/Artists/...etc then get a max page of videos meta data in each of them 574 return array of directories and their video metadata 577 self.
channel = {
'channel_title':
u'MTV',
'channel_link':
u'http://www.mtv.com',
'channel_description':
u"Visit MTV (Music Television) for TV shows, music videos, celebrity photos, news.",
'channel_numresults': 0,
'channel_returned': 1,
u'channel_startindex': 0}
579 if self.
config[
'debug_enabled']:
580 print self.
config[
u'urls']
586 if 'max-results' in self.
tree_customize[key][
'__default__'].keys():
597 return [[self.
channel, dictionaries]]
601 '''Form a URL to search for videos 613 for ky
in additions.keys():
614 if ky.startswith(
'add_'):
615 addition+=
u'/%s' % additions[ky]
617 addition+=
u'&%s=%s' % (ky, additions[ky])
618 index = URL.find(
'%')
620 return (URL+addition)
622 return (URL+addition) % self.feed
627 '''Parse a list made of genres/artists ... etc lists and retrieve video meta data 628 return a dictionary of directory names and categories video metadata 630 for sets
in dir_dict:
631 if not isinstance(sets[1], list):
638 dictionaries.append([
'',
u''])
641 for self.feed
in sets[1]:
642 if self.
config[
u'urls'][
u'tree.view'][self.
tree_key].has_key(
'__all__'):
643 URL = self.
config[
u'urls'][
u'tree.view'][self.
tree_key][
'__all__']
645 URL = self.
config[
u'urls'][
u'tree.view'][self.
tree_key][self.feed]
646 temp_dictionary = self.
config[
'item_parser'][URL[1]](self.
makeURL(URL[0]), temp_dictionary)
647 if len(temp_dictionary):
653 for element
in temp_dictionary:
654 dictionaries.append(element)
656 dictionaries.append([
'',
u''])
662 '''Get the video metadata for url search 663 return the video dictionary of directories and their video mata data 665 initial_length = len(dictionaries)
667 if self.
config[
'debug_enabled']:
668 print "Category URL:" 674 except Exception, errormsg:
675 sys.stderr.write(self.
error_messages[
'MtvUrlError'] % (url, errormsg))
679 sys.stderr.write(
u'1-No Videos for (%s)\n' % self.feed)
682 dictionary_first =
False 683 for elements
in etree:
684 if elements.tag.endswith(
u'totalResults'):
685 self.
channel[
'channel_numresults'] += int(elements.text)
686 self.
channel[
'channel_startindex'] = self.page_limit
687 self.
channel[
'channel_returned'] = self.page_limit
690 if not elements.tag.endswith(
u'entry'):
696 metadata[
'language'] = self.
config[
'language']
698 if e.tag.endswith(
u'title'):
702 metadata[
'title'] =
u'' 704 if e.tag ==
u'content':
708 metadata[
'media_description'] =
u'' 710 if e.tag.endswith(
u'published'):
712 pub_time = time.strptime(e.text.strip(),
"%Y-%m-%dT%H:%M:%SZ")
713 metadata[
'published_parsed'] = time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', pub_time)
715 metadata[
'published_parsed'] =
u'' 717 if e.tag.endswith(
u'content')
and e.text ==
None:
718 metadata[
'video'] = self.
ampReplace(e.get(
'url'))
719 metadata[
'duration'] = e.get(
'duration')
721 if e.tag.endswith(
u'player'):
722 metadata[
'link'] = self.
ampReplace(e.get(
'url'))
724 if e.tag.endswith(
u'thumbnail'):
725 if cur_size ==
False:
727 height = e.get(
'height')
728 width = e.get(
'width')
729 if int(width) > cur_size:
730 metadata[
'thumbnail'] = self.
ampReplace(e.get(
'url'))
731 cur_size = int(width)
732 if int(width) >= 200:
735 if e.tag.endswith(
u'author'):
737 if a.tag.endswith(
u'name'):
741 metadata[
'media_credit'] =
u'' 745 if not len(metadata):
746 raise MtvVideoDetailError(
u'2-No Video meta data for (%s)' % url)
748 if not metadata.has_key(
'video')
and not metadata.has_key(
'link'):
751 if not metadata.has_key(
'video'):
752 metadata[
'video'] = metadata[
'link']
754 index = metadata[
'video'].rindex(
u':')
755 metadata[
'video'] = self.
mtvHtmlPath % (urllib.quote(metadata[
'title'].encode(
"utf-8")), metadata[
'video'][index+1:])
756 metadata[
'link'] = metadata[
'video']
760 if not dictionary_first:
762 dictionary_first =
True 766 if not metadata.has_key(key):
770 dictionaries.append(final_item)
772 if initial_length < len(dictionaries):
773 dictionaries.append([
'',
u''])
def ampReplace(self, text)
def searchForVideos(self, title, pagenumber)
def getVideosForURL(self, url, dictionaries)
def __init__(self, outstream, encoding=None)
def __getattr__(self, attr)
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
def displayTreeView(self)
def getVideos(self, dir_dict, dictionaries)
def searchTitle(self, title, pagenumber, pagelen)
End of Utility functions.
def videoDetails(self, url, title=u'')
def setTreeViewIcon(self, dir_icon=None)
def massageDescription(self, text)
Start - Utility functions.