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.request, urllib.parse, urllib.error, urllib.request, urllib.error, urllib.parse
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)
52 """Wraps a stream with an encoder"""
61 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
62 if isinstance(obj, str):
64 self.
out.buffer.write(obj)
67 """Delegate everything but write to the stream"""
68 return getattr(self.
out, attr)
70 if isinstance(sys.stdout, io.TextIOWrapper):
76 """Deals with retrieval of XML files from API
83 urlhandle = urllib.request.urlopen(url)
84 except IOError
as errormsg:
86 return urlhandle.read()
91 et = ElementTree.fromstring(xml)
92 except SyntaxError
as errormsg:
98 """Main interface to http://www.mtv.com/
99 This is done to support a common naming framework for all python Netvision plugins no matter their site
102 Supports search methods
103 The apikey is a not required to access http://www.mtv.com/
109 select_first = False,
113 search_all_languages = False,
115 """apikey (str/unicode):
116 Specify the target site API key. Applications need their own key in some cases
119 When True, the returned meta data is being returned has the key and values massaged to match MythTV
120 When False, the returned meta data is being returned matches what target site returned
122 interactive (True/False): (This option is not supported by all target site apis)
123 When True, uses built-in console UI is used to select the correct show.
124 When False, the first search result is used.
126 select_first (True/False): (This option is not supported currently implemented in any grabbers)
127 Automatically selects the first series search result (rather
128 than showing the user a list of more than one series).
129 Is overridden by interactive = False, or specifying a custom_ui
132 shows verbose debugging information
134 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
135 A callable subclass of interactive class (overrides interactive option)
137 language (2 character language abbreviation): (This option is not supported by all target site apis)
138 The language of the returned data. Is also the language search
139 uses. Default is "en" (English). For full list, run..
141 search_all_languages (True/False): (This option is not supported by all target site apis)
142 By default, a Netvision grabber will only search in the language specified using
143 the language option. When this is True, it will search for the
149 if apikey
is not None:
150 self.
config[
'apikey'] = apikey
154 self.
config[
'debug_enabled'] = debug
159 self.
config[
'custom_ui'] = custom_ui
163 self.
config[
'select_first'] = select_first
165 self.
config[
'search_all_languages'] = search_all_languages
168 self.
config[
'language'] =
"en"
170 self.
error_messages = {
'MtvUrlError':
"! Error: The URL (%s) cause the exception error (%s)\n",
'MtvHttpError':
"! Error: An HTTP communications error with MTV was raised (%s)\n",
'MtvRssError':
"! Error: Invalid RSS metadata\nwas received from MTV error (%s). Skipping item.\n",
'MtvVideoNotFound':
"! Error: Video search with MTV did not return any results (%s)\n",
'MtvVideoDetailError':
"! Error: Invalid Video metadata detail\nwas received from MTV error (%s). Skipping item.\n", }
173 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'}]
177 self.
config[
'image_extentions'] = [
"png",
"jpg",
"bmp"]
181 self.
config[
'item_parser'] = {}
187 '__all__': [
'http://api.mtvnservices.com/1/genre/%s/videos/?',
'main'],
190 '__all__': [
'http://api.mtvnservices.com/1/genre/%s/videos/?',
'main'],
196 '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', ]],
198 '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', ]],
204 yr = d1 - timedelta(weeks=52)
205 mts = d1 - timedelta(days=93)
206 last_3_months =
'%s-%s' % (mts.strftime(
'%m%d%Y'), d1.strftime(
'%m%d%Y'))
207 last_year =
'%s-%s' % (yr.strftime(
'%m%d%Y'), d1.strftime(
'%m%d%Y'))
212 '__default__': {
'max-results':
'20',
'start-index':
'1',
'date': last_3_months,
'sort':
'date_descending'},
216 '__default__': {
'max-results':
'20',
'start-index':
'1',
'sort':
'date_descending'},
218 'rock': {
'date': last_year, },
219 'R&B': {
'date': last_year, },
220 'country': {
'date': last_year, },
221 'hip_hop': {
'date': last_year, },
226 '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'
228 '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',
233 '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',
235 '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',
239 self.
mtvHtmlPath =
'file://'+os.path.dirname( os.path.realpath( __file__ )).replace(
'/nv_python_libs/mtv',
'/nv_python_libs/configs/HTML/mtv.html?title=%s&videocode=%s')
243 self.
channel_icon =
'%SHAREDIR%/mythnetvision/icons/mtv.png'
253 '''Removes HTML markup from a text string.
254 @param text The HTML source.
255 @return The plain text. If the HTML source contains non-ASCII
256 entities or character references, this is a Unicode string.
264 if text[:3] ==
"&#x":
265 return chr(int(text[3:-1], 16))
267 return chr(int(text[2:-1]))
270 elif text[:1] ==
"&":
272 entity = html.entities.entitydefs.get(text[1:-1])
274 if entity[:2] ==
"&#":
276 return chr(int(entity[2:-1]))
280 return str(entity,
"iso-8859-1")
282 return self.
ampReplace(re.sub(
r"(?s)<[^>]*>|&#?\w+;", fixup, self.
textUtf8(text))).replace(
'\n',
' ')
287 """Setups a logger using the logging module, returns a log object
289 logger = logging.getLogger(self.
log_name)
290 formatter = logging.Formatter(
'%(asctime)s) %(levelname)s %(message)s')
292 hdlr = logging.StreamHandler(sys.stdout)
294 hdlr.setFormatter(formatter)
295 logger.addHandler(hdlr)
297 if self.
config[
'debug_enabled']:
298 logger.setLevel(logging.DEBUG)
300 logger.setLevel(logging.WARNING)
309 return str(text,
'utf8')
310 except UnicodeDecodeError:
312 except (UnicodeEncodeError, TypeError):
318 '''Replace all "&" characters with "&"
321 return text.replace(
'&',
'~~~~~').replace(
'&',
'&').replace(
'~~~~~',
'&')
326 '''Check if there is a specific generic tree view icon. If not default to the channel icon.
327 return self.tree_dir_icon
338 self.
tree_dir_icon =
'%%SHAREDIR%%/mythnetvision/icons/%s.png' % (dir_icon, )
350 '''Key word video search of the MTV web site
351 return an array of matching item dictionaries
354 url = self.
config[
'urls'][
'video.search'] % (urllib.parse.quote_plus(title.encode(
"utf-8")), pagenumber , pagelen,)
355 if self.
config[
'debug_enabled']:
361 except Exception
as errormsg:
365 raise MtvVideoNotFound(
"No MTV Video matches found for search value (%s)" % title)
369 if not entry.tag.endswith(
'entry'):
373 if parts.tag.endswith(
'id'):
374 item[
'id'] = parts.text
375 if parts.tag.endswith(
'title'):
376 item[
'title'] = parts.text
377 if parts.tag.endswith(
'author'):
379 if e.tag.endswith(
'name'):
380 item[
'media_credit'] = e.text
382 if parts.tag.endswith(
'published'):
383 item[
'published_parsed'] = parts.text
384 if parts.tag.endswith(
'description'):
385 item[
'media_description'] = parts.text
390 for key
in list(item.keys()):
391 if item[key]
is None:
397 if not 'id' in list(item.keys()):
402 video_details = self.
videoDetails(item[
'id'], urllib.parse.quote(item[
'title'].encode(
"utf-8")))
403 except MtvUrlError
as msg:
405 except MtvVideoDetailError
as msg:
406 sys.stderr.write(self.
error_messages[
'MtvVideoDetailError'] % msg)
407 except Exception
as e:
408 sys.stderr.write(
"! Error: Unknown error while retrieving a Video's meta data. Skipping video.' (%s)\nError(%s)\n" % (title, e))
411 for key
in list(video_details.keys()):
412 item[key] = video_details[key]
414 item[
'language'] =
''
415 for key
in list(item.keys()):
418 if 'language' in item[key][0]:
419 if item[key][0][
'language']
is not None:
420 item[
'language'] = item[key][0][
'language']
421 if key ==
'published_parsed':
423 pub_time = time.strptime(item[key].strip(),
"%Y-%m-%dT%H:%M:%SZ")
424 item[key] = time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', pub_time)
426 if key ==
'media_description' or key ==
'title':
430 item[key] = item[key].replace(
'|',
'-')
434 item[key] = item[key].replace(
'"\n',
' ').strip()
435 elements_final.append(item)
437 if not len(elements_final):
438 raise MtvVideoNotFound(
"No MTV Video matches found for search value (%s)" % title)
440 return elements_final
445 '''Using the passed URL retrieve the video meta data details
446 return a dictionary of video metadata details
449 if self.
config[
'debug_enabled']:
455 except Exception
as errormsg:
464 if e.tag.endswith(
'content')
and e.text
is None:
465 index = e.get(
'url').rindex(
':')
466 metadata[
'video'] = self.
mtvHtmlPath % (title, e.get(
'url')[index+1:])
469 metadata[
'duration'] = e.get(
'duration')
470 if e.tag.endswith(
'player'):
471 metadata[
'link'] = e.get(
'url')
472 if e.tag.endswith(
'thumbnail'):
473 if cur_size ==
False:
475 height = e.get(
'height')
476 width = e.get(
'width')
477 if int(width) > cur_size:
478 metadata[
'thumbnail'] = e.get(
'url')
479 cur_size = int(width)
480 if int(width) >= 200:
484 if not len(metadata):
487 if 'video' not in metadata:
488 metadata[
'video'] = metadata[
'link']
489 metadata[
'duration'] =
''
491 metadata[
'link'] = metadata[
'video']
498 """Common name for a video search. Used to interface with MythTV plugin NetVision
502 self.
config[
'urls'][
'video.search'] =
"http://api.mtvnservices.com/1/video/search/?term=%s&start-index=%s&max-results=%s"
504 self.
config[
'urls'][
'video.search'] =
"http://api.mtvnservices.com/1/artist/search/?term=%s&start-index=%s&max-results=%s"
506 sys.stderr.write(
"! Error: MtvInvalidSearchType - The grabber name (%s) is invalid \n" % self.
grabber_title)
516 startindex = (int(pagenumber) -1) * self.page_limit + 1
518 data = self.
searchTitle(title, startindex, self.page_limit)
519 except MtvVideoNotFound
as msg:
520 sys.stderr.write(
"%s\n" % msg)
522 except MtvUrlError
as msg:
523 sys.stderr.write(
'%s\n' % msg)
525 except MtvHttpError
as msg:
528 except MtvRssError
as msg:
531 except Exception
as e:
532 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
544 if key
in list(match.keys()):
548 items.append(item_data)
551 channel = {
'channel_title':
'MTV',
'channel_link':
'http://www.mtv.com',
'channel_description':
"Visit MTV (Music Television) for TV shows, music videos, celebrity photos, news.",
'channel_numresults': 0,
'channel_returned': 1,
'channel_startindex': 0}
553 if len(items) == self.page_limit:
554 channel[
'channel_numresults'] = self.page_limit * int(pagenumber) + 1
555 elif len(items) < self.page_limit:
556 channel[
'channel_numresults'] = self.page_limit * (int(pagenumber)-1) + len(items)
558 channel[
'channel_numresults'] = self.page_limit * int(pagenumber)
559 channel[
'channel_startindex'] = self.page_limit * int(pagenumber)
560 channel[
'channel_returned'] = len(items)
563 return [[channel, items]]
569 '''Gather the MTV Genres/Artists/...etc then get a max page of videos meta data in each of them
570 return array of directories and their video metadata
573 self.
channel = {
'channel_title':
'MTV',
'channel_link':
'http://www.mtv.com',
'channel_description':
"Visit MTV (Music Television) for TV shows, music videos, celebrity photos, news.",
'channel_numresults': 0,
'channel_returned': 1,
'channel_startindex': 0}
575 if self.
config[
'debug_enabled']:
582 if 'max-results' in list(self.
tree_customize[key][
'__default__'].keys()):
583 self.
tree_customize[key][
'__default__'][
'max-results'] = str(self.page_limit)
593 return [[self.
channel, dictionaries]]
597 '''Form a URL to search for videos
609 for ky
in list(additions.keys()):
610 if ky.startswith(
'add_'):
611 addition+=
'/%s' % additions[ky]
613 addition+=
'&%s=%s' % (ky, additions[ky])
614 index = URL.find(
'%')
616 return (URL+addition)
618 return (URL+addition) % self.feed
623 '''Parse a list made of genres/artists ... etc lists and retrieve video meta data
624 return a dictionary of directory names and categories video metadata
626 for sets
in dir_dict:
627 if not isinstance(sets[1], list):
634 dictionaries.append([
'',
''])
637 for self.feed
in sets[1]:
642 temp_dictionary = self.
config[
'item_parser'][URL[1]](self.
makeURL(URL[0]), temp_dictionary)
643 if len(temp_dictionary):
649 for element
in temp_dictionary:
650 dictionaries.append(element)
652 dictionaries.append([
'',
''])
658 '''Get the video metadata for url search
659 return the video dictionary of directories and their video mata data
661 initial_length = len(dictionaries)
663 if self.
config[
'debug_enabled']:
664 print(
"Category URL:")
670 except Exception
as errormsg:
671 sys.stderr.write(self.
error_messages[
'MtvUrlError'] % (url, errormsg))
675 sys.stderr.write(
'1-No Videos for (%s)\n' % self.feed)
678 dictionary_first =
False
679 for elements
in etree:
680 if elements.tag.endswith(
'totalResults'):
681 self.
channel[
'channel_numresults'] += int(elements.text)
682 self.
channel[
'channel_startindex'] = self.page_limit
683 self.
channel[
'channel_returned'] = self.page_limit
686 if not elements.tag.endswith(
'entry'):
692 metadata[
'language'] = self.
config[
'language']
694 if e.tag.endswith(
'title'):
695 if e.text
is not None:
698 metadata[
'title'] =
''
700 if e.tag ==
'content':
701 if e.text
is not None:
704 metadata[
'media_description'] =
''
706 if e.tag.endswith(
'published'):
707 if e.text
is not None:
708 pub_time = time.strptime(e.text.strip(),
"%Y-%m-%dT%H:%M:%SZ")
709 metadata[
'published_parsed'] = time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', pub_time)
711 metadata[
'published_parsed'] =
''
713 if e.tag.endswith(
'content')
and e.text
is None:
714 metadata[
'video'] = self.
ampReplace(e.get(
'url'))
715 metadata[
'duration'] = e.get(
'duration')
717 if e.tag.endswith(
'player'):
718 metadata[
'link'] = self.
ampReplace(e.get(
'url'))
720 if e.tag.endswith(
'thumbnail'):
721 if cur_size ==
False:
723 height = e.get(
'height')
724 width = e.get(
'width')
725 if int(width) > cur_size:
726 metadata[
'thumbnail'] = self.
ampReplace(e.get(
'url'))
727 cur_size = int(width)
728 if int(width) >= 200:
731 if e.tag.endswith(
'author'):
733 if a.tag.endswith(
'name'):
737 metadata[
'media_credit'] =
''
741 if not len(metadata):
744 if 'video' not in metadata
and 'link' not in metadata:
747 if 'video' not in metadata:
748 metadata[
'video'] = metadata[
'link']
750 index = metadata[
'video'].rindex(
':')
751 metadata[
'video'] = self.
mtvHtmlPath % (urllib.parse.quote(metadata[
'title'].encode(
"utf-8")), metadata[
'video'][index+1:])
752 metadata[
'link'] = metadata[
'video']
756 if not dictionary_first:
758 dictionary_first =
True
762 if key
not in metadata:
766 dictionaries.append(final_item)
768 if initial_length < len(dictionaries):
769 dictionaries.append([
'',
''])