/src/pyechonest/pyechonest/util.py

http://echo-nest-remix.googlecode.com/ · Python · 241 lines · 159 code · 35 blank · 47 comment · 46 complexity · d3e4b750e36ee8d74d7d00090fb7ebb6 MD5 · raw file

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. """
  4. Copyright (c) 2010 The Echo Nest. All rights reserved.
  5. Created by Tyler Williams on 2010-04-25.
  6. Utility functions to support the Echo Nest web API interface.
  7. """
  8. import urllib
  9. import urllib2
  10. import httplib
  11. import config
  12. import logging
  13. import socket
  14. import re
  15. import time
  16. import os
  17. import subprocess
  18. import traceback
  19. from types import StringType, UnicodeType
  20. try:
  21. import json
  22. except ImportError:
  23. import simplejson as json
  24. logging.basicConfig(level=logging.INFO)
  25. logger = logging.getLogger(__name__)
  26. TYPENAMES = (
  27. ('AR', 'artist'),
  28. ('SO', 'song'),
  29. ('RE', 'release'),
  30. ('TR', 'track'),
  31. ('PE', 'person'),
  32. ('DE', 'device'),
  33. ('LI', 'listener'),
  34. ('ED', 'editor'),
  35. ('TW', 'tweditor'),
  36. ('CA', 'catalog'),
  37. )
  38. foreign_regex = re.compile(r'^.+?:(%s):([^^]+)\^?([0-9\.]+)?' % r'|'.join(n[1] for n in TYPENAMES))
  39. short_regex = re.compile(r'^((%s)[0-9A-Z]{16})\^?([0-9\.]+)?' % r'|'.join(n[0] for n in TYPENAMES))
  40. long_regex = re.compile(r'music://id.echonest.com/.+?/(%s)/(%s)[0-9A-Z]{16}\^?([0-9\.]+)?' % (r'|'.join(n[0] for n in TYPENAMES), r'|'.join(n[0] for n in TYPENAMES)))
  41. headers = [('User-Agent', 'Pyechonest %s' % (config.__version__,))]
  42. class MyBaseHandler(urllib2.BaseHandler):
  43. def default_open(self, request):
  44. if config.TRACE_API_CALLS:
  45. logger.info("%s" % (request.get_full_url(),))
  46. request.start_time = time.time()
  47. return None
  48. class MyErrorProcessor(urllib2.HTTPErrorProcessor):
  49. def http_response(self, request, response):
  50. code = response.code
  51. if config.TRACE_API_CALLS:
  52. logger.info("took %2.2fs: (%i)" % (time.time()-request.start_time,code))
  53. if code in [200, 400, 403, 500]:
  54. return response
  55. else:
  56. urllib2.HTTPErrorProcessor.http_response(self, request, response)
  57. opener = urllib2.build_opener(MyBaseHandler(), MyErrorProcessor())
  58. opener.addheaders = headers
  59. class EchoNestAPIError(Exception):
  60. """
  61. Generic API errors.
  62. """
  63. def __init__(self, code, message):
  64. self.args = ('Echo Nest API Error %d: %s' % (code, message),)
  65. def get_successful_response(raw_json):
  66. try:
  67. response_dict = json.loads(raw_json)
  68. status_dict = response_dict['response']['status']
  69. code = int(status_dict['code'])
  70. message = status_dict['message']
  71. if (code != 0):
  72. # do some cute exception handling
  73. raise EchoNestAPIError(code, message)
  74. del response_dict['response']['status']
  75. return response_dict
  76. except ValueError:
  77. logger.debug(traceback.format_exc())
  78. raise EchoNestAPIError(-1, "Unknown error.")
  79. # These two functions are to deal with the unknown encoded output of codegen (varies by platform and ID3 tag)
  80. def reallyunicode(s, encoding="utf-8"):
  81. if type(s) is StringType:
  82. for args in ((encoding,), ('utf-8',), ('latin-1',), ('ascii', 'replace')):
  83. try:
  84. s = s.decode(*args)
  85. break
  86. except UnicodeDecodeError:
  87. continue
  88. if type(s) is not UnicodeType:
  89. raise ValueError, "%s is not a string at all." % s
  90. return s
  91. def reallyUTF8(s):
  92. return reallyunicode(s).encode("utf-8")
  93. def codegen(filename, start=0, duration=30):
  94. # Run codegen on the file and return the json. If start or duration is -1 ignore them.
  95. cmd = config.CODEGEN_BINARY_OVERRIDE
  96. if not cmd:
  97. # Is this is posix platform, or is it windows?
  98. if hasattr(os, 'uname'):
  99. if(os.uname()[0] == "Darwin"):
  100. cmd = "codegen.Darwin"
  101. else:
  102. cmd = 'codegen.'+os.uname()[0]+'-'+os.uname()[4]
  103. else:
  104. cmd = "codegen.windows.exe"
  105. if not os.path.exists(cmd):
  106. raise Exception("Codegen binary not found.")
  107. command = cmd + " \"" + filename + "\" "
  108. if start >= 0:
  109. command = command + str(start) + " "
  110. if duration >= 0:
  111. command = command + str(duration)
  112. p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  113. (json_block, errs) = p.communicate()
  114. json_block = reallyUTF8(json_block)
  115. try:
  116. return json.loads(json_block)
  117. except ValueError:
  118. logger.debug("No JSON object came out of codegen: error was %s" % (errs))
  119. return None
  120. def callm(method, param_dict, POST=False, socket_timeout=None, data=None):
  121. """
  122. Call the api!
  123. Param_dict is a *regular* *python* *dictionary* so if you want to have multi-valued params
  124. put them in a list.
  125. ** note, if we require 2.6, we can get rid of this timeout munging.
  126. """
  127. param_dict['api_key'] = config.ECHO_NEST_API_KEY
  128. param_list = []
  129. if not socket_timeout:
  130. socket_timeout = config.CALL_TIMEOUT
  131. for key,val in param_dict.iteritems():
  132. if isinstance(val, list):
  133. param_list.extend( [(key,subval) for subval in val] )
  134. elif val is not None:
  135. if isinstance(val, unicode):
  136. val = val.encode('utf-8')
  137. param_list.append( (key,val) )
  138. params = urllib.urlencode(param_list)
  139. orig_timeout = socket.getdefaulttimeout()
  140. socket.setdefaulttimeout(socket_timeout)
  141. if(POST):
  142. if (not method == 'track/upload') or ((method == 'track/upload') and 'url' in param_dict):
  143. """
  144. this is a normal POST call
  145. """
  146. url = 'http://%s/%s/%s/%s' % (config.API_HOST, config.API_SELECTOR,
  147. config.API_VERSION, method)
  148. if data is None:
  149. data = ''
  150. data = urllib.urlencode(data)
  151. data = "&".join([data, params])
  152. f = opener.open(url, data=data)
  153. else:
  154. """
  155. upload with a local file is special, as the body of the request is the content of the file,
  156. and the other parameters stay on the URL
  157. """
  158. url = '/%s/%s/%s?%s' % (config.API_SELECTOR, config.API_VERSION,
  159. method, params)
  160. if ':' in config.API_HOST:
  161. host, port = config.API_HOST.split(':')
  162. else:
  163. host = config.API_HOST
  164. port = 80
  165. if config.TRACE_API_CALLS:
  166. logger.info("%s/%s" % (host+':'+str(port), url,))
  167. conn = httplib.HTTPConnection(host, port = port)
  168. conn.request('POST', url, body = data, headers = dict([('Content-Type', 'application/octet-stream')]+headers))
  169. f = conn.getresponse()
  170. else:
  171. """
  172. just a normal GET call
  173. """
  174. url = 'http://%s/%s/%s/%s?%s' % (config.API_HOST, config.API_SELECTOR, config.API_VERSION,
  175. method, params)
  176. f = opener.open(url)
  177. socket.setdefaulttimeout(orig_timeout)
  178. # try/except
  179. response_dict = get_successful_response(f.read())
  180. return response_dict
  181. def postChunked(host, selector, fields, files):
  182. """
  183. Attempt to replace postMultipart() with nearly-identical interface.
  184. (The files tuple no longer requires the filename, and we only return
  185. the response body.)
  186. Uses the urllib2_file.py originally from
  187. http://fabien.seisen.org which was also drawn heavily from
  188. http://code.activestate.com/recipes/146306/ .
  189. This urllib2_file.py is more desirable because of the chunked
  190. uploading from a file pointer (no need to read entire file into
  191. memory) and the ability to work from behind a proxy (due to its
  192. basis on urllib2).
  193. """
  194. params = urllib.urlencode(fields)
  195. url = 'http://%s%s?%s' % (host, selector, params)
  196. u = urllib2.urlopen(url, files)
  197. result = u.read()
  198. [fp.close() for (key, fp) in files]
  199. return result
  200. def fix(x):
  201. # we need this to fix up all the dict keys to be strings, not unicode objects
  202. assert(isinstance(x,dict))
  203. return dict((str(k), v) for (k,v) in x.iteritems())