PageRenderTime 98ms CodeModel.GetById 40ms app.highlight 33ms RepoModel.GetById 18ms app.codeStats 0ms

/src/pyechonest/pyechonest/song.py

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