15 __title__ =
"bliptv_api - Simple-to-use Python interface to the bliptv API (http://blip.tv/about/api/)"
16 __author__=
"R.D. Vaughan"
18 This python script is intended to perform a variety of utility functions to search and access text
19 meta data, video and image URLs from blip.tv. These routines are based on the v2.0 api. Specifications
20 for this api are published at http://blip.tv/about/api/
40 import os, struct, sys, re, time
41 import urllib.request, urllib.parse, urllib.error, urllib.request, urllib.error, urllib.parse
43 from MythTV
import MythXML
46 import xml.etree.cElementTree
as ElementTree
48 import xml.etree.ElementTree
as ElementTree
50 from .bliptv_exceptions
import (BliptvUrlError, BliptvHttpError, BliptvRssError, BliptvVideoNotFound, BliptvXmlError)
54 """Wraps a stream with an encoder"""
63 """Wraps the output stream, encoding Unicode strings with the specified encoding"""
64 if isinstance(obj, str):
66 self.
out.buffer.write(obj)
69 """Delegate everything but write to the stream"""
70 return getattr(self.
out, attr)
72 if isinstance(sys.stdout, io.TextIOWrapper):
78 """Deals with retrieval of XML files from API
85 urlhandle = urllib.request.urlopen(url)
86 except IOError
as errormsg:
88 return urlhandle.read()
93 et = ElementTree.fromstring(xml)
94 except SyntaxError
as errormsg:
100 """Main interface to http://blip.tv/
101 This is done to support a common naming framework for all python Netvision plugins no matter their site
104 Supports search and tree view methods
105 The apikey is a not required to access http://blip.tv/
111 select_first = False,
115 search_all_languages = False,
117 """apikey (str/unicode):
118 Specify the target site API key. Applications need their own key in some cases
121 When True, the returned meta data is being returned has the key and values massaged to match MythTV
122 When False, the returned meta data is being returned matches what target site returned
124 interactive (True/False): (This option is not supported by all target site apis)
125 When True, uses built-in console UI is used to select the correct show.
126 When False, the first search result is used.
128 select_first (True/False): (This option is not supported currently implemented in any grabbers)
129 Automatically selects the first series search result (rather
130 than showing the user a list of more than one series).
131 Is overridden by interactive = False, or specifying a custom_ui
134 shows verbose debugging information
136 custom_ui (xx_ui.BaseUI subclass): (This option is not supported currently implemented in any grabbers)
137 A callable subclass of interactive class (overrides interactive option)
139 language (2 character language abbreviation): (This option is not supported by all target site apis)
140 The language of the returned data. Is also the language search
141 uses. Default is "en" (English). For full list, run..
143 search_all_languages (True/False): (This option is not supported by all target site apis)
144 By default, a Netvision grabber will only search in the language specified using
145 the language option. When this is True, it will search for the
152 if apikey
is not None:
153 self.
config[
'apikey'] = apikey
157 self.
config[
'debug_enabled'] = debug
162 self.
config[
'custom_ui'] = custom_ui
166 self.
config[
'select_first'] = select_first
168 self.
config[
'search_all_languages'] = search_all_languages
171 self.
config[
'language'] =
"en"
173 self.
error_messages = {
'BliptvUrlError':
"! Error: The URL (%s) cause the exception error (%s)\n",
'BliptvHttpError':
"! Error: An HTTP communicating error with blip.tv was raised (%s)\n",
'BliptvRssError':
"! Error: Invalid RSS meta data\nwas received from blip.tv error (%s). Skipping item.\n",
'BliptvVideoNotFound':
"! Error: Video search with blip.tv did not return any results (%s)\n", }
176 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',
'blip_safeusername':
'item_author',
'updated':
'item_pubdate',
'blip_puredescription':
'item_description',
'link':
'item_link',
'blip_picture':
'item_thumbnail',
'video':
'item_url',
'blip_runtime':
'item_duration',
'blip_rating':
'item_rating',
'width':
'item_width',
'height':
'item_height',
'language':
'item_lang'}]
180 self.
config[
'base_url'] =
"http://www.blip.tv%s"
181 self.
config[
'thumb_url'] =
"http://a.images.blip.tv%s"
186 self.
config[
'urls'][
'video.search'] =
"http://www.blip.tv/?search=%s;&page=%s;&pagelen=%s;&language_code=%s;&skin=rss"
187 self.
config[
'urls'][
'categories'] =
"http://www.blip.tv/?section=categories&cmd=view&skin=api"
189 self.
config[
'image_extentions'] = [
"png",
"jpg",
"bmp"]
192 self.
config[
'item_parser'] = {}
196 self.
config[
'urls'][
'tree.view'] = {
198 '__all__': [
'http://www.blip.tv/%s/?skin=rss',
'main'],
201 '__all__': [
'http://www.blip.tv/rss/',
'main'],
211 'P_R_R_F': [[
'', [
'popular',
'recent',
'random',
'featured',]],
225 '__default__': {
'categories_id':
'',
'sort':
'', },
231 'P_R_R_F': {
'popular':
'Most Comments',
'recent':
'Most Recent',
'random':
'Random selection',
233 'categories': {
'featured':
'Featured Videos',
'popular':
'Most Comments',
'recent':
'Most Recent',
'random':
'Random selection',
238 'P_R_R_F': {
'popular':
'directories/topics/most_comments',
'recent':
'directories/topics/most_recent',
'random':
'directories/topics/random',
240 'categories': {
'featured':
'directories/topics/featured',
'popular':
'directories/topics/most_comments',
'recent':
'directories/topics/most_recent',
'random':
'directories/topics/random',
247 self.
channel_icon =
'%SHAREDIR%/mythnetvision/icons/bliptv.png'
258 '''Get longitude and latitiude to find videos relative to your location. Up to three different
259 servers will be tried before giving up.
260 return a dictionary e.g.
261 {'Latitude': '43.6667', 'Country': 'Canada', 'Longitude': '-79.4167', 'City': 'Toronto'}
262 return an empty dictionary if there were any errors
263 Code found at: http://blog.suinova.com/2009/04/from-ip-to-geolocation-country-city.html
266 '''Find the external IP address of this computer.
268 url = urllib.request.URLopener()
270 resp = url.open(
'http://www.whatismyip.com/automation/n09230945.asp')
282 gs = urllib.request.urlopen(
'http://blogama.org/ip_query.php?ip=%s&output=xml' % ip)
286 gs = urllib.request.urlopen(
'http://www.seomoz.org/ip2location/look.php?ip=%s' % ip)
290 gs = urllib.request.urlopen(
'http://api.hostip.info/?ip=%s' % ip)
293 logging.error(
'GeoIP servers not available')
296 if txt.find(
'<Response>') > 0:
297 countrys = re.findall(
r'<CountryName>([\w ]+)<',txt)[0]
298 citys = re.findall(
r'<City>([\w ]+)<',txt)[0]
299 lats,lons = re.findall(
r'<Latitude>([\d\-\.]+)</Latitude>\s*<Longitude>([\d\-\.]+)<',txt)[0]
300 elif txt.find(
'GLatLng') > 0:
301 citys,countrys = re.findall(
r'<br />\s*([^<]+)<br />\s*([^<]+)<',txt)[0]
302 lats,lons = re.findall(
r'LatLng\(([-\d\.]+),([-\d\.]+)',txt)[0]
303 elif txt.find(
'<gml:coordinates>') > 0:
304 citys = re.findall(
r'<Hostip>\s*<gml:name>(\w+)</gml:name>',txt)[0]
305 countrys = re.findall(
r'<countryName>([\w ,\.]+)</countryName>',txt)[0]
306 lats,lons = re.findall(
r'gml:coordinates>([-\d\.]+),([-\d\.]+)<',txt)[0]
308 logging.error(
'error parsing IP result %s'%txt)
310 return {
'Country':countrys,
'City':citys,
'Latitude':lats,
'Longitude':lons}
312 logging.error(
'Error parsing IP result %s'%txt)
317 '''Removes HTML markup from a text string.
318 @param text The HTML source.
319 @return The plain text. If the HTML source contains non-ASCII
320 entities or character references, this is a Unicode string.
328 if text[:3] ==
"&#x":
329 return chr(int(text[3:-1], 16))
331 return chr(int(text[2:-1]))
334 elif text[:1] ==
"&":
336 entity = html.entities.entitydefs.get(text[1:-1])
338 if entity[:2] ==
"&#":
340 return chr(int(entity[2:-1]))
344 return str(entity,
"iso-8859-1")
346 return self.
ampReplace(re.sub(
r"(?s)<[^>]*>|&#?\w+;", fixup, self.
textUtf8(text))).replace(
'\n',
' ')
351 """Setups a logger using the logging module, returns a log object
353 logger = logging.getLogger(self.
log_name)
354 formatter = logging.Formatter(
'%(asctime)s) %(levelname)s %(message)s')
356 hdlr = logging.StreamHandler(sys.stdout)
358 hdlr.setFormatter(formatter)
359 logger.addHandler(hdlr)
361 if self.
config[
'debug_enabled']:
362 logger.setLevel(logging.DEBUG)
364 logger.setLevel(logging.WARNING)
373 return str(text,
'utf8')
374 except UnicodeDecodeError:
376 except (UnicodeEncodeError, TypeError):
382 '''Replace all "&" characters with "&"
385 return text.replace(
'&',
'~~~~~').replace(
'&',
'&').replace(
'~~~~~',
'&')
389 '''Check if there is a specific generic tree view icon. If not default to the channel icon.
390 return self.tree_dir_icon
401 self.
tree_dir_icon =
'%%SHAREDIR%%/mythnetvision/icons/%s.png' % (dir_icon, )
412 playerUrl = self.
mythxml.getInternetContentUrl(
"nv_python_libs/configs/HTML/bliptv.html", \
413 url.replace(
'http://blip.tv/play/',
''))
417 '''Key word video search of the blip.tv web site
418 return an array of matching item dictionaries
421 url = self.
config[
'urls'][
'video.search'] % (urllib.parse.quote_plus(title.encode(
"utf-8")), pagenumber, pagelen, self.
config[
'language'])
423 if self.
config[
'debug_enabled']:
430 except Exception
as errormsg:
434 raise BliptvVideoNotFound(
"1-No blip.tv Video matches found for search value (%s)" % title)
438 dictionary_first =
False
441 language = self.
config[
'language']
442 for elements
in etree.find(
'channel'):
443 if elements.tag ==
'language':
445 language = elements.text[:2]
447 if not elements.tag ==
'item':
450 item[
'language'] = language
452 for elem
in elements:
453 if elem.tag ==
'title':
457 if elem.tag.endswith(
'safeusername'):
461 if elem.tag.endswith(
'pubDate'):
465 if elem.tag.endswith(
'puredescription'):
469 if elem.tag.endswith(
'link'):
471 item[
'link'] = self.
ampReplace(elem.text.strip())
473 if elem.tag.endswith(
'embedUrl'):
477 if elem.tag.endswith(
'thumbnail'):
479 item[
'blip_picture'] = self.
ampReplace(elem.get(
'url').strip())
481 if elem.tag.endswith(
'group'):
484 if e.tag.endswith(
'content'):
485 if e.get(
'fileSize'):
487 if int(e.get(
'fileSize')) > file_size:
488 item[
'video'] = self.
ampReplace(e.get(
'url').strip())
489 file_size = int(e.get(
'fileSize'))
494 if elem.tag.endswith(
'runtime'):
498 if elem.tag.endswith(
'rating'):
502 if 'video' not in item
and 'link' not in item
and not embedURL:
506 if 'link' in item
and 'video' not in item:
508 if 'video' in item
and 'link' not in item:
509 item[
'link'] = item[
'video']
510 elements_final.append(item)
512 if not len(elements_final):
513 raise BliptvVideoNotFound(
"2-No blip.tv Video matches found for search value (%s)" % title)
515 return elements_final
520 """Common name for a video search. Used to interface with MythTV plugin NetVision
523 data = self.
searchTitle(title, pagenumber, self.page_limit)
524 except BliptvVideoNotFound
as msg:
525 sys.stderr.write(
"%s\n" % msg)
527 except BliptvUrlError
as msg:
528 sys.stderr.write(
'%s' % msg)
530 except BliptvHttpError
as msg:
533 except BliptvRssError
as msg:
536 except Exception
as e:
537 sys.stderr.write(
"! Error: Unknown error during a Video search (%s)\nError(%s)\n" % (title, e))
549 if key
in list(match.keys()):
553 items.append(item_data)
556 channel = {
'channel_title':
'blip.tv',
'channel_link':
'http://blip.tv',
'channel_description':
"We're the next generation television network",
'channel_numresults': 0,
'channel_returned': 1,
'channel_startindex': 0}
558 if len(items) == self.page_limit:
559 channel[
'channel_numresults'] = self.page_limit * int(pagenumber) + 1
561 channel[
'channel_numresults'] = self.page_limit * int(pagenumber)
562 channel[
'channel_startindex'] = self.page_limit * int(pagenumber)
563 channel[
'channel_returned'] = len(items)
566 return [[channel, items]]
572 '''Get the list of valid category ids and their name and update the proper dictionaries
575 url = self.
config[
'urls'][
'categories']
576 if self.
config[
'debug_enabled']:
577 print(
"Category list URL:")
583 except Exception
as errormsg:
584 sys.stderr.write(self.
error_messages[
'BliptvUrlError'] % (url, errormsg))
589 sys.stderr.write(
'1-No Categories found at (%s)\n' % url)
593 if not etree.find(
'payload'):
594 sys.stderr.write(
'2-No Categories found at (%s)\n' % url)
599 for element
in etree.find(
'payload'):
600 if element.tag ==
'category':
612 if tmp_id
and tmp_name:
614 self.
tree_org[
'categories'].append([tmp_name, [
'popular',
'recent',
'random',
'featured',]])
615 self.
feed_names[
'categories'][tmp_name] = tmp_id
618 sys.stderr.write(
'3-No Categories found at (%s)\n' % url)
622 self.
tree_org[
'categories'].append([
'',
''])
628 '''Gather the categories/feeds/...etc then retrieve a max page of videos meta data in each of them
629 return array of directories and their video meta data
632 self.
channel = {
'channel_title':
'blip.tv',
'channel_link':
'http://blip.tv',
'channel_description':
"We're the next generation television network",
'channel_numresults': 0,
'channel_returned': 1,
'channel_startindex': 0}
634 if self.
config[
'debug_enabled']:
645 if key ==
'categories':
652 return [[self.
channel, dictionaries]]
656 '''Form a URL to search for videos
668 for ky
in list(additions.keys()):
669 if ky.startswith(
'add_'):
670 addition+=
'/%s' % additions[ky]
672 addition+=
'?%s=%s' % (ky, additions[ky])
673 index = URL.find(
'%')
675 return (URL+addition)
677 return (URL+addition) % self.feed
682 '''Parse a list made of categories and retrieve video meta data
683 return a dictionary of directory names and categories video meta data
685 for sets
in dir_dict:
686 if not isinstance(sets[1], list):
690 dictionaries.append([
'',
''])
693 for self.feed
in sets[1]:
701 temp_dictionary = self.
config[
'item_parser'][URL[1]](self.
makeURL(URL[0]), temp_dictionary)
702 if len(temp_dictionary):
705 for element
in temp_dictionary:
706 dictionaries.append(element)
708 dictionaries.append([
'',
''])
713 '''Get the video meta data for url search
714 return the video dictionary of directories and their video mata data
716 initial_length = len(dictionaries)
718 if self.
config[
'debug_enabled']:
725 except Exception
as errormsg:
726 sys.stderr.write(self.
error_messages[
'BliptvUrlError'] % (url, errormsg))
730 sys.stderr.write(
'1-No Videos for (%s)\n' % self.feed)
733 dictionary_first =
False
735 language = self.
config[
'language']
736 for elements
in etree.find(
'channel'):
737 if elements.tag.endswith(
'language'):
739 language = elements.text[:2]
742 if not elements.tag.endswith(
'item'):
746 item[
'language'] = language
748 for elem
in elements:
749 if elem.tag ==
'title':
753 if elem.tag.endswith(
'safeusername'):
757 if elem.tag.endswith(
'pubDate'):
761 if elem.tag.endswith(
'puredescription'):
765 if elem.tag ==
'link':
767 item[
'link'] = self.
ampReplace(elem.text.strip())
769 if elem.tag.endswith(
'embedUrl'):
773 if elem.tag.endswith(
'thumbnail'):
775 item[
'blip_picture'] = self.
ampReplace(elem.get(
'url').strip())
777 if elem.tag.endswith(
'group'):
780 if e.tag.endswith(
'content'):
781 for key
in list(e.keys()):
782 if key.endswith(
'vcodec'):
786 if e.get(
'fileSize'):
788 if int(e.get(
'fileSize')) > file_size:
789 item[
'video'] = self.
ampReplace(e.get(
'url').strip())
790 file_size = int(e.get(
'fileSize'))
794 item[
'height'] = e.get(
'height').strip()
796 item[
'width'] = e.get(
'width').strip()
799 if elem.tag.endswith(
'runtime'):
803 if elem.tag.endswith(
'rating'):
807 if 'video' not in item
and 'link' not in item:
810 item[
'link'] = embedURL
811 if 'link' in item
and 'video' not in item:
813 if 'video' in item
and 'link' not in item:
814 item[
'link'] = item[
'video']
817 if not dictionary_first:
819 dictionary_first =
True
827 dictionaries.append(final_item)
830 if initial_length < len(dictionaries):
831 dictionaries.append([
'',
''])