14__title__ =
"youtube_api - Simple-to-use Python interface to the youtube API (http://developer.youtubenservices.com/docs)"
15__author__=
"R.D. Vaughan"
17This python script is intended to perform a variety of utility functions to search and access text
18meta data, video and image URLs from youtube. These routines are based on the api. Specifications
19for this api are published at http://developer.youtubenservices.com/docs
39import os, struct, sys, re, time, shutil
43from MythTV
import MythXML
44from ..common
import common_api
46from .youtube_exceptions
import (YouTubeUrlError, YouTubeHttpError, YouTubeRssError, YouTubeVideoNotFound, YouTubeInvalidSearchType, YouTubeXmlError, YouTubeVideoDetailError, YouTubeCategoryNotFound)
47from .youtube_data
import getData
52 sys.stderr.write(
"The module aniso8601 could not be imported, duration "
53 "parsing will be disabled\n")
58 """Deals with retrieval of JSON data from API
65 urlhandle = urllib.request.urlopen(self.
url)
66 return json.load(urlhandle)
67 except IOError
as errormsg:
72 """Main interface to http://www.youtube.com/
73 This is done to support a common naming framework
for all python Netvision plugins no matter their site
76 Supports search methods
86 search_all_languages = False,
88 """apikey (str/unicode):
89 Specify the target site API key. Applications need their own key in some cases
92 When
True, the returned meta data
is being returned has the key
and values massaged to match MythTV
93 When
False, the returned meta data
is being returned matches what target site returned
95 interactive (
True/
False): (This option
is not supported by all target site apis)
96 When
True, uses built-
in console UI
is used to select the correct show.
97 When
False, the first search result
is used.
99 select_first (
True/
False): (This option
is not supported currently implemented
in any grabbers)
100 Automatically selects the first series search result (rather
101 than showing the user a list of more than one series).
102 Is overridden by interactive =
False,
or specifying a custom_ui
105 shows verbose debugging information
107 custom_ui (xx_ui.BaseUI subclass): (This option
is not supported currently implemented
in any grabbers)
108 A callable subclass of interactive
class (overrides interactive option)
110 language (2 character language abbreviation): (This option
is not supported by all target site apis)
111 The language of the returned data. Is also the language search
112 uses. Default
is "en" (English). For full list, run..
114 search_all_languages (
True/
False): (This option
is not supported by all target site apis)
115 By default, a Netvision grabber will only search
in the language specified using
116 the language option. When this
is True, it will search
for the
124 self.config['debug_enabled'] = debug
129 self.
config[
'custom_ui'] = custom_ui
131 self.
config[
'interactive'] = interactive
133 self.
config[
'select_first'] = select_first
135 self.
config[
'search_all_languages'] = search_all_languages
138 {
'YouTubeUrlError':
"! Error: The URL (%s) cause the exception error (%s)\n",
139 'YouTubeHttpError':
"! Error: An HTTP communications error with YouTube was raised (%s)\n",
140 'YouTubeRssError':
"! Error: Invalid RSS meta data\nwas received from YouTube error (%s). Skipping item.\n",
141 'YouTubeVideoNotFound':
"! Error: Video search with YouTube did not return any results (%s)\n",
142 'YouTubeVideoDetailError':
"! Error: Invalid Video meta data detail\nwas received from YouTube error (%s). Skipping item.\n", }
146 [{
'channel_title':
'channel_title',
147 'channel_link':
'channel_link',
148 'channel_description':
'channel_description',
149 'channel_numresults':
'channel_numresults',
150 'channel_returned':
'channel_returned',
151 'channel_startindex':
'channel_startindex'},
152 {
'title':
'item_title',
153 'author':
'item_author',
154 'published_parsed':
'item_pubdate',
155 'media_description':
'item_description',
156 'video':
'item_link',
157 'thumbnail':
'item_thumbnail',
159 'duration':
'item_duration',
160 'rating':
'item_rating',
161 'item_width':
'item_width',
162 'item_height':
'item_height',
163 'language':
'item_lang'}]
167 self.
config[
'language'] = language
169 self.
config[
'language'] =
''
175 if region
is not None and region.text:
176 self.
config[
'region'] = region.text
178 self.
config[
'region'] =
'us'
183 if apikey
is not None and apikey.text:
187 'Film & Animation':
'directories/topics/movies',
188 'Movies':
'directories/topics/movies',
189 'Trailers':
'directories/topics/movies',
190 'Sports':
'directories/topics/sports',
191 'News & Politics':
'directories/topics/news',
192 'Science & Technology':
'directories/topics/technology',
193 'Education':
'directories/topics/education',
194 'Howto & Style':
'directories/topics/howto',
195 'Music':
'directories/topics/music',
196 'Gaming':
'directories/topics/games',
197 'Entertainment':
'directories/topics/entertainment',
198 'Autos & Vehicles':
'directories/topics/automotive',
199 'Pets & Animals':
'directories/topics/animals',
200 'Travel & Events':
'directories/topics/travel',
201 'People & Blogs':
'directories/topics/people',
209 userPreferenceFilePath = os.path.expanduser(userPreferenceFilePath)
212 if not os.path.isfile(userPreferenceFilePath):
214 prefDir = os.path.dirname(userPreferenceFilePath)
215 if not os.path.isdir(prefDir):
218 fileName = os.path.basename(userPreferenceFilePath)
219 defaultConfig =
'%s/nv_python_libs/configs/XML/defaultUserPrefs/%s' \
220 % (baseProcessingDir, fileName)
221 shutil.copy2(defaultConfig, userPreferenceFilePath)
224 url =
'file://%s' % userPreferenceFilePath
225 if self.
config[
'debug_enabled']:
230 except Exception
as e:
231 raise Exception(url, e)
240 '''Get longitude and latitiude to find videos relative to your location. Up to three different
241 servers will be tried before giving up.
242 return a dictionary e.g.
243 {
'Latitude':
'43.6667',
'Country':
'Canada',
'Longitude':
'-79.4167',
'City':
'Toronto'}
244 return an empty dictionary
if there were any errors
245 Code found at: http://blog.suinova.com/2009/04/
from-ip-to-geolocation-country-city.html
248 '''Find the external IP address of this computer.
250 url = urllib.request.URLopener()
252 resp = url.open(
'http://www.whatismyip.com/automation/n09230945.asp')
264 gs = urllib.request.urlopen(
'http://blogama.org/ip_query.php?ip=%s&output=xml' % ip)
268 gs = urllib.request.urlopen(
'http://www.seomoz.org/ip2location/look.php?ip=%s' % ip)
272 gs = urllib.request.urlopen(
'http://api.hostip.info/?ip=%s' % ip)
275 logging.error(
'GeoIP servers not available')
278 if txt.find(
'<Response>') > 0:
279 countrys = re.findall(
r'<CountryName>([\w ]+)<',txt)[0]
280 citys = re.findall(
r'<City>([\w ]+)<',txt)[0]
281 lats,lons = re.findall(
r'<Latitude>([\d\-\.]+)</Latitude>\s*<Longitude>([\d\-\.]+)<',txt)[0]
282 elif txt.find(
'GLatLng') > 0:
283 citys,countrys = re.findall(
r'<br />\s*([^<]+)<br />\s*([^<]+)<',txt)[0]
284 lats,lons = re.findall(
r'LatLng\(([-\d\.]+),([-\d\.]+)',txt)[0]
285 elif txt.find(
'<gml:coordinates>') > 0:
286 citys = re.findall(
r'<Hostip>\s*<gml:name>(\w+)</gml:name>',txt)[0]
287 countrys = re.findall(
r'<countryName>([\w ,\.]+)</countryName>',txt)[0]
288 lats,lons = re.findall(
r'gml:coordinates>([-\d\.]+),([-\d\.]+)<',txt)[0]
290 logging.error(
'error parsing IP result %s'%txt)
292 return {
'Country':countrys,
'City':citys,
'Latitude':lats,
'Longitude':lons}
294 logging.error(
'Error parsing IP result %s'%txt)
300 '''Removes HTML markup from a text string.
301 @param text The HTML source.
302 @return The plain text. If the HTML source contains non-ASCII
303 entities
or character references, this
is a Unicode string.
311 if text[:3] ==
"&#x":
312 return chr(int(text[3:-1], 16))
314 return chr(int(text[2:-1]))
317 elif text[:1] ==
"&":
319 entity = html.entities.entitydefs.get(text[1:-1])
321 if entity[:2] ==
"&#":
323 return chr(int(entity[2:-1]))
327 return str(entity,
"iso-8859-1")
329 return self.
common.ampReplace(re.sub(
r"(?s)<[^>]*>|&#?\w+;", fixup, self.
common.textUtf8(text)))
333 """Setups a logger using the logging module, returns a log object
335 logger = logging.getLogger(self.log_name)
336 formatter = logging.Formatter('%(asctime)s) %(levelname)s %(message)s')
338 hdlr = logging.StreamHandler(sys.stdout)
340 hdlr.setFormatter(formatter)
341 logger.addHandler(hdlr)
343 if self.
config[
'debug_enabled']:
344 logger.setLevel(logging.DEBUG)
346 logger.setLevel(logging.WARNING)
351 '''Check if there is a specific generic tree view icon. If not default to the channel icon.
361 self.
tree_dir_icon =
'%%SHAREDIR%%/mythnetvision/icons/%s.png' % (dir_icon, )
373 '''Key word video search of the YouTube web site
374 return an array of matching item dictionaries
386 self.
channel[
'channel_numresults'] = int(result[
'pageInfo'][
'totalResults'])
387 if 'nextPageToken' in result:
388 self.
channel[
'nextpagetoken'] = result[
'nextPageToken']
389 if 'prevPageToken' in result:
390 self.
channel[
'prevpagetoken'] = result[
'prevPageToken']
392 ids = [entry[
'id'][
'videoId']
for entry
in result[
'items']]
395 data = [self.
parseDetails(entry)
for entry
in result[
'items']]
404 url = (
'https://www.googleapis.com/youtube/v3/search?part=snippet&' + \
405 'type=video&q=%s&maxResults=%s&order=relevance&' + \
406 'videoEmbeddable=true&key=%s&pageToken=%s') % \
407 (urllib.parse.quote_plus(title.encode(
"utf-8")), pagelen, self.
apikey,
409 if self.
config[
'debug_enabled']:
415 except Exception
as errormsg:
419 url =
'https://www.googleapis.com/youtube/v3/videos?part=id,snippet,' + \
420 'contentDetails&key=%s&id=%s' % (self.
apikey,
",".join(ids))
423 except Exception
as errormsg:
429 item[
'id'] = entry[
'id']
431 self.
mythxml.getInternetContentUrl(
"nv_python_libs/configs/HTML/youtube.html", \
433 item[
'link'] = item[
'video']
434 snippet = entry[
'snippet']
435 item[
'title'] = snippet[
'title']
436 item[
'media_description'] = snippet[
'description']
437 item[
'thumbnail'] = snippet[
'thumbnails'][
'high'][
'url']
438 item[
'author'] = snippet[
'channelTitle']
439 item[
'published_parsed'] = snippet[
'publishedAt']
442 duration = aniso8601.parse_duration(entry[
'contentDetails'][
'duration'])
443 item[
'duration'] = duration.days * 24 * 3600 + duration.seconds
447 for key
in list(item.keys()):
449 if item[key]
is None:
451 elif key ==
'published_parsed':
453 pub_time = time.strptime(item[key].strip(),
"%Y-%m-%dT%H:%M:%SZ")
454 item[key] = time.strftime(
'%a, %d %b %Y %H:%M:%S GMT', pub_time)
455 elif key ==
'media_description' or key ==
'title':
459 item[key] = item[key].replace(
'|',
'-')
462 item[key] = self.
common.ampReplace(item[key].replace(
'"\n',
' ').strip())
469 """Common name for a video search. Used to interface with MythTV plugin NetVision
473 'channel_title':
'YouTube',
474 'channel_link':
'http://www.youtube.com/',
475 'channel_description':
"Share your videos with friends, family, and the world.",
476 'channel_numresults': 0,
477 'channel_returned': 1,
478 'channel_startindex': 0}
486 data = self.
searchTitle(title, pagenumber, self.page_limit)
487 except YouTubeVideoNotFound
as msg:
488 sys.stderr.write(
"%s\n" % msg)
490 except YouTubeUrlError
as msg:
491 sys.stderr.write(
'%s\n' % msg)
493 except YouTubeHttpError
as msg:
496 except YouTubeRssError
as msg:
499 except Exception
as e:
500 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
509 self.
channel[
'channel_returned'] = len(items)
519 if key
in list(item.keys()):
526 '''Gather the Youtube categories/feeds/...etc then get a max page of videos meta data in each of them
527 return array of directories
and their video metadata
531 'channel_title':
'YouTube',
532 'channel_link':
'http://www.youtube.com/',
533 'channel_description':
"Share your videos with friends, family, and the world.",
534 'channel_numresults': 0,
535 'channel_returned': 1,
536 'channel_startindex': 0}
543 for category
in etree[
'items']:
544 snippet = category[
'snippet']
545 feed_names[snippet[
'title']] = self.
common.ampReplace(category[
'id'])
551 for category
in feed_names:
555 return [[self.
channel, dictionaries]]
560 url =
'https://www.googleapis.com/youtube/v3/videoCategories?' + \
561 'part=snippet®ionCode=%s&key=%s' % \
564 except Exception
as errormsg:
568 '''Parse a list made of category lists and retrieve video meta data
569 return a dictionary of directory names
and categories video metadata
571 url = 'https://www.googleapis.com/youtube/v3/videos?part=snippet&' + \
572 'chart=mostPopular&videoCategoryId=%s&maxResults=%s&key=%s' % \
573 (categoryId, self.page_limit, self.
apikey)
576 for element
in temp_dictionary:
577 dictionaries.append(element)
582 '''Get the video metadata for url search
583 return the video dictionary of directories
and their video mata data
585 initial_length = len(dictionaries)
587 if self.
config[
'debug_enabled']:
588 print(
"Category URL:")
594 except Exception
as errormsg:
595 sys.stderr.write(self.
error_messages[
'YouTubeUrlError'] % (url, errormsg))
599 sys.stderr.write(
'1-No Videos for (%s)\n' % self.feed)
602 if 'pageInfo' not in result
or 'items' not in result:
605 dictionary_first =
False
606 self.
channel[
'channel_numresults'] += int(result[
'pageInfo'][
'totalResults'])
607 self.
channel[
'channel_startindex'] = self.page_limit
608 self.
channel[
'channel_returned'] = len(result[
'items'])
609 for entry
in result[
'items']:
612 if not dictionary_first:
615 dictionary_first =
True
619 if initial_length < len(dictionaries):
620 dictionaries.append([
'',
''])
def getVideosForURL(self, url, dictionaries)
def displayTreeView(self)
def getVideoDetails(self, ids)
def searchForVideos(self, title, pagenumber)
def getUserPreferences(self, userPreferenceFilePath)
def detectUserLocationByIP(self)
Start - Utility functions.
def __init__(self, apikey, mythtv=True, interactive=False, select_first=False, debug=False, custom_ui=None, language=None, search_all_languages=False)
def getSearchResults(self, title, pagenumber, pagelen)
def parseDetails(self, entry)
def massageDescription(self, text)
def setTreeViewIcon(self, dir_icon=None)
def translateItem(self, item)
def searchTitle(self, title, pagenumber, pagelen)
End of Utility functions.
def getVideoCategories(self)
def getVideosForCategory(self, categoryId, dictionaries)
static pid_list_t::iterator find(const PIDInfoMap &map, pid_list_t &list, pid_list_t::iterator begin, pid_list_t::iterator end, bool find_open)