/src/pyechonest/pyechonest/song.py

http://echo-nest-remix.googlecode.com/ · Python · 545 lines · 455 code · 39 blank · 51 comment · 11 complexity · 0b5a4cc51b5a670fed7e74374f9922fb 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. The Song module loosely covers http://developer.echonest.com/docs/v4/song.html
  7. Refer to the official api documentation if you are unsure about something.
  8. """
  9. import os
  10. import util
  11. from proxies import SongProxy
  12. try:
  13. import json
  14. except ImportError:
  15. import simplejson as json
  16. class Song(SongProxy):
  17. """
  18. A Song object
  19. Attributes:
  20. id (str): Echo Nest Song ID
  21. title (str): Song Title
  22. artist_name (str): Artist Name
  23. artist_id (str): Artist ID
  24. audio_summary (dict): An Audio Summary dict
  25. song_hotttnesss (float): A float representing a song's hotttnesss
  26. artist_hotttnesss (float): A float representing a song's parent artist's hotttnesss
  27. artist_familiarity (float): A float representing a song's parent artist's familiarity
  28. artist_location (dict): A dictionary of strings specifying a song's parent artist's location, lattitude and longitude
  29. Create a song object like so:
  30. >>> s = song.Song('SOPEXHZ12873FD2AC7')
  31. """
  32. def __init__(self, id, buckets=None, **kwargs):
  33. """
  34. Song class
  35. Args:
  36. id (str): a song ID
  37. Kwargs:
  38. buckets (list): A list of strings specifying which buckets to retrieve
  39. Returns:
  40. A Song object
  41. Example:
  42. >>> s = song.Song('SOPEXHZ12873FD2AC7', buckets=['song_hotttnesss', 'artist_hotttnesss'])
  43. >>> s.song_hotttnesss
  44. 0.58602500000000002
  45. >>> s.artist_hotttnesss
  46. 0.80329715999999995
  47. >>>
  48. """
  49. buckets = buckets or []
  50. super(Song, self).__init__(id, buckets, **kwargs)
  51. def __repr__(self):
  52. return "<%s - %s>" % (self._object_type.encode('utf-8'), self.title.encode('utf-8'))
  53. def __str__(self):
  54. return self.title.encode('utf-8')
  55. def get_audio_summary(self, cache=True):
  56. """Get an audio summary of a song containing mode, tempo, key, duration, time signature, loudness, danceability, energy, and analysis_url.
  57. Args:
  58. Kwargs:
  59. cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True.
  60. Returns:
  61. A dictionary containing mode, tempo, key, duration, time signature, loudness, danceability, energy and analysis_url keys.
  62. Example:
  63. >>> s = song.Song('SOGNMKX12B0B806320')
  64. >>> s.audio_summary
  65. {u'analysis_url': u'https://echonest-analysis.s3.amazonaws.com:443/TR/TRCPUOG123E85891F2/3/full.json?Signature=wcML1ZKsl%2F2FU4k68euHJcF7Jbc%3D&Expires=1287518562&AWSAccessKeyId=AKIAIAFEHLM3KJ2XMHRA',
  66. u'danceability': 0.20964757782725996,
  67. u'duration': 472.63301999999999,
  68. u'energy': 0.64265230549809549,
  69. u'key': 0,
  70. u'loudness': -9.6820000000000004,
  71. u'mode': 1,
  72. u'tempo': 126.99299999999999,
  73. u'time_signature': 4}
  74. >>>
  75. """
  76. if not (cache and ('audio_summary' in self.cache)):
  77. response = self.get_attribute('profile', bucket='audio_summary')
  78. self.cache['audio_summary'] = response['songs'][0]['audio_summary']
  79. return self.cache['audio_summary']
  80. audio_summary = property(get_audio_summary)
  81. def get_song_hotttnesss(self, cache=True):
  82. """Get our numerical description of how hottt a song currently is
  83. Args:
  84. Kwargs:
  85. cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True.
  86. Returns:
  87. A float representing hotttnesss.
  88. Example:
  89. >>> s = song.Song('SOLUHKP129F0698D49')
  90. >>> s.get_song_hotttnesss()
  91. 0.57344379999999995
  92. >>> s.song_hotttnesss
  93. 0.57344379999999995
  94. >>>
  95. """
  96. if not (cache and ('song_hotttnesss' in self.cache)):
  97. response = self.get_attribute('profile', bucket='song_hotttnesss')
  98. self.cache['song_hotttnesss'] = response['songs'][0]['song_hotttnesss']
  99. return self.cache['song_hotttnesss']
  100. song_hotttnesss = property(get_song_hotttnesss)
  101. def get_artist_hotttnesss(self, cache=True):
  102. """Get our numerical description of how hottt a song's artist currently is
  103. Args:
  104. Kwargs:
  105. cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True.
  106. Returns:
  107. A float representing hotttnesss.
  108. Example:
  109. >>> s = song.Song('SOOLGAZ127F3E1B87C')
  110. >>> s.artist_hotttnesss
  111. 0.45645633000000002
  112. >>> s.get_artist_hotttnesss()
  113. 0.45645633000000002
  114. >>>
  115. """
  116. if not (cache and ('artist_hotttnesss' in self.cache)):
  117. response = self.get_attribute('profile', bucket='artist_hotttnesss')
  118. self.cache['artist_hotttnesss'] = response['songs'][0]['artist_hotttnesss']
  119. return self.cache['artist_hotttnesss']
  120. artist_hotttnesss = property(get_artist_hotttnesss)
  121. def get_artist_familiarity(self, cache=True):
  122. """Get our numerical estimation of how familiar a song's artist currently is to the world
  123. Args:
  124. cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True.
  125. Returns:
  126. A float representing familiarity.
  127. Example:
  128. >>> s = song.Song('SOQKVPH12A58A7AF4D')
  129. >>> s.get_artist_familiarity()
  130. 0.639626025843539
  131. >>> s.artist_familiarity
  132. 0.639626025843539
  133. >>>
  134. """
  135. if not (cache and ('artist_familiarity' in self.cache)):
  136. response = self.get_attribute('profile', bucket='artist_familiarity')
  137. self.cache['artist_familiarity'] = response['songs'][0]['artist_familiarity']
  138. return self.cache['artist_familiarity']
  139. artist_familiarity = property(get_artist_familiarity)
  140. def get_artist_location(self, cache=True):
  141. """Get the location of a song's artist.
  142. Args:
  143. cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True.
  144. Returns:
  145. An artist location object.
  146. Example:
  147. >>> s = song.Song('SOQKVPH12A58A7AF4D')
  148. >>> s.artist_location
  149. {u'latitude': 34.053489999999996, u'location': u'Los Angeles, CA', u'longitude': -118.24532000000001}
  150. >>>
  151. """
  152. if not (cache and ('artist_location' in self.cache)):
  153. response = self.get_attribute('profile', bucket='artist_location')
  154. self.cache['artist_location'] = response['songs'][0]['artist_location']
  155. return self.cache['artist_location']
  156. artist_location = property(get_artist_location)
  157. def get_foreign_id(self, idspace='', cache=True):
  158. """Get the foreign id for this song for a specific id space
  159. Args:
  160. Kwargs:
  161. idspace (str): A string indicating the idspace to fetch a foreign id for.
  162. Returns:
  163. A foreign ID string
  164. Example:
  165. >>> s = song.Song('SOYRVMR12AF729F8DC')
  166. >>> s.get_foreign_id('CAGPXKK12BB06F9DE9')
  167. >>>
  168. """
  169. if not (cache and ('foreign_ids' in self.cache) and filter(lambda d: d.get('catalog') == idspace, self.cache['foreign_ids'])):
  170. response = self.get_attribute('profile', bucket=['id:'+idspace])
  171. rsongs = response['songs']
  172. if len(rsongs) == 0:
  173. return None
  174. foreign_ids = rsongs[0].get("foreign_ids", [])
  175. self.cache['foreign_ids'] = self.cache.get('foreign_ids', []) + foreign_ids
  176. cval = filter(lambda d: d.get('catalog') == idspace, self.cache.get('foreign_ids'))
  177. return cval[0].get('foreign_id') if cval else None
  178. def get_tracks(self, catalog, cache=True):
  179. """Get the tracks for a song given a catalog.
  180. Args:
  181. catalog (str): a string representing the catalog whose track you want to retrieve.
  182. Returns:
  183. A list of Track dicts.
  184. Example:
  185. >>> s = song.Song('SOWDASQ12A6310F24F')
  186. >>> s.get_tracks('7digital')[0]
  187. {u'catalog': u'7digital',
  188. u'foreign_id': u'7digital:track:8445818',
  189. u'id': u'TRJGNNY12903CC625C',
  190. u'preview_url': u'http://previews.7digital.com/clips/34/8445818.clip.mp3',
  191. u'release_image': u'http://cdn.7static.com/static/img/sleeveart/00/007/628/0000762838_200.jpg'}
  192. >>>
  193. """
  194. if not (cache and ('tracks' in self.cache) and (catalog in [td['catalog'] for td in self.cache['tracks']])):
  195. kwargs = {
  196. 'bucket':['tracks', 'id:%s' % catalog],
  197. }
  198. response = self.get_attribute('profile', **kwargs)
  199. if not 'tracks' in self.cache:
  200. self.cache['tracks'] = []
  201. # don't blow away the cache for other catalogs
  202. potential_tracks = response['songs'][0].get('tracks', [])
  203. existing_track_ids = [tr['foreign_id'] for tr in self.cache['tracks']]
  204. new_tds = filter(lambda tr: tr['foreign_id'] not in existing_track_ids, potential_tracks)
  205. self.cache['tracks'].extend(new_tds)
  206. return filter(lambda tr: tr['catalog']==catalog, self.cache['tracks'])
  207. def identify(filename=None, query_obj=None, code=None, artist=None, title=None, release=None, duration=None, genre=None, buckets=None, version=None, codegen_start=0, codegen_duration=30):
  208. """Identify a song.
  209. Args:
  210. Kwargs:
  211. filename (str): The path of the file you want to analyze (requires codegen binary!)
  212. query_obj (dict or list): A dict or list of dicts containing a 'code' element with an fp code
  213. code (str): A fingerprinter code
  214. artist (str): An artist name
  215. title (str): A song title
  216. release (str): A release name
  217. duration (int): A song duration
  218. genre (str): A string representing the genre
  219. buckets (list): A list of strings specifying which buckets to retrieve
  220. version (str): The version of the code generator used to generate the code
  221. codegen_start (int): The point (in seconds) where the codegen should start
  222. codegen_duration (int): The duration (in seconds) the codegen should analyze
  223. Example:
  224. >>> qo
  225. {'code': 'eJxlldehHSEMRFsChAjlAIL-S_CZvfaXXxAglEaBTen300Qu__lAyoJYhVQdXTvXrmvXdTsKZOqoU1q63QNydBGfOd1cGX3scpb1jEiWRLaPcJureC6RVkXE69jL8pGHjpP48pLI1m7r9oiEyBXvoVv45Q-5IhylYLkIRxGO4rp18ZpEOmpFPopwfJjL0u3WceO3HB1DIvJRnkQeO1PCLIsIjBWEzYaShq4pV9Z0KzDiQ8SbSNuSyBZPOOxIJKR7dauEmXwotxDCqllEAVZlrX6F8Y-IJ0e169i_HQaqslaVtTq1W-1vKeupImzrxWWVI5cPlw-XDxckN-3kyeXDm3jKmqv6PtB1gfH1Eey5qu8qvAuMC4zLfPv1l3aqviylJhytFhF0mzqs6aYpYU04mlqgKWtNjppwNKWubR2FowlHUws0gWmPi668dSHq6rOuPuhqgRcVKKM8s-fZS937nBe23iz3Uctx9607z-kLph1i8YZ8f_TfzLXseBh7nXy9nn1YBAg4Nwjp4AzTL23M_U3Rh0-sdDFtyspNOb1bYeZZqz2Y6TaHmXeuNmfFdTueLuvdsbOU9luvtIkl4vI5F_92PVprM1-sdJ_o9_Guc0b_WimpD_Rt1DFg0sY3wyw08e6jlqhjH3o76naYvzWqhX9rOv15Y7Ww_MIF8dXzw30s_uHO5PPDfUonnzq_NJ8J93mngAkIz5jA29SqxGwwvxQsih-sozX0zVk__RFaf_qyG9hb8dktZZXd4a8-1ljB-c5bllXOe1HqHplzeiN4E7q9ZRdmJuI73gBEJ_HcAxUm74PAVDNL47D6OAfzTHI0mHpXAmY60QNmlqjDfIPzwUDYhVnoXqtvZGrBdMi3ClQUQ8D8rX_1JE_In94CBXER4lrrw0H867ei8x-OVz8c-Osh5plzTOySpKIROmFkbn5xVuK784vTyPpS3OlcSjHpL16saZnm4Bk66hte9sd80Dcj02f7xDVrExjk32cssKXjmflU_SxXmn4Y9Ttued10YM552h5Wtt_WeVR4U6LPWfbIdW31J4JOXnpn4qhH7yE_pdBH9E_sMwbNFr0z0IW5NA8aOZhLmOh3zSVNRZwxiZc5pb8fikGzIf-ampJnCSb3r-ZPfjPuvLm7CY_Vfa_k7SCzdwHNg5mICTSHDxyBWmaOSyLQpPmCSXyF-eL7MHo7zNd668JMb_N-AJJRuMwrX0jNx7a8-Rj5oN6nyWoL-jRv4pu7Ue821TzU3MhvpD9Fo-XI',
  226. 'code_count': 151,
  227. 'low_rank': 0,
  228. 'metadata': {'artist': 'Harmonic 313',
  229. 'bitrate': 198,
  230. 'codegen_time': 0.57198400000000005,
  231. 'decode_time': 0.37954599999999999,
  232. 'duration': 226,
  233. 'filename': 'koln.mp3',
  234. 'genre': 'Electronic',
  235. 'given_duration': 30,
  236. 'release': 'When Machines Exceed Human Intelligence',
  237. 'sample_rate': 44100,
  238. 'samples_decoded': 661816,
  239. 'start_offset': 0,
  240. 'title': 'kln',
  241. 'version': 3.1499999999999999},
  242. 'tag': 0}
  243. >>> song.identify(query_obj=qo)
  244. [<song - K??ln>]
  245. >>>
  246. """
  247. post, has_data, data = False, False, False
  248. if filename:
  249. if os.path.exists(filename):
  250. query_obj = util.codegen(filename, start=codegen_start, duration=codegen_duration)
  251. if query_obj is None:
  252. raise Exception("The filename specified: %s could not be decoded." % filename)
  253. else:
  254. raise Exception("The filename specified: %s does not exist." % filename)
  255. if query_obj and not isinstance(query_obj, list):
  256. query_obj = [query_obj]
  257. if filename:
  258. # check codegen results from file in case we had a bad result
  259. for q in query_obj:
  260. if 'error' in q:
  261. raise Exception(q['error'] + ": " + q.get('metadata', {}).get('filename', ''))
  262. if not (filename or query_obj or code):
  263. raise Exception("Not enough information to identify song.")
  264. kwargs = {}
  265. if code:
  266. has_data = True
  267. kwargs['code'] = code
  268. if title:
  269. kwargs['title'] = title
  270. if release:
  271. kwargs['release'] = release
  272. if duration:
  273. kwargs['duration'] = duration
  274. if genre:
  275. kwargs['genre'] = genre
  276. if buckets:
  277. kwargs['bucket'] = buckets
  278. if version:
  279. kwargs['version'] = version
  280. if query_obj and any(query_obj):
  281. has_data = True
  282. data = {'query':json.dumps(query_obj)}
  283. post = True
  284. if has_data:
  285. result = util.callm("%s/%s" % ('song', 'identify'), kwargs, POST=post, data=data)
  286. return [Song(**util.fix(s_dict)) for s_dict in result['response'].get('songs',[])]
  287. def search(title=None, artist=None, artist_id=None, combined=None, description=None, style=None, mood=None, \
  288. results=None, start=None, max_tempo=None, min_tempo=None, \
  289. max_duration=None, min_duration=None, max_loudness=None, min_loudness=None, \
  290. artist_max_familiarity=None, artist_min_familiarity=None, artist_max_hotttnesss=None, \
  291. artist_min_hotttnesss=None, song_max_hotttnesss=None, song_min_hotttnesss=None, mode=None, \
  292. min_energy=None, max_energy=None, min_danceability=None, max_danceability=None, \
  293. key=None, max_latitude=None, min_latitude=None, max_longitude=None, min_longitude=None, \
  294. sort=None, buckets = None, limit=False, test_new_things=None, rank_type=None,
  295. artist_start_year_after=None, artist_start_year_before=None, artist_end_year_after=None, artist_end_year_before=None):
  296. """Search for songs by name, description, or constraint.
  297. Args:
  298. Kwargs:
  299. title (str): the name of a song
  300. artist (str): the name of an artist
  301. artist_id (str): the artist_id
  302. combined (str): the artist name and song title
  303. description (str): A string describing the artist and song
  304. style (str): A string describing the style/genre of the artist and song
  305. mood (str): A string describing the mood of the artist and song
  306. results (int): An integer number of results to return
  307. max_tempo (float): The max tempo of song results
  308. min_tempo (float): The min tempo of song results
  309. max_duration (float): The max duration of song results
  310. min_duration (float): The min duration of song results
  311. max_loudness (float): The max loudness of song results
  312. min_loudness (float): The min loudness of song results
  313. artist_max_familiarity (float): A float specifying the max familiarity of artists to search for
  314. artist_min_familiarity (float): A float specifying the min familiarity of artists to search for
  315. artist_max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for
  316. artist_min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for
  317. song_max_hotttnesss (float): A float specifying the max hotttnesss of songs to search for
  318. song_min_hotttnesss (float): A float specifying the max hotttnesss of songs to search for
  319. max_energy (float): The max energy of song results
  320. min_energy (float): The min energy of song results
  321. max_dancibility (float): The max dancibility of song results
  322. min_dancibility (float): The min dancibility of song results
  323. mode (int): 0 or 1 (minor or major)
  324. key (int): 0-11 (c, c-sharp, d, e-flat, e, f, f-sharp, g, a-flat, a, b-flat, b)
  325. max_latitude (float): A float specifying the max latitude of artists to search for
  326. min_latitude (float): A float specifying the min latitude of artists to search for
  327. max_longitude (float): A float specifying the max longitude of artists to search for
  328. min_longitude (float): A float specifying the min longitude of artists to search for
  329. sort (str): A string indicating an attribute and order for sorting the results
  330. buckets (list): A list of strings specifying which buckets to retrieve
  331. limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets
  332. rank_type (str): A string denoting the desired ranking for description searches, either 'relevance' or 'familiarity
  333. artist_start_year_before (int): Returned songs's artists will have started recording music before this year.
  334. artist_start_year_after (int): Returned songs's artists will have started recording music after this year.
  335. artist_end_year_before (int): Returned songs's artists will have stopped recording music before this year.
  336. artist_end_year_after (int): Returned songs's artists will have stopped recording music after this year.
  337. Returns:
  338. A list of Song objects
  339. Example:
  340. >>> results = song.search(artist='shakira', title='she wolf', buckets=['id:7digital', 'tracks'], limit=True, results=1)
  341. >>> results
  342. [<song - She Wolf>]
  343. >>> results[0].get_tracks('7digital')[0]
  344. {u'catalog': u'7digital',
  345. u'foreign_id': u'7digital:track:7854109',
  346. u'id': u'TRTOBSE12903CACEC4',
  347. u'preview_url': u'http://previews.7digital.com/clips/34/7854109.clip.mp3',
  348. u'release_image': u'http://cdn.7static.com/static/img/sleeveart/00/007/081/0000708184_200.jpg'}
  349. >>>
  350. """
  351. limit = str(limit).lower()
  352. kwargs = locals()
  353. kwargs['bucket'] = buckets
  354. del kwargs['buckets']
  355. result = util.callm("%s/%s" % ('song', 'search'), kwargs)
  356. return [Song(**util.fix(s_dict)) for s_dict in result['response']['songs']]
  357. def profile(ids, buckets=None, limit=False):
  358. """get the profiles for multiple songs at once
  359. Args:
  360. ids (str or list): a song ID or list of song IDs
  361. Kwargs:
  362. buckets (list): A list of strings specifying which buckets to retrieve
  363. limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets
  364. Returns:
  365. A list of term document dicts
  366. Example:
  367. >>> song_ids = [u'SOGNMKX12B0B806320', u'SOLUHKP129F0698D49', u'SOOLGAZ127F3E1B87C', u'SOQKVPH12A58A7AF4D', u'SOHKEEM1288D3ED9F5']
  368. >>> songs = song.profile(song_ids, buckets=['audio_summary'])
  369. [<song - chickfactor>,
  370. <song - One Step Closer>,
  371. <song - And I Am Telling You I'm Not Going (Glee Cast Version)>,
  372. <song - In This Temple As In The Hearts Of Man For Whom He Saved The Earth>,
  373. <song - Octet>]
  374. >>> songs[0].audio_summary
  375. {u'analysis_url': u'https://echonest-analysis.s3.amazonaws.com:443/TR/TRKHTDL123E858AC4B/3/full.json?Signature=sE6OwAzg6UvrtiX6nJJW1t7E6YI%3D&Expires=1287585351&AWSAccessKeyId=AKIAIAFEHLM3KJ2XMHRA',
  376. u'danceability': None,
  377. u'duration': 211.90485000000001,
  378. u'energy': None,
  379. u'key': 7,
  380. u'loudness': -16.736999999999998,
  381. u'mode': 1,
  382. u'tempo': 94.957999999999998,
  383. u'time_signature': 4}
  384. >>>
  385. """
  386. buckets = buckets or []
  387. if not isinstance(ids, list):
  388. ids = [ids]
  389. kwargs = {}
  390. kwargs['id'] = ids
  391. if buckets:
  392. kwargs['bucket'] = buckets
  393. if limit:
  394. kwargs['limit'] = 'true'
  395. result = util.callm("%s/%s" % ('song', 'profile'), kwargs)
  396. return [Song(**util.fix(s_dict)) for s_dict in result['response']['songs']]