14 __title__ =
"youtube_api - Simple-to-use Python interface to the youtube API (http://developer.youtubenservices.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 meta data, video and image URLs from youtube. These routines are based on the api. Specifications
19 for this api are published at http://developer.youtubenservices.com/docs
39 import os, struct, sys, re, time, shutil
40 import urllib.request, urllib.parse, urllib.error, urllib.request, urllib.error, urllib.parse
43 from MythTV
import MythXML
44 from ..common
import common_api
46 from .youtube_exceptions
import (YouTubeUrlError, YouTubeHttpError, YouTubeRssError, YouTubeVideoNotFound, YouTubeInvalidSearchType, YouTubeXmlError, YouTubeVideoDetailError, YouTubeCategoryNotFound)
47 from .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
121 self.
common = common_api.Common()
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',
205 self.
channel_icon =
'%SHAREDIR%/mythnetvision/icons/youtube.png'
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.
352 return self.tree_dir_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([
'',
''])