├── MANIFEST.in ├── generate_dist.sh ├── doc ├── source │ ├── 200x160_lt.png │ ├── util.rst │ ├── proxies.rst │ ├── config.rst │ ├── catalog.rst │ ├── song.rst │ ├── playlist.rst │ ├── artist.rst │ ├── contents.rst │ ├── track.rst │ ├── _templates │ │ ├── layout.html │ │ └── index.html │ └── conf.py └── Makefile ├── .gitmodules ├── INSTALL ├── setup.cfg ├── AUTHORS ├── pyechonest ├── __init__.py ├── results.py ├── config.py ├── sandbox.py ├── proxies.py ├── util.py ├── catalog.py ├── track.py ├── playlist.py ├── song.py └── artist.py ├── .gitignore ├── examples ├── tempo.py ├── show_tempos.py └── try_new_things.py ├── setup.py ├── LICENSE ├── mkrelease.sh ├── README.md └── CHANGELOG /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /generate_dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /usr/bin/env python setup.py bdist_egg -------------------------------------------------------------------------------- /doc/source/200x160_lt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/variable/pyechonest/master/doc/source/200x160_lt.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "doc/build/html"] 2 | path = doc/build/html 3 | url = git@github.com:echonest/pyechonest.git 4 | -------------------------------------------------------------------------------- /doc/source/util.rst: -------------------------------------------------------------------------------- 1 | Util -- utility functions 2 | ========================= 3 | 4 | .. automodule:: pyechonest.util 5 | :members: -------------------------------------------------------------------------------- /doc/source/proxies.rst: -------------------------------------------------------------------------------- 1 | Proxies -- object proxies 2 | ========================= 3 | 4 | .. automodule:: pyechonest.proxies 5 | :members: -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | As per usual python routine, the following should get you setup: 2 | 3 | $ python setup.py build 4 | $ python setup.py install 5 | 6 | -------------------------------------------------------------------------------- /doc/source/config.rst: -------------------------------------------------------------------------------- 1 | Config -- configuration file 2 | ============================ 3 | .. automodule:: pyechonest.config 4 | :members: 5 | 6 | 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = doc/source 3 | build-dir = doc/build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = doc/build/html 8 | -------------------------------------------------------------------------------- /doc/source/catalog.rst: -------------------------------------------------------------------------------- 1 | Catalog -- catalog methods 2 | ========================== 3 | 4 | .. autoclass:: pyechonest.catalog.Catalog 5 | :members: 6 | 7 | .. automethod:: pyechonest.catalog.list_catalogs 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | This file contains the name of the people who have contributed to 2 | pyechonest. The names are sorted alphabetically by last name. 3 | 4 | Aaron Daubman 5 | David DesRoches 6 | Reid Draper 7 | Ben Lacker 8 | Scotty Vercoe 9 | Brian Whitman 10 | Tyler Williams -------------------------------------------------------------------------------- /pyechonest/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2009-06-25. 7 | """ 8 | 9 | __all__ = ['config', 'util', 'proxies', 'artist', 'catalog', 'song', 'track', 'playlist'] 10 | -------------------------------------------------------------------------------- /doc/source/song.rst: -------------------------------------------------------------------------------- 1 | Song -- song methods 2 | ========================= 3 | 4 | .. autoclass:: pyechonest.song.Song 5 | :members: 6 | 7 | .. automethod:: pyechonest.song.identify 8 | 9 | .. automethod:: pyechonest.song.search 10 | 11 | .. automethod:: pyechonest.song.profile -------------------------------------------------------------------------------- /doc/source/playlist.rst: -------------------------------------------------------------------------------- 1 | Playlist -- playlist methods 2 | ============================ 3 | 4 | .. autoclass:: pyechonest.playlist.Playlist 5 | :members: 6 | 7 | .. automethod:: pyechonest.playlist.basic 8 | 9 | .. automethod:: pyechonest.playlist.static 10 | 11 | .. autoclass:: pyechonest.playlist.DeprecatedPlaylist 12 | :members: -------------------------------------------------------------------------------- /doc/source/artist.rst: -------------------------------------------------------------------------------- 1 | Artist -- artist methods 2 | ========================= 3 | 4 | .. autoclass:: pyechonest.artist.Artist 5 | :members: 6 | 7 | .. automethod:: pyechonest.artist.search 8 | 9 | .. automethod:: pyechonest.artist.top_hottt 10 | 11 | .. automethod:: pyechonest.artist.top_terms 12 | 13 | .. automethod:: pyechonest.artist.similar -------------------------------------------------------------------------------- /doc/source/contents.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | artist 8 | 9 | song 10 | 11 | track 12 | 13 | playlist 14 | 15 | catalog 16 | 17 | util 18 | 19 | config 20 | 21 | proxies 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /doc/source/track.rst: -------------------------------------------------------------------------------- 1 | Track -- track methods 2 | ========================= 3 | 4 | .. autoclass:: pyechonest.track.Track 5 | :members: 6 | 7 | .. automethod:: pyechonest.track.track_from_file 8 | 9 | .. automethod:: pyechonest.track.track_from_filename 10 | 11 | .. automethod:: pyechonest.track.track_from_url 12 | 13 | .. automethod:: pyechonest.track.track_from_id 14 | 15 | .. automethod:: pyechonest.track.track_from_md5 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | doc/build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # IntelliJ IDEA 39 | .idea 40 | *.iml 41 | -------------------------------------------------------------------------------- /doc/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block extrahead %} 4 | {{ super() }} 5 | {%- if not embedded %} 6 | 10 | 11 | {%- endif %} 12 | {% endblock %} 13 | 14 | {% block rootrellink %} 15 | 16 | 17 |
  • Pyechonest home | 
  • 18 |
  • Documentation 19 | »
  • 20 | {% endblock %} 21 | 22 | {% block header %} 23 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /examples/tempo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pyechonest import song 3 | 4 | def get_tempo(artist, title): 5 | "gets the tempo for a song" 6 | results = song.search(artist=artist, title=title, results=1, buckets=['audio_summary']) 7 | if len(results) > 0: 8 | return results[0].audio_summary['tempo'] 9 | else: 10 | return None 11 | 12 | 13 | if __name__ == '__main__': 14 | if len(sys.argv) <> 3: 15 | print "Usage: python tempo.py 'artist name' 'song title'" 16 | else: 17 | tempo = get_tempo(sys.argv[1], sys.argv[2]) 18 | if tempo: 19 | print 'Tempo for', sys.argv[1], sys.argv[2], 'is', tempo 20 | else: 21 | print "Can't find Tempo for artist:", sys.argv[1], 'song:', sys.argv[2] 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/show_tempos.py: -------------------------------------------------------------------------------- 1 | # Shows the tempos for all of the songs in a directory 2 | # requires eyeD3, available from http://eyed3.nicfit.net/ 3 | 4 | import sys 5 | import os 6 | import tempo 7 | 8 | from pyechonest import track 9 | 10 | def show_tempo(mp3): 11 | "given an mp3, print out the artist, title and tempo of the song" 12 | pytrack = track.track_from_filename(mp3) 13 | print 'File: ', mp3 14 | print 'Artist:', pytrack.artist if hasattr(pytrack, 'artist') else 'Unknown' 15 | print 'Title: ', pytrack.title if hasattr(pytrack, 'title') else 'Unknown' 16 | print 'Tempo: ', pytrack.tempo 17 | print 18 | 19 | 20 | def show_tempos(dir): 21 | "print out the tempo for each MP3 in the give directory" 22 | for f in os.listdir(dir): 23 | if f.lower().endswith(".mp3"): 24 | path = os.path.join(dir, f) 25 | show_tempo(path) 26 | 27 | 28 | if __name__ == '__main__': 29 | if len(sys.argv) == 1: 30 | print 'usage: python show_tempos.py path' 31 | else: 32 | show_tempos(sys.argv[1]) 33 | -------------------------------------------------------------------------------- /pyechonest/results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-04-25. 7 | """ 8 | 9 | import logging 10 | from util import attrdict 11 | 12 | # I want a: 13 | # generic object that takes a dict and turns it into an object 14 | # should take on the name of a key in the dict 15 | # should handle lists 16 | class Result(attrdict): 17 | def __init__(self, result_type, result_dict): 18 | self._object_type = result_type 19 | assert(isinstance(result_dict,dict)) 20 | self.__dict__.update(result_dict) 21 | 22 | def __repr__(self): 23 | return "" % (self._object_type) 24 | 25 | def __str__(self): 26 | return "" % (self._object_type) 27 | 28 | def make_results(result_type, response, accessor_function): 29 | try: 30 | data = accessor_function(response) 31 | if isinstance(data, list): 32 | return [Result(result_type, item) for item in data] 33 | elif isinstance(data, dict): 34 | return Result(result_type, data) 35 | else: 36 | return data 37 | except IndexError: 38 | logging.info("No songs found") 39 | 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | __version__ = "8.0.1" 5 | 6 | # $Source$ 7 | from sys import version 8 | import os 9 | from setuptools import setup 10 | 11 | if version < '2.6': 12 | requires=['urllib', 'urllib2', 'simplejson'] 13 | elif version >= '2.6': 14 | requires=['urllib', 'urllib2', 'json'] 15 | else: 16 | #unknown version? 17 | requires=['urllib', 'urllib2'] 18 | 19 | def read(fname): 20 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 21 | 22 | setup( 23 | name='pyechonest', 24 | version=__version__, 25 | description='Python interface to The Echo Nest APIs.', 26 | long_description=""" 27 | Tap into The Echo Nest's Musical Brain for the best music search, information, recommendations and remix tools on the web. 28 | Pyechonest is an open source Python library for the Echo Nest API. With Pyechonest you have Python access to the entire set of API methods. 29 | See: http://developer.echonest.com 30 | """, 31 | author='Tyler Williams', 32 | author_email='tyler@echonest.com', 33 | maintainer='David DesRoches', 34 | maintainer_email='delicious@echonest.com', 35 | url='https://github.com/echonest/pyechonest', 36 | download_url='https://github.com/echonest/pyechonest', 37 | package_dir={'pyechonest':'pyechonest'}, 38 | packages=['pyechonest'], 39 | requires=requires 40 | ) 41 | -------------------------------------------------------------------------------- /pyechonest/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-04-25. 7 | 8 | Global configuration variables for accessing the Echo Nest web API. 9 | """ 10 | 11 | import pkg_resources 12 | 13 | try: 14 | __version__ = pkg_resources.require("pyechonest")[0].version 15 | except pkg_resources.DistributionNotFound: 16 | __version__ = "0.0.0" 17 | 18 | import sys, os 19 | 20 | envkeys = ["ECHO_NEST_API_KEY", "ECHO_NEST_CONSUMER_KEY", "ECHO_NEST_SHARED_SECRET"] 21 | this_module = sys.modules[__name__] 22 | for key in envkeys: 23 | setattr(this_module, key, os.environ.get(key, None)) 24 | 25 | API_HOST = 'developer.echonest.com' 26 | "The API endpoint you're talking to" 27 | 28 | API_SELECTOR = 'api' 29 | "API selector... just 'api' for now" 30 | 31 | API_VERSION = 'v4' 32 | "Version of api to use... only 4 for now" 33 | 34 | HTTP_USER_AGENT = 'PyEchonest' 35 | """ 36 | You may change this to be a user agent string of your 37 | own choosing 38 | """ 39 | 40 | TRACE_API_CALLS = False 41 | """ 42 | If true, API calls will be traced to the console 43 | """ 44 | 45 | CALL_TIMEOUT = 10 46 | """ 47 | The API call timeout (seconds) 48 | """ 49 | 50 | CODEGEN_BINARY_OVERRIDE = None 51 | """ 52 | Location of your codegen binary. If not given, we will guess codegen.platform-architecture on your system path, e.g. codegen.Darwin, codegen.Linux-i386 53 | """ 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, The Echo Nest Corporation 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /mkrelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ============================================== 4 | # = This script will make a pyechonest release = 5 | # ============================================== 6 | 7 | args=`getopt to: $*` 8 | 9 | function usage() { 10 | echo "$0 -o [-t ]" 11 | } 12 | 13 | if [ $? != 0 ]; then 14 | usage 15 | exit 2 16 | fi 17 | 18 | EXPORT_LOCATION="" 19 | TEMP_LOCATION="/tmp" 20 | set -- $args 21 | for i 22 | do 23 | case "$i" in 24 | -o) 25 | EXPORT_LOCATION=$2; shift; 26 | shift;; 27 | -t) 28 | TEMP_LOCATION=$2; shift; 29 | shift;; 30 | --) 31 | shift; break;; 32 | esac 33 | done 34 | 35 | if [ -z "${EXPORT_LOCATION}" ]; then 36 | usage 37 | exit 2 38 | fi 39 | 40 | # check that sphinx is installed, we need it to make the docs! 41 | type -P sphinx-build &>/dev/null || { echo "Please install sphinx (easy_install -U sphinx)" >&2; exit 1; } 42 | # export a clean copy to export location 43 | git archive master --format tar --prefix pyechonest/ --output $TEMP_LOCATION/pyechonest.tar.gz 44 | tar -xvzf $TEMP_LOCATION/pyechonest.tar.gz -C $TEMP_LOCATION/ 45 | 46 | # remove this script, as well as our test files or .pyc files 47 | rm -rf "$TEMP_LOCATION"/mkrelease.sh 48 | rm -rf "$TEMP_LOCATION"/test.py 49 | rm -rf "$TEMP_LOCATION"/test 50 | rm -rf "$TEMP_LOCATION"/tmp 51 | 52 | # remake the docs 53 | cd "$TEMP_LOCATION"/pyechonest && \ 54 | python setup.py build_sphinx 55 | 56 | # remove pyc files 57 | find "$TEMP_LOCATION"/pyechonest -name "*.pyc" | xargs rm -rf 58 | 59 | # make zip and copy 60 | cd "$TEMP_LOCATION" && \ 61 | zip -r "$EXPORT_LOCATION"/pyechonest.zip pyechonest 62 | 63 | # make egg and copy 64 | cd "$TEMP_LOCATION"/pyechonest && \ 65 | python setup.py bdist_egg && \ 66 | cp dist/*.egg "$EXPORT_LOCATION" 67 | 68 | # remove temp dir 69 | rm -rf "$TEMP_LOCATION"/pyechonest 70 | rm -rf "$TEMP_LOCATION"/pyechonest.tar.gz 71 | -------------------------------------------------------------------------------- /pyechonest/sandbox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2011-10-21. 7 | 8 | The Sandbox module loosely covers http://developer.echonest.com/docs/v4/sandbox.html 9 | Refer to the official api documentation if you are unsure about something. 10 | """ 11 | try: 12 | import json 13 | except ImportError: 14 | import simplejson as json 15 | import datetime 16 | 17 | import util 18 | from proxies import ResultList 19 | 20 | def list(sandbox_name, results=15, start=0): 21 | """ 22 | Returns a list of all assets available in this sandbox 23 | 24 | Args: 25 | sandbox_name (str): A string representing the name of the sandbox 26 | 27 | Kwargs: 28 | results (int): An integer number of results to return 29 | 30 | start (int): An integer starting value for the result set 31 | 32 | Returns: 33 | A list of asset dictionaries 34 | 35 | Example: 36 | 37 | >>> sandbox.list('bluenote') 38 | [{}, {}] 39 | >>> 40 | 41 | 42 | """ 43 | result = util.callm("%s/%s" % ('sandbox', 'list'), {'sandbox':sandbox_name, 'results': results, 'start': start}) 44 | assets = result['response']['assets'] 45 | start = result['response']['start'] 46 | total = result['response']['total'] 47 | 48 | return ResultList(assets, start, total) 49 | 50 | 51 | def access(sandbox_name, asset_ids): 52 | """ 53 | Returns a list of assets with expiring access urls that can be used to download them 54 | *Requires Oauth* 55 | 56 | Args: 57 | sandbox_name (str): A string representing the name of the sandbox 58 | asset_ids (list): A list of asset_ids (str) to fetch 59 | 60 | Kwargs: 61 | 62 | Returns: 63 | A list of asset dictionaries 64 | 65 | Example: 66 | 67 | >>> sandbox.access('bluenote', ['12345']) 68 | [{}, {}] 69 | >>> 70 | 71 | 72 | """ 73 | result = util.oauthgetm("%s/%s" % ('sandbox', 'access'), {'sandbox':sandbox_name, 'id':asset_ids}) 74 | return result['response']['assets'] 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyechonest 2 | 3 | Tap into [The Echo Nest's](http://the.echonest.com/) Musical Brain for the best music search, information, recommendations and remix tools on the web. 4 | 5 | Pyechonest is an open source Python library for the Echo Nest API. With Pyechonest you have Python access to the entire set of API methods including: 6 | 7 | * **artist** - search for artists by name, description, or attribute, and get back detailed information about any artist including audio, similar artists, blogs, familiarity, hotttnesss, news, reviews, urls and video. 8 | * **song** - search songs by artist, title, description, or attribute (tempo, duration, etc) and get detailed information back about each song, such as hotttnesss, audio_summary, or tracks. 9 | * **track** - upload a track to the Echo Nest and receive summary information about the track including key, duration, mode, tempo, time signature along with detailed track info including timbre, pitch, rhythm and loudness information. 10 | 11 | ## Install 12 | There are a few different ways you can install pyechonest: 13 | 14 | * Use setuptools: `easy_install -U pyechonest` 15 | * Download the zipfile from the [releases](https://github.com/echonest/pyechonest/releases) page and install it. 16 | * Checkout the source: `git clone git://github.com/echonest/pyechonest.git` and install it yourself. 17 | 18 | ## Getting Started 19 | * Install Pyechonest 20 | * **Get an API key** - to use the Echo Nest API you need an Echo Nest API key. You can get one for free at [developer.echonest.com](http://developer.echonest.com). 21 | * **Set the API** key - you can do this one of two ways: 22 | * set an environment variable named `ECHO_NEST_API_KEY` to your API key 23 | * Include this snippet of code at the beginning of your python scripts: 24 | 25 | ```python 26 | from pyechonest import config 27 | config.ECHO_NEST_API_KEY="YOUR API KEY" 28 | ``` 29 | 30 | * Check out the [docs](http://echonest.github.com/pyechonest/) and examples below. 31 | 32 | ## Examples 33 | *All examples assume you have already setup your api key!* 34 | 35 | Find artists that are similar to 'Bikini Kill': 36 | 37 | ```python 38 | from pyechonest import artist 39 | bk = artist.Artist('bikini kill') 40 | print "Artists similar to: %s:" % (bk.name,) 41 | for similar_artist in bk.similar: print "\t%s" % (similar_artist.name,) 42 | ``` 43 | 44 | Search for artist: 45 | ```python 46 | from pyechonest import artist 47 | weezer_results = artist.search(name='weezer') 48 | weezer = weezer_results[0] 49 | weezer_blogs = weezer.blogs 50 | print 'Blogs about weezer:', [blog.get('url') for blog in weezer_blogs] 51 | ``` 52 | 53 | Get an artist by name: 54 | ```python 55 | from pyechonest import artist 56 | a = artist.Artist('lady gaga') 57 | print a.id 58 | ``` 59 | 60 | Get an artist by Musicbrainz ID: 61 | ```python 62 | from pyechonest import artist 63 | a = artist.Artist('musicbrainz:artist:a74b1b7f-71a5-4011-9441-d0b5e4122711') 64 | print a.name 65 | ``` 66 | 67 | Get the top hottt artists: 68 | ```python 69 | from pyechonest import artist 70 | for hottt_artist in artist.top_hottt(): 71 | print hottt_artist.name, hottt_artist.hotttnesss 72 | ``` 73 | 74 | Search for songs: 75 | ```python 76 | from pyechonest import song 77 | rkp_results = song.search(artist='radiohead', title='karma police') 78 | karma_police = rkp_results[0] 79 | print karma_police.artist_location 80 | print 'tempo:',karma_police.audio_summary['tempo'],'duration:',karma_police.audio_summary['duration'] 81 | ``` 82 | 83 | Get a song's audio_url and analysis_url: 84 | ```python 85 | from pyechonest import song 86 | ss_results = song.search(artist='the national', title='slow show', buckets=['id:7digital-US', 'tracks'], limit=True) 87 | slow_show = ss_results[0] 88 | ss_tracks = slow_show.get_tracks('7digital-US') 89 | print ss_tracks[0].get('preview_url') 90 | ``` 91 | 92 | -![alt text](http://i.imgur.com/WWLYo.gif "Frustrated cat can't believe this is the 12th time he's clicked on an auto-linked README.md URL") 93 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Rev 8.0.0 to 8.0.1 2 | 3 | EchoNestAPIError now reports the HTTP status code of the failed request. 4 | 5 | Rev 7.1.1 to 8.0.0 6 | 7 | Delete old deprecated playlist functions. 8 | Support new Track attributes 'valence' and 'acousticness'. 9 | No longer return Track attributes as 0 if they don't exist on the track. 10 | Audio attributes can now be None. 11 | No longer always get track analysis details (bars, beats, meta, etc.). You now 12 | need to call Track.get_analysis() to populate those attributes. See track.py for 13 | details. (This makes track.track_from_XXX methods much faster for most uses.) 14 | Handle all 2XX, 4XX, and 5XX response codes from the API. 15 | Fix some examples for Tracks from mp3s. Use 7digital-US as preview catalog. 16 | 17 | Rev 7.1.0 to 7.1.1 18 | 19 | Merge pull request #18 from njl/create_catalog_by_name_factory_function 20 | 21 | Factory function to create a catalog by name, avoiding the profile call. 22 | Adds a factory function to create a catalog by name, avoiding the profile call. 23 | 24 | add list_genres 25 | genres fix in proxies.py 26 | genres field 27 | Merge branch 'master' of github.com:echonest/pyechonest 28 | 29 | do not do logger.basicConfig 30 | 31 | Merge pull request #14 from alex/patch-1 32 | 33 | Removed print statements 34 | 35 | Update pyechonest/catalog.py 36 | 37 | get_catalog_by_name 38 | Returning cat. 39 | fix up long_description, README.md 40 | 41 | add support for Catalog.keyvalues(); version upped to 7.1.0 42 | 43 | CHANGELOG and setup.py updates 44 | 45 | Merge branch 'master' of github.com:echonest/pyechonest 46 | 47 | don't wrap in a try/except, let the exception bubble up 48 | 49 | Merge pull request #13 from sebpiq/master 50 | 51 | minor doc fix 52 | small doc fix 53 | dont crash if total isn't provided 54 | Add some current contributors 55 | bump version + update CHANGELOG 56 | 57 | Deprecate the legacy Playlist, move BetaPlaylist to Playlist, add in support for new session catalog feature 58 | 59 | more doc and .gitignore tweaks 60 | 61 | fix up some minor doc issues 62 | 63 | Delete old GITMOVE doc file. 64 | 65 | bump version + update CHANGELOG 66 | 67 | Bump version number for deletion of track/analyze methods. 68 | 69 | Fixup comment and code in get_song_type. Improve/update comments on audio profile. 70 | 71 | Eliminate track/analyze methods track_from_reanalyzing_id and track_from_reanalyzing_md5. Merge pull requests for timeout handling on profile calls. Fixup exception messages. 72 | 73 | Merge pull request #12 from andreasjansson/master 74 | 75 | Respect timeouts in track._wait_for_pending_track. Improve exception strings. 76 | respect timeout in _wait_for_pending_track 77 | 78 | This really bothered me, sorry 79 | 80 | list_catalogs instead of list, resultlist response from catalog/list 81 | 82 | bump version 83 | 84 | don't override builtin list, return ResultList for catalog list call 85 | 86 | Update changelog, bump version to 4.3 87 | 88 | Support for song_type in song and playlist; bump version, update changelog 89 | 90 | bump version 91 | 92 | add a changelog, fix some spacing 93 | 94 | debugging: The 'info' playlist call is unnecessary unless a user wants debug information 95 | 96 | exception-handling: Encapsulate URL/HTTP errors in a new general class 97 | Added EchoNestException and EchoNestIOError so that a user can use a single point for exception handling. 98 | Note, I maintained the self.code, which was truncated before. 99 | 100 | Merge pull request #7 from psobot/patch-1 101 | 102 | Renamed call to non-existent function (_track_from_string) in exception handler. 103 | Fixed bad function name (_track_from_string) 104 | Make all track/analyze calls async. 105 | 106 | Guard against empty song results when getting an audio summary. 107 | 108 | Bump version to 4.2.21 109 | 110 | Support tracks that do not have audio analysis. 111 | 112 | Adding twitter functionality. 113 | 114 | Adding qupdate functionality and get_item_dicts(). 115 | 116 | add manifest, bump release 117 | 118 | beta playlist support for new dynamic playlist api, move version parameter to a single place 119 | 120 | Added distribution parameter to static playlists, some extra info when errors are thrown, and basic playlist docs. 121 | 122 | Add speechiness 123 | 124 | Add sandbox methods. 125 | 126 | don't choke if an artist does not have songs. 127 | Don't send None to server 128 | 129 | Fix item_id bug. 130 | 131 | Add track_id parameters to playlist and song profile calls, add adventurousness, and catalog/read item ids. 132 | 133 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyechonest.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyechonest.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyechonest" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyechonest" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /doc/source/_templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% set title = 'Overview' %} 3 | {% block body %} 4 | 125 | 126 | 131 | 132 | 133 |

    Welcome to Pyechonest!

    134 | 135 |

    136 | Tap into The Echo Nest's Musical Brain for the best music search, information, recommendations and remix tools on the web. 137 |

    138 |

    139 | Pyechonest is an open source Python library for the Echo Nest API. With Pyechonest you have Python access to the entire set of API methods including: 140 |

    141 |
      142 |
    • artist - search for artists by name, description, or attribute, and get back detailed information about any artist including audio, similar artists, blogs, familiarity, hotttnesss, news, reviews, urls and video.
    • 143 |
    • song - search songs by artist, title, description, or attribute (tempo, duration, etc) and get detailed information back about each song, such as hotttnesss, audio_summary, or tracks.
    • 144 |
    • track - upload a track to the Echo Nest and receive summary information about the track including key, duration, mode, tempo, time signature along with detailed track info including timbre, pitch, rhythm and loudness information.
    • 145 |
    146 | 147 |

    Documentation

    148 | 149 | 150 | 161 |
    151 | 153 | 155 | 156 | 158 | 160 |
    162 | 163 |

    Examples

    164 |

    Some simple examples can be found in the 165 | README. 166 |

    167 |

    168 | For more complex examples of what you can make with pyechonest, check out the bundled examples. 169 |

    170 | 171 |

    Get Pyechonest

    172 |

    173 | Pyechonest is available as an easy-installable 175 | package on the Python Package 176 | Index. You can install it by running: 177 |

    178 |

    179 | "easy_install pyechonest" 180 | 181 |

    182 |

    The code can be found in a git repository, at 183 | https://github.com/echonest/pyechonest.

    184 | 185 | {% endblock %} 186 | -------------------------------------------------------------------------------- /examples/try_new_things.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-09-01 7 | 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2, or (at your option) 11 | # any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | """ 18 | 19 | # ======================== 20 | # = try_new_things.py = 21 | # ======================== 22 | # 23 | # enter a few of your favorite artists and create a playlist of new music that 24 | # you might like. 25 | # 26 | 27 | import sys, os, logging 28 | import xml.sax.saxutils as saxutils 29 | from optparse import OptionParser 30 | from pyechonest import artist, playlist 31 | 32 | # set your api key here if it's not set in the environment 33 | # config.ECHO_NEST_API_KEY = "XXXXXXXXXXXXXXXXX" 34 | logger = logging.getLogger(__name__) 35 | 36 | class XmlWriter(object): 37 | """ code from: http://users.musicbrainz.org/~matt/xspf/m3u2xspf 38 | Copyright (c) 2006, Matthias Friedrich 39 | """ 40 | def __init__(self, outStream, indentAmount=' '): 41 | self._out = outStream 42 | self._indentAmount = indentAmount 43 | self._stack = [ ] 44 | 45 | def prolog(self, encoding='UTF-8', version='1.0'): 46 | pi = '' % (version, encoding) 47 | self._out.write(pi + '\n') 48 | 49 | def start(self, name, attrs={ }): 50 | indent = self._getIndention() 51 | self._stack.append(name) 52 | self._out.write(indent + self._makeTag(name, attrs) + '\n') 53 | 54 | def end(self): 55 | name = self._stack.pop() 56 | indent = self._getIndention() 57 | self._out.write('%s\n' % (indent, name)) 58 | 59 | def elem(self, name, value, attrs={ }): 60 | # delete attributes with an unset value 61 | for (k, v) in attrs.items(): 62 | if v is None or v == '': 63 | del attrs[k] 64 | 65 | if value is None or value == '': 66 | if len(attrs) == 0: 67 | return 68 | self._out.write(self._getIndention()) 69 | self._out.write(self._makeTag(name, attrs, True) + '\n') 70 | else: 71 | escValue = saxutils.escape(value or '') 72 | self._out.write(self._getIndention()) 73 | self._out.write(self._makeTag(name, attrs)) 74 | self._out.write(escValue) 75 | self._out.write('\n' % name) 76 | 77 | def _getIndention(self): 78 | return self._indentAmount * len(self._stack) 79 | 80 | def _makeTag(self, name, attrs={ }, close=False): 81 | ret = '<' + name 82 | 83 | for (k, v) in attrs.iteritems(): 84 | if v is not None: 85 | v = saxutils.quoteattr(str(v)) 86 | ret += ' %s=%s' % (k, v) 87 | 88 | if close: 89 | return ret + '/>' 90 | else: 91 | return ret + '>' 92 | 93 | 94 | 95 | def write_xspf(f, tuples): 96 | """send me a list of (artist,title,mp3_url)""" 97 | xml = XmlWriter(f, indentAmount=' ') 98 | xml.prolog() 99 | xml.start('playlist', { 'xmlns': 'http://xspf.org/ns/0/', 'version': '1' }) 100 | xml.start('trackList') 101 | for tupe in tuples: 102 | xml.start('track') 103 | xml.elem('creator',tupe[0]) 104 | xml.elem('title',tupe[1]) 105 | xml.elem('location', tupe[2]) 106 | xml.end() 107 | xml.end() 108 | xml.end() 109 | f.close() 110 | 111 | 112 | def lookup_seeds(seed_artist_names): 113 | seed_ids = [] 114 | for artist_name in seed_artist_names: 115 | try: 116 | seed_ids.append("%s" % (artist.Artist(artist_name).id,)) 117 | except Exception: 118 | logger.info('artist "%s" not found.' % (artist_name,)) 119 | # we could try to do full artist search here 120 | # and let them choose the right artist 121 | logger.info('seed_ids: %s' % (seed_ids,)) 122 | return seed_ids 123 | 124 | 125 | def find_playlist(seed_artist_ids, playable=False): 126 | if playable: 127 | logger.info("finding playlist with audio...") 128 | p = playlist.static(type='artist-radio', artist_id=seed_artist_ids, variety=1, buckets=['id:7digital-US', 'tracks'], limit=True) 129 | else: 130 | logger.info("finding playlist without audio...") 131 | p = playlist.static(type='artist-radio', artist_id=seed_artist_ids, variety=1) 132 | return p 133 | 134 | 135 | 136 | if __name__ == "__main__": 137 | usage = 'usage: %prog [options] "artist 1" "artist 2" ... "artist N"\n\n' \ 138 | 'example:\n' \ 139 | '\t ./%prog "arcade fire" "feist" "broken social scene" -x -f arcade_feist_scene.xspf\n' \ 140 | '\t ./%prog "justice" "four tet" "bitshifter" -v\n' 141 | 142 | parser = OptionParser(usage=usage) 143 | parser.add_option("-v", "--verbose", 144 | action="store_true", dest="verbose", default=False, 145 | help="say what you're doing") 146 | parser.add_option("-a", "--audio", 147 | action="store_true", dest="audio", default=False, 148 | help="fetch sample audio for songs") 149 | parser.add_option("-x", "--xspf", 150 | action="store_true", dest="xspf", default=False, 151 | help="output an xspf format playlist") 152 | parser.add_option("-f", "--filename", 153 | metavar="FILE", help="write output to FILE") 154 | 155 | (options, args) = parser.parse_args() 156 | if len(args) < 1: 157 | parser.error("you must provide at least 1 seed artist!") 158 | 159 | # handle verbose logging 160 | log_level = logging.ERROR 161 | if options.verbose: 162 | log_level = logging.INFO 163 | logging.basicConfig(level=log_level) 164 | logger.setLevel(log_level) 165 | 166 | # make sure output file doesn't already exist 167 | if options.filename and os.path.exists(options.filename): 168 | logger.error("The file path: %s already exists." % (options.filename,)) 169 | sys.exit(1) 170 | 171 | # resolve seed artists 172 | seed_ids = lookup_seeds(args) 173 | 174 | # find playlist 175 | raw_plist = find_playlist(seed_ids, playable=(options.audio or options.xspf)) 176 | 177 | tuple_plist = [] 178 | for s in raw_plist: 179 | name = s.artist_name 180 | title = s.title 181 | url = "" 182 | if options.audio: 183 | url = s.get_tracks('7digital-US', [{}])[0].get('preview_url') 184 | tuple_plist.append((name,title,url)) 185 | 186 | # write to stdout or file specified 187 | fout = open(options.filename, 'w') if options.filename else sys.stdout 188 | if options.xspf: 189 | write_xspf(fout, tuple_plist) 190 | else: 191 | for tupe in tuple_plist: 192 | fout.write("%s - %s \t %s\n" % tupe) 193 | logger.info("all done!") 194 | sys.exit(0) -------------------------------------------------------------------------------- /pyechonest/proxies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-04-25. 7 | """ 8 | import util 9 | 10 | class ResultList(list): 11 | def __init__(self, li, start=0, total=0): 12 | self.extend(li) 13 | self.start = start 14 | if total == 0: 15 | total = len(li) 16 | self.total = total 17 | 18 | class GenericProxy(object): 19 | def __init__(self): 20 | self.cache = {} 21 | 22 | def get_attribute(self, method_name, **kwargs): 23 | result = util.callm("%s/%s" % (self._object_type, method_name), kwargs) 24 | return result['response'] 25 | 26 | def post_attribute(self, method_name, **kwargs): 27 | data = kwargs.pop('data') if 'data' in kwargs else {} 28 | result = util.callm("%s/%s" % (self._object_type, method_name), kwargs, POST=True, data=data) 29 | return result['response'] 30 | 31 | 32 | class ArtistProxy(GenericProxy): 33 | def __init__(self, identifier, buckets = None, **kwargs): 34 | super(ArtistProxy, self).__init__() 35 | buckets = buckets or [] 36 | self.id = identifier 37 | self._object_type = 'artist' 38 | kwargs = dict((str(k), v) for (k,v) in kwargs.iteritems()) 39 | # the following are integral to all artist objects... the rest is up to you! 40 | core_attrs = ['name'] 41 | 42 | if not all(ca in kwargs for ca in core_attrs): 43 | profile = self.get_attribute('profile', **{'bucket':buckets}) 44 | kwargs.update(profile.get('artist')) 45 | [self.__dict__.update({ca:kwargs.pop(ca)}) for ca in core_attrs+['id'] if ca in kwargs] 46 | self.cache.update(kwargs) 47 | 48 | def get_attribute(self, *args, **kwargs): 49 | if util.short_regex.match(self.id) or util.long_regex.match(self.id) or util.foreign_regex.match(self.id): 50 | kwargs['id'] = self.id 51 | else: 52 | kwargs['name'] = self.id 53 | return super(ArtistProxy, self).get_attribute(*args, **kwargs) 54 | 55 | 56 | class CatalogProxy(GenericProxy): 57 | def __init__(self, identifier, type, buckets = None, **kwargs): 58 | super(CatalogProxy, self).__init__() 59 | buckets = buckets or [] 60 | self.id = identifier 61 | self._object_type = 'catalog' 62 | kwargs = dict((str(k), v) for (k,v) in kwargs.iteritems()) 63 | # the following are integral to all catalog objects... the rest is up to you! 64 | core_attrs = ['name'] 65 | if not all(ca in kwargs for ca in core_attrs): 66 | if util.short_regex.match(self.id) or util.long_regex.match(self.id) or util.foreign_regex.match(self.id): 67 | profile = self.get_attribute('profile') 68 | kwargs.update(profile['catalog']) 69 | else: 70 | if not type: 71 | raise Exception('You must specify a "type"!') 72 | try: 73 | profile = self.get_attribute('profile') 74 | existing_type = profile['catalog'].get('type', 'Unknown') 75 | if type != existing_type: 76 | raise Exception("Catalog type requested (%s) does not match existing catalog type (%s)" % (type, existing_type)) 77 | 78 | kwargs.update(profile['catalog']) 79 | except util.EchoNestAPIError: 80 | profile = self.post_attribute('create', type=type, **kwargs) 81 | kwargs.update(profile) 82 | [self.__dict__.update({ca:kwargs.pop(ca)}) for ca in core_attrs+['id'] if ca in kwargs] 83 | self.cache.update(kwargs) 84 | 85 | def get_attribute_simple(self, *args, **kwargs): 86 | # omit name/id kwargs for this call 87 | return super(CatalogProxy, self).get_attribute(*args, **kwargs) 88 | 89 | def get_attribute(self, *args, **kwargs): 90 | if util.short_regex.match(self.id) or util.long_regex.match(self.id) or util.foreign_regex.match(self.id): 91 | kwargs['id'] = self.id 92 | else: 93 | kwargs['name'] = self.id 94 | return super(CatalogProxy, self).get_attribute(*args, **kwargs) 95 | 96 | def post_attribute(self, *args, **kwargs): 97 | if util.short_regex.match(self.id) or util.long_regex.match(self.id) or util.foreign_regex.match(self.id): 98 | kwargs['id'] = self.id 99 | else: 100 | kwargs['name'] = self.id 101 | return super(CatalogProxy, self).post_attribute(*args, **kwargs) 102 | 103 | 104 | class PlaylistProxy(GenericProxy): 105 | def __init__(self, session_id = None, buckets = None, **kwargs): 106 | super(PlaylistProxy, self).__init__() 107 | core_attrs = ['session_id'] 108 | self._object_type = 'playlist' 109 | if session_id: 110 | self.session_id=session_id 111 | else: 112 | buckets = buckets or [] 113 | kwargs['bucket'] = buckets 114 | kwargs['genre'] = kwargs['genres'] 115 | del kwargs['genres'] 116 | kwargs = dict((str(k), v) for (k,v) in kwargs.iteritems()) 117 | 118 | if not all(ca in kwargs for ca in core_attrs): 119 | kwargs = dict((str(k), v) for (k,v) in kwargs.iteritems()) 120 | profile = self.get_attribute('create', **kwargs) 121 | kwargs.update(profile) 122 | [self.__dict__.update({ca:kwargs.pop(ca)}) for ca in core_attrs if ca in kwargs] 123 | self.cache.update(kwargs) 124 | 125 | def get_attribute(self, method, **kwargs): 126 | return super(PlaylistProxy, self).get_attribute('dynamic/' + method, **kwargs) 127 | 128 | class SongProxy(GenericProxy): 129 | def __init__(self, identifier, buckets = None, **kwargs): 130 | super(SongProxy, self).__init__() 131 | buckets = buckets or [] 132 | self.id = identifier 133 | self._object_type = 'song' 134 | kwargs = dict((str(k), v) for (k,v) in kwargs.iteritems()) 135 | 136 | # BAW -- this is debug output from identify that returns a track_id. i am not sure where else to access this.. 137 | if kwargs.has_key("track_id"): 138 | self.track_id = kwargs["track_id"] 139 | if kwargs.has_key("tag"): 140 | self.tag = kwargs["tag"] 141 | if kwargs.has_key("score"): 142 | self.score = kwargs["score"] 143 | if kwargs.has_key('audio'): 144 | self.audio = kwargs['audio'] 145 | if kwargs.has_key('release_image'): 146 | self.release_image = kwargs['release_image'] 147 | 148 | # the following are integral to all song objects... the rest is up to you! 149 | core_attrs = ['title', 'artist_name', 'artist_id'] 150 | 151 | if not all(ca in kwargs for ca in core_attrs): 152 | profile = self.get_attribute('profile', **{'id':self.id, 'bucket':buckets}) 153 | kwargs.update(profile.get('songs')[0]) 154 | [self.__dict__.update({ca:kwargs.pop(ca)}) for ca in core_attrs] 155 | self.cache.update(kwargs) 156 | 157 | def get_attribute(self, *args, **kwargs): 158 | kwargs['id'] = self.id 159 | return super(SongProxy, self).get_attribute(*args, **kwargs) 160 | 161 | 162 | class TrackProxy(GenericProxy): 163 | def __init__(self, identifier, md5, properties): 164 | """ 165 | You should not call this constructor directly, rather use the convenience functions 166 | that are in track.py. For example, call track.track_from_filename 167 | Let's always get the bucket `audio_summary` 168 | """ 169 | super(TrackProxy, self).__init__() 170 | self.id = identifier 171 | self.md5 = md5 172 | self.analysis_url = None 173 | self._object_type = 'track' 174 | self.__dict__.update(properties) 175 | 176 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyechonest documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Sep 30 15:51:03 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os, inspect 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | sys.path.insert(0,os.path.abspath("../../pyechonest")) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'contents' 42 | 43 | # General information about the project. 44 | project = u'pyechonest' 45 | copyright = u'2013, The Echo Nest' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '8.0.0' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '8.0.0' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = [] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'default' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | #html_theme_options = {} 101 | 102 | # Add any paths that contain custom themes here, relative to this directory. 103 | # html_theme_path = ['themes/'] 104 | 105 | # The name for this set of Sphinx documents. If None, it defaults to 106 | # " v documentation". 107 | #html_title = None 108 | 109 | # A shorter title for the navigation bar. Default is the same as html_title. 110 | #html_short_title = None 111 | 112 | # The name of an image file (relative to this directory) to place at the top 113 | # of the sidebar. 114 | html_logo = '200x160_lt.png' 115 | 116 | # The name of an image file (within the static path) to use as favicon of the 117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 118 | # pixels large. 119 | #html_favicon = None 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | # html_static_path = ['_static'] 125 | 126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 127 | # using the given strftime format. 128 | #html_last_updated_fmt = '%b %d, %Y' 129 | 130 | # If true, SmartyPants will be used to convert quotes and dashes to 131 | # typographically correct entities. 132 | #html_use_smartypants = True 133 | 134 | # Custom sidebar templates, maps document names to template names. 135 | #html_sidebars = {} 136 | 137 | # Additional templates that should be rendered to pages, maps page names to 138 | # template names. 139 | html_additional_pages = { 140 | "index": "index.html", 141 | } 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'pyechonestdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | # The paper size ('letter' or 'a4'). 176 | #latex_paper_size = 'letter' 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #latex_font_size = '10pt' 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ('index', 'pyechonest.tex', u'pyechonest Documentation', 185 | u'The Echo Nest', 'manual'), 186 | ] 187 | 188 | # The name of an image file (relative to this directory) to place at the top of 189 | # the title page. 190 | #latex_logo = None 191 | 192 | # For "manual" documents, if this is true, then toplevel headings are parts, 193 | # not chapters. 194 | #latex_use_parts = False 195 | 196 | # If true, show page references after internal links. 197 | #latex_show_pagerefs = False 198 | 199 | # If true, show URL addresses after external links. 200 | #latex_show_urls = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'pyechonest', u'pyechonest Documentation', 218 | [u'The Echo Nest'], 1) 219 | ] 220 | 221 | 222 | # -- Options for Epub output --------------------------------------------------- 223 | 224 | # Bibliographic Dublin Core info. 225 | epub_title = u'pyechonest' 226 | epub_author = u'The Echo Nest' 227 | epub_publisher = u'The Echo Nest' 228 | epub_copyright = u'2012, The Echo Nest' 229 | 230 | # The language of the text. It defaults to the language option 231 | # or en if the language is not set. 232 | #epub_language = '' 233 | 234 | # The scheme of the identifier. Typical schemes are ISBN or URL. 235 | #epub_scheme = '' 236 | 237 | # The unique identifier of the text. This can be a ISBN number 238 | # or the project homepage. 239 | #epub_identifier = '' 240 | 241 | # A unique identification for the text. 242 | #epub_uid = '' 243 | 244 | # HTML files that should be inserted before the pages created by sphinx. 245 | # The format is a list of tuples containing the path and title. 246 | #epub_pre_files = [] 247 | 248 | # HTML files shat should be inserted after the pages created by sphinx. 249 | # The format is a list of tuples containing the path and title. 250 | #epub_post_files = [] 251 | 252 | # A list of files that should not be packed into the epub file. 253 | #epub_exclude_files = [] 254 | 255 | # The depth of the table of contents in toc.ncx. 256 | #epub_tocdepth = 3 257 | 258 | # Allow duplicate toc entries. 259 | #epub_tocdup = True 260 | 261 | 262 | # Example configuration for intersphinx: refer to the Python standard library. 263 | intersphinx_mapping = {'http://docs.python.org/': None} 264 | 265 | # don't document the properties! 266 | def maybe_skip_member(app, what, name, obj, skip, options): 267 | if what == 'module': 268 | return False 269 | else: 270 | return not inspect.ismethod(obj) 271 | 272 | def setup(app): 273 | app.connect('autodoc-skip-member', maybe_skip_member) 274 | -------------------------------------------------------------------------------- /pyechonest/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-04-25. 7 | 8 | Utility functions to support the Echo Nest web API interface. 9 | """ 10 | import urllib 11 | import urllib2 12 | import httplib 13 | import config 14 | import logging 15 | import socket 16 | import re 17 | import time 18 | import os 19 | import subprocess 20 | import traceback 21 | from types import StringType, UnicodeType 22 | 23 | try: 24 | import json 25 | except ImportError: 26 | import simplejson as json 27 | 28 | logger = logging.getLogger(__name__) 29 | TYPENAMES = ( 30 | ('AR', 'artist'), 31 | ('SO', 'song'), 32 | ('RE', 'release'), 33 | ('TR', 'track'), 34 | ('PE', 'person'), 35 | ('DE', 'device'), 36 | ('LI', 'listener'), 37 | ('ED', 'editor'), 38 | ('TW', 'tweditor'), 39 | ('CA', 'catalog'), 40 | ) 41 | foreign_regex = re.compile(r'^.+?:(%s):([^^]+)\^?([0-9\.]+)?' % r'|'.join(n[1] for n in TYPENAMES)) 42 | short_regex = re.compile(r'^((%s)[0-9A-Z]{16})\^?([0-9\.]+)?' % r'|'.join(n[0] for n in TYPENAMES)) 43 | 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))) 44 | headers = [('User-Agent', 'Pyechonest %s' % (config.__version__,))] 45 | 46 | class MyBaseHandler(urllib2.BaseHandler): 47 | def default_open(self, request): 48 | if config.TRACE_API_CALLS: 49 | logger.info("%s" % (request.get_full_url(),)) 50 | request.start_time = time.time() 51 | return None 52 | 53 | class MyErrorProcessor(urllib2.HTTPErrorProcessor): 54 | def http_response(self, request, response): 55 | code = response.code 56 | if config.TRACE_API_CALLS: 57 | logger.info("took %2.2fs: (%i)" % (time.time()-request.start_time,code)) 58 | if code/100 in (2, 4, 5): 59 | return response 60 | else: 61 | urllib2.HTTPErrorProcessor.http_response(self, request, response) 62 | 63 | opener = urllib2.build_opener(MyBaseHandler(), MyErrorProcessor()) 64 | opener.addheaders = headers 65 | 66 | class EchoNestException(Exception): 67 | """ 68 | Parent exception class. Catches API and URL/HTTP errors. 69 | """ 70 | def __init__(self, code, message, headers): 71 | if code is None: 72 | code = -1 73 | message = 'Echo Nest Unknown Error' 74 | 75 | if message is None: 76 | super(EchoNestException, self).__init__('Echo Nest Error %d' % code,) 77 | else: 78 | super(EchoNestException, self).__init__(message,) 79 | self.headers = headers 80 | self.code = code 81 | 82 | class EchoNestAPIError(EchoNestException): 83 | """ 84 | API Specific Errors. 85 | """ 86 | def __init__(self, code, message, headers, http_status): 87 | if http_status: 88 | http_status_message_part = ' [HTTP %d]' % http_status 89 | else: 90 | http_status_message_part = '' 91 | self.http_status = http_status 92 | 93 | formatted_message = ('Echo Nest API Error %d: %s%s' % 94 | (code, message, http_status_message_part),) 95 | super(EchoNestAPIError, self).__init__(code, formatted_message, headers) 96 | 97 | class EchoNestIOError(EchoNestException): 98 | """ 99 | URL and HTTP errors. 100 | """ 101 | def __init__(self, code=None, error=None, headers=headers): 102 | formatted_message = ('Echo Nest IOError: %s' % headers,) 103 | super(EchoNestIOError, self).__init__(code, formatted_message, headers) 104 | 105 | def get_successful_response(raw_json): 106 | if hasattr(raw_json, 'headers'): 107 | headers = raw_json.headers 108 | else: 109 | headers = {'Headers':'No Headers'} 110 | if hasattr(raw_json, 'getcode'): 111 | http_status = raw_json.getcode() 112 | else: 113 | http_status = None 114 | raw_json = raw_json.read() 115 | try: 116 | response_dict = json.loads(raw_json) 117 | status_dict = response_dict['response']['status'] 118 | code = int(status_dict['code']) 119 | message = status_dict['message'] 120 | if (code != 0): 121 | # do some cute exception handling 122 | raise EchoNestAPIError(code, message, headers, http_status) 123 | del response_dict['response']['status'] 124 | return response_dict 125 | except ValueError: 126 | logger.debug(traceback.format_exc()) 127 | raise EchoNestAPIError(-1, "Unknown error.", headers, http_status) 128 | 129 | 130 | # These two functions are to deal with the unknown encoded output of codegen (varies by platform and ID3 tag) 131 | def reallyunicode(s, encoding="utf-8"): 132 | if type(s) is StringType: 133 | for args in ((encoding,), ('utf-8',), ('latin-1',), ('ascii', 'replace')): 134 | try: 135 | s = s.decode(*args) 136 | break 137 | except UnicodeDecodeError: 138 | continue 139 | if type(s) is not UnicodeType: 140 | raise ValueError, "%s is not a string at all." % s 141 | return s 142 | 143 | def reallyUTF8(s): 144 | return reallyunicode(s).encode("utf-8") 145 | 146 | def codegen(filename, start=0, duration=30): 147 | # Run codegen on the file and return the json. If start or duration is -1 ignore them. 148 | cmd = config.CODEGEN_BINARY_OVERRIDE 149 | if not cmd: 150 | # Is this is posix platform, or is it windows? 151 | if hasattr(os, 'uname'): 152 | if(os.uname()[0] == "Darwin"): 153 | cmd = "codegen.Darwin" 154 | else: 155 | cmd = 'codegen.'+os.uname()[0]+'-'+os.uname()[4] 156 | else: 157 | cmd = "codegen.windows.exe" 158 | 159 | if not os.path.exists(cmd): 160 | raise Exception("Codegen binary not found.") 161 | 162 | command = cmd + " \"" + filename + "\" " 163 | if start >= 0: 164 | command = command + str(start) + " " 165 | if duration >= 0: 166 | command = command + str(duration) 167 | 168 | p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 169 | (json_block, errs) = p.communicate() 170 | json_block = reallyUTF8(json_block) 171 | 172 | try: 173 | return json.loads(json_block) 174 | except ValueError: 175 | logger.debug("No JSON object came out of codegen: error was %s" % (errs)) 176 | return None 177 | 178 | 179 | def callm(method, param_dict, POST=False, socket_timeout=None, data=None): 180 | """ 181 | Call the api! 182 | Param_dict is a *regular* *python* *dictionary* so if you want to have multi-valued params 183 | put them in a list. 184 | 185 | ** note, if we require 2.6, we can get rid of this timeout munging. 186 | """ 187 | try: 188 | param_dict['api_key'] = config.ECHO_NEST_API_KEY 189 | param_list = [] 190 | if not socket_timeout: 191 | socket_timeout = config.CALL_TIMEOUT 192 | 193 | for key,val in param_dict.iteritems(): 194 | if isinstance(val, list): 195 | param_list.extend( [(key,subval) for subval in val] ) 196 | elif val is not None: 197 | if isinstance(val, unicode): 198 | val = val.encode('utf-8') 199 | param_list.append( (key,val) ) 200 | 201 | params = urllib.urlencode(param_list) 202 | 203 | orig_timeout = socket.getdefaulttimeout() 204 | socket.setdefaulttimeout(socket_timeout) 205 | 206 | if(POST): 207 | if (not method == 'track/upload') or ((method == 'track/upload') and 'url' in param_dict): 208 | """ 209 | this is a normal POST call 210 | """ 211 | url = 'http://%s/%s/%s/%s' % (config.API_HOST, config.API_SELECTOR, 212 | config.API_VERSION, method) 213 | 214 | if data is None: 215 | data = '' 216 | data = urllib.urlencode(data) 217 | data = "&".join([data, params]) 218 | 219 | f = opener.open(url, data=data) 220 | else: 221 | """ 222 | upload with a local file is special, as the body of the request is the content of the file, 223 | and the other parameters stay on the URL 224 | """ 225 | url = '/%s/%s/%s?%s' % (config.API_SELECTOR, config.API_VERSION, 226 | method, params) 227 | 228 | if ':' in config.API_HOST: 229 | host, port = config.API_HOST.split(':') 230 | else: 231 | host = config.API_HOST 232 | port = 80 233 | 234 | if config.TRACE_API_CALLS: 235 | logger.info("%s/%s" % (host+':'+str(port), url,)) 236 | conn = httplib.HTTPConnection(host, port = port) 237 | conn.request('POST', url, body = data, headers = dict([('Content-Type', 'application/octet-stream')]+headers)) 238 | f = conn.getresponse() 239 | 240 | else: 241 | """ 242 | just a normal GET call 243 | """ 244 | url = 'http://%s/%s/%s/%s?%s' % (config.API_HOST, config.API_SELECTOR, config.API_VERSION, 245 | method, params) 246 | 247 | f = opener.open(url) 248 | 249 | socket.setdefaulttimeout(orig_timeout) 250 | 251 | # try/except 252 | response_dict = get_successful_response(f) 253 | return response_dict 254 | 255 | except IOError, e: 256 | if hasattr(e, 'reason'): 257 | raise EchoNestIOError(error=e.reason) 258 | elif hasattr(e, 'code'): 259 | raise EchoNestIOError(code=e.code) 260 | else: 261 | raise 262 | 263 | def oauthgetm(method, param_dict, socket_timeout=None): 264 | try: 265 | import oauth2 # lazy import this so oauth2 is not a hard dep 266 | except ImportError: 267 | raise Exception("You must install the python-oauth2 library to use this method.") 268 | 269 | """ 270 | Call the api! With Oauth! 271 | Param_dict is a *regular* *python* *dictionary* so if you want to have multi-valued params 272 | put them in a list. 273 | 274 | ** note, if we require 2.6, we can get rid of this timeout munging. 275 | """ 276 | def build_request(url): 277 | params = { 278 | 'oauth_version': "1.0", 279 | 'oauth_nonce': oauth2.generate_nonce(), 280 | 'oauth_timestamp': int(time.time()) 281 | } 282 | consumer = oauth2.Consumer(key=config.ECHO_NEST_CONSUMER_KEY, secret=config.ECHO_NEST_SHARED_SECRET) 283 | params['oauth_consumer_key'] = config.ECHO_NEST_CONSUMER_KEY 284 | 285 | req = oauth2.Request(method='GET', url=url, parameters=params) 286 | signature_method = oauth2.SignatureMethod_HMAC_SHA1() 287 | req.sign_request(signature_method, consumer, None) 288 | return req 289 | 290 | param_dict['api_key'] = config.ECHO_NEST_API_KEY 291 | param_list = [] 292 | if not socket_timeout: 293 | socket_timeout = config.CALL_TIMEOUT 294 | 295 | for key,val in param_dict.iteritems(): 296 | if isinstance(val, list): 297 | param_list.extend( [(key,subval) for subval in val] ) 298 | elif val is not None: 299 | if isinstance(val, unicode): 300 | val = val.encode('utf-8') 301 | param_list.append( (key,val) ) 302 | 303 | params = urllib.urlencode(param_list) 304 | 305 | orig_timeout = socket.getdefaulttimeout() 306 | socket.setdefaulttimeout(socket_timeout) 307 | """ 308 | just a normal GET call 309 | """ 310 | url = 'http://%s/%s/%s/%s?%s' % (config.API_HOST, config.API_SELECTOR, config.API_VERSION, 311 | method, params) 312 | req = build_request(url) 313 | f = opener.open(req.to_url()) 314 | 315 | socket.setdefaulttimeout(orig_timeout) 316 | 317 | # try/except 318 | response_dict = get_successful_response(f) 319 | return response_dict 320 | 321 | 322 | def postChunked(host, selector, fields, files): 323 | """ 324 | Attempt to replace postMultipart() with nearly-identical interface. 325 | (The files tuple no longer requires the filename, and we only return 326 | the response body.) 327 | Uses the urllib2_file.py originally from 328 | http://fabien.seisen.org which was also drawn heavily from 329 | http://code.activestate.com/recipes/146306/ . 330 | 331 | This urllib2_file.py is more desirable because of the chunked 332 | uploading from a file pointer (no need to read entire file into 333 | memory) and the ability to work from behind a proxy (due to its 334 | basis on urllib2). 335 | """ 336 | params = urllib.urlencode(fields) 337 | url = 'http://%s%s?%s' % (host, selector, params) 338 | u = urllib2.urlopen(url, files) 339 | result = u.read() 340 | [fp.close() for (key, fp) in files] 341 | return result 342 | 343 | 344 | def fix(x): 345 | # we need this to fix up all the dict keys to be strings, not unicode objects 346 | assert(isinstance(x,dict)) 347 | return dict((str(k), v) for (k,v) in x.iteritems()) 348 | 349 | -------------------------------------------------------------------------------- /pyechonest/catalog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Scotty Vercoe on 2010-08-25. 7 | 8 | The Catalog module loosely covers http://developer.echonest.com/docs/v4/catalog.html 9 | Refer to the official api documentation if you are unsure about something. 10 | """ 11 | try: 12 | import json 13 | except ImportError: 14 | import simplejson as json 15 | import datetime 16 | 17 | import warnings 18 | import util 19 | from proxies import CatalogProxy, ResultList 20 | import artist, song 21 | 22 | # deal with datetime in json 23 | dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None 24 | 25 | def create_catalog_by_name(name, T="general"): 26 | """ 27 | Creates a catalog object, with a given name. Does not check to see if the catalog already exists. 28 | 29 | Create a catalog object like 30 | """ 31 | result = util.callm("catalog/create", {}, POST=True, 32 | data={"name":name, "type":T}) 33 | result = result['response'] 34 | return Catalog(result['id'], **dict( (k,result[k]) for k in ('name', 'type'))) 35 | 36 | class Catalog(CatalogProxy): 37 | """ 38 | A Catalog object 39 | 40 | Attributes: 41 | id (str): Catalog ID 42 | 43 | name (str): Catalog Name 44 | 45 | read (list): A list of catalog items (objects if they are resolved, else dictionaries) 46 | 47 | feed (list): A list of dictionaries for news, blogs, reviews, audio, video for a catalog's artists 48 | 49 | Create an catalog object like so: 50 | 51 | >>> c = catalog.Catalog('CAGPXKK12BB06F9DE9') # get existing catalog 52 | >>> c = catalog.Catalog('test_song_catalog', 'song') # get existing or create new catalog 53 | 54 | """ 55 | def __init__(self, id, type=None, **kwargs): 56 | """ 57 | Create a catalog object (get a catalog by ID or get or create one given by name and type) 58 | 59 | Args: 60 | id (str): A catalog id or name 61 | 62 | Kwargs: 63 | type (str): 'song' or 'artist', specifying the catalog type 64 | 65 | Returns: 66 | A catalog object 67 | 68 | Example: 69 | 70 | >>> c = catalog.Catalog('my_songs', type='song') 71 | >>> c.id 72 | u'CAVKUPC12BCA792120' 73 | >>> c.name 74 | u'my_songs' 75 | >>> 76 | 77 | """ 78 | super(Catalog, self).__init__(id, type, **kwargs) 79 | 80 | def __repr__(self): 81 | return "<%s - %s>" % (self._object_type.encode('utf-8'), self.name.encode('utf-8')) 82 | 83 | def __str__(self): 84 | return self.name.encode('utf-8') 85 | 86 | def update(self, items): 87 | """ 88 | Update a catalog object 89 | 90 | Args: 91 | items (list): A list of dicts describing update data and action codes (see api docs) 92 | 93 | Kwargs: 94 | 95 | Returns: 96 | A ticket id 97 | 98 | Example: 99 | 100 | >>> c = catalog.Catalog('my_songs', type='song') 101 | >>> items 102 | [{'action': 'update', 103 | 'item': {'artist_name': 'dAn ThE aUtOmAtOr', 104 | 'disc_number': 1, 105 | 'genre': 'Instrumental', 106 | 'item_id': '38937DDF04BC7FC4', 107 | 'play_count': 5, 108 | 'release': 'Bombay the Hard Way: Guns, Cars & Sitars', 109 | 'song_name': 'Inspector Jay From Dehli', 110 | 'track_number': 9, 111 | 'url': 'file://localhost/Users/tylerw/Music/iTunes/iTunes%20Media/Music/Dan%20the%20Automator/Bombay%20the%20Hard%20Way_%20Guns,%20Cars%20&%20Sitars/09%20Inspector%20Jay%20From%20Dehli.m4a'}}] 112 | >>> ticket = c.update(items) 113 | >>> ticket 114 | u'7dcad583f2a38e6689d48a792b2e4c96' 115 | >>> c.status(ticket) 116 | {u'ticket_status': u'complete', u'update_info': []} 117 | >>> 118 | 119 | """ 120 | post_data = {} 121 | items_json = json.dumps(items, default=dthandler) 122 | post_data['data'] = items_json 123 | 124 | response = self.post_attribute("update", data=post_data) 125 | 126 | return response['ticket'] 127 | 128 | def status(self, ticket): 129 | """ 130 | Check the status of a catalog update 131 | 132 | Args: 133 | ticket (str): A string representing a ticket ID 134 | 135 | Kwargs: 136 | 137 | Returns: 138 | A dictionary representing ticket status 139 | 140 | Example: 141 | 142 | >>> ticket 143 | u'7dcad583f2a38e6689d48a792b2e4c96' 144 | >>> c.status(ticket) 145 | {u'ticket_status': u'complete', u'update_info': []} 146 | >>> 147 | 148 | """ 149 | return self.get_attribute_simple("status", ticket=ticket) 150 | 151 | def get_profile(self): 152 | """ 153 | Check the status of a catalog update 154 | 155 | Args: 156 | 157 | Kwargs: 158 | 159 | Returns: 160 | A dictionary representing ticket status 161 | 162 | Example: 163 | 164 | >>> c 165 | 166 | >>> c.profile() 167 | {u'id': u'CAGPXKK12BB06F9DE9', 168 | u'name': u'test_song_catalog', 169 | u'pending_tickets': [], 170 | u'resolved': 2, 171 | u'total': 4, 172 | u'type': u'song'} 173 | >>> 174 | 175 | """ 176 | result = self.get_attribute("profile") 177 | return result['catalog'] 178 | 179 | profile = property(get_profile) 180 | def read_items(self, buckets=None, results=15, start=0,item_ids=None): 181 | """ 182 | Returns data from the catalog; also expanded for the requested buckets. 183 | This method is provided for backwards-compatibility 184 | 185 | Args: 186 | 187 | Kwargs: 188 | buckets (list): A list of strings specifying which buckets to retrieve 189 | 190 | results (int): An integer number of results to return 191 | 192 | start (int): An integer starting value for the result set 193 | 194 | Returns: 195 | A list of objects in the catalog; list contains additional attributes 'start' and 'total' 196 | 197 | Example: 198 | 199 | >>> c 200 | 201 | >>> c.read_items(results=1) 202 | [] 203 | >>> 204 | """ 205 | warnings.warn("catalog.read_items() is depreciated. Please use catalog.get_item_dicts() instead.") 206 | kwargs = {} 207 | kwargs['bucket'] = buckets or [] 208 | kwargs['item_id'] = item_ids or [] 209 | response = self.get_attribute("read", results=results, start=start, **kwargs) 210 | rval = ResultList([]) 211 | if item_ids: 212 | rval.start=0; 213 | rval.total=len(response['catalog']['items']) 214 | else: 215 | rval.start = response['catalog']['start'] 216 | rval.total = response['catalog']['total'] 217 | for item in response['catalog']['items']: 218 | new_item = None 219 | # song items 220 | if 'song_id' in item: 221 | item['id'] = item.pop('song_id') 222 | item['title'] = item.pop('song_name') 223 | request = item['request'] 224 | new_item = song.Song(**util.fix(item)) 225 | new_item.request = request 226 | # artist item 227 | elif 'artist_id' in item: 228 | item['id'] = item.pop('artist_id') 229 | item['name'] = item.pop('artist_name') 230 | request = item['request'] 231 | new_item = artist.Artist(**util.fix(item)) 232 | new_item.request = request 233 | # unresolved item 234 | else: 235 | new_item = item 236 | rval.append(new_item) 237 | return rval 238 | 239 | read = property(read_items) 240 | 241 | def get_item_dicts(self, buckets=None, results=15, start=0,item_ids=None): 242 | """ 243 | Returns data from the catalog; also expanded for the requested buckets 244 | 245 | Args: 246 | 247 | Kwargs: 248 | buckets (list): A list of strings specifying which buckets to retrieve 249 | 250 | results (int): An integer number of results to return 251 | 252 | start (int): An integer starting value for the result set 253 | 254 | Returns: 255 | A list of dicts representing objects in the catalog; list has additional attributes 'start' and 'total' 256 | 257 | Example: 258 | 259 | >>> c 260 | 261 | >>> c.read_items(results=1) 262 | [ 263 | { 264 | "artist_id": "AR78KRI1187B98E6F2", 265 | "artist_name": "Art of Noise", 266 | "date_added": "2012-04-02T16:50:02", 267 | "foreign_id": "CAHLYLR13674D1CF83:song:1000", 268 | "request": { 269 | "artist_name": "The Art Of Noise", 270 | "item_id": "1000", 271 | "song_name": "Love" 272 | }, 273 | "song_id": "SOSBCTO1311AFE7AE0", 274 | "song_name": "Love" 275 | } 276 | ] 277 | """ 278 | kwargs = {} 279 | kwargs['bucket'] = buckets or [] 280 | kwargs['item_id'] = item_ids or [] 281 | response = self.get_attribute("read", results=results, start=start, **kwargs) 282 | rval = ResultList(response['catalog']['items']) 283 | if item_ids: 284 | rval.start=0; 285 | rval.total=len(response['catalog']['items']) 286 | else: 287 | rval.start = response['catalog']['start'] 288 | rval.total = response['catalog']['total'] 289 | return rval 290 | 291 | item_dicts = property(get_item_dicts) 292 | 293 | def get_feed(self, buckets=None, since=None, results=15, start=0): 294 | """ 295 | Returns feed (news, blogs, reviews, audio, video) for the catalog artists; response depends on requested buckets 296 | 297 | Args: 298 | 299 | Kwargs: 300 | buckets (list): A list of strings specifying which feed items to retrieve 301 | 302 | results (int): An integer number of results to return 303 | 304 | start (int): An integer starting value for the result set 305 | 306 | Returns: 307 | A list of news, blogs, reviews, audio or video document dicts; 308 | 309 | Example: 310 | 311 | >>> c 312 | 313 | >>> c.get_feed(results=15) 314 | {u'date_found': u'2011-02-06T07:50:25', 315 | u'date_posted': u'2011-02-06T07:50:23', 316 | u'id': u'caec686c0dff361e4c53dceb58fb9d2f', 317 | u'name': u'Linkin Park \u2013 \u201cWaiting For The End\u201d + \u201cWhen They Come For Me\u201d 2/5 SNL', 318 | u'references': [{u'artist_id': u'ARQUMH41187B9AF699', 319 | u'artist_name': u'Linkin Park'}], 320 | u'summary': u'Linkin Park performed "Waiting For The End" and "When They Come For Me" on Saturday Night Live. Watch the videos below and pick up their album A Thousand Suns on iTunes, Amazon MP3, CD Social Bookmarking ... ', 321 | u'type': u'blogs', 322 | u'url': u'http://theaudioperv.com/2011/02/06/linkin-park-waiting-for-the-end-when-they-come-for-me-25-snl/'} 323 | >>> 324 | """ 325 | kwargs = {} 326 | kwargs['bucket'] = buckets or [] 327 | if since: 328 | kwargs['since']=since 329 | response = self.get_attribute("feed", results=results, start=start, **kwargs) 330 | rval = ResultList(response['feed']) 331 | return rval 332 | 333 | feed = property(get_feed) 334 | 335 | 336 | def delete(self): 337 | """ 338 | Deletes the entire catalog 339 | 340 | Args: 341 | 342 | Kwargs: 343 | 344 | Returns: 345 | The deleted catalog's id. 346 | 347 | Example: 348 | 349 | >>> c 350 | 351 | >>> c.delete() 352 | {u'id': u'CAXGUPY12BB087A21D'} 353 | >>> 354 | 355 | """ 356 | return self.post_attribute("delete") 357 | 358 | def play(self, items, plays=None): 359 | return self.get_attribute("play", item=items, plays=plays) 360 | 361 | def skip(self, items, skips=None): 362 | return self.get_attribute("skip", item=items, skips=skips) 363 | 364 | def keyvalues(self): 365 | return self.get_attribute("keyvalues")['keyvalues'] 366 | 367 | def favorite(self, items, favorite=None): 368 | if favorite != None: 369 | favorite = str(favorite).lower() 370 | return self.get_attribute("favorite", item=items, favorite=favorite) 371 | 372 | def ban(self, items, ban=None): 373 | if ban != None: 374 | ban = str(ban).lower() 375 | return self.get_attribute("ban", item=items, ban=ban) 376 | 377 | def rate(self, items, rating=None): 378 | return self.get_attribute("rate", item=items, rating=rating) 379 | 380 | def get_catalog_by_name(name): 381 | """ 382 | Grabs a catalog by name, if its there on the api key. 383 | Otherwise, an error is thrown (mirroring the API) 384 | """ 385 | kwargs = { 386 | 'name' : name, 387 | } 388 | result = util.callm("%s/%s" % ('catalog', 'profile'), kwargs) 389 | return Catalog(**util.fix(result['response']['catalog'])) 390 | 391 | def list_catalogs(results=30, start=0): 392 | """ 393 | Returns list of all catalogs created on this API key 394 | 395 | Args: 396 | 397 | Kwargs: 398 | results (int): An integer number of results to return 399 | 400 | start (int): An integer starting value for the result set 401 | 402 | Returns: 403 | A list of catalog objects 404 | 405 | Example: 406 | 407 | >>> catalog.list_catalogs() 408 | [, , ] 409 | >>> 410 | 411 | """ 412 | result = util.callm("%s/%s" % ('catalog', 'list'), {'results': results, 'start': start}) 413 | cats = [Catalog(**util.fix(d)) for d in result['response']['catalogs']] 414 | start = result['response']['start'] 415 | total = result['response']['total'] 416 | return ResultList(cats, start, total) 417 | 418 | -------------------------------------------------------------------------------- /pyechonest/track.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | try: 3 | import json 4 | except ImportError: 5 | import simplejson as json 6 | 7 | import hashlib 8 | from proxies import TrackProxy 9 | import util 10 | import time 11 | 12 | # Seconds to wait for asynchronous track/upload or track/analyze jobs to complete. 13 | DEFAULT_ASYNC_TIMEOUT = 60 14 | 15 | class Track(TrackProxy): 16 | """ 17 | Represents an audio file and its analysis from The Echo Nest. 18 | All public methods in this module return Track objects. 19 | 20 | Depending on the information available, a Track may have some or all of the 21 | following attributes: 22 | 23 | acousticness float: confidence the track is "acoustic" (0.0 to 1.0) 24 | analysis_url URL to retrieve the complete audio analysis (time expiring) 25 | analyzer_version str: e.g. '3.01a' 26 | artist str or None: artist name 27 | artist_id Echo Nest ID of artist, if known 28 | danceability float: relative danceability (0.0 to 1.0) 29 | duration float: length of track in seconds 30 | energy float: relative energy (0.0 to 1.0) 31 | id str: Echo Nest Track ID, e.g. 'TRTOBXJ1296BCDA33B' 32 | key int: between 0 (key of C) and 11 (key of B flat) inclusive 33 | liveness float: confidence the track is "live" (0.0 to 1.0) 34 | loudness float: overall loudness in decibels (dB) 35 | md5 str: 32-character checksum of the original audio file, if available 36 | mode int: 0 (major) or 1 (minor) 37 | song_id The Echo Nest song ID for the track, if known 38 | speechiness float: likelihood the track contains speech (0.0 to 1.0) 39 | status str: analysis status, e.g. 'complete' 40 | tempo float: overall BPM (beats per minute) 41 | time_signature beats per measure (e.g. 3, 4, 5, 7) 42 | title str or None: song title 43 | valence float: a range from negative to positive emotional content (0.0 to 1.0) 44 | 45 | The following attributes are available only after calling Track.get_analysis(): 46 | 47 | analysis_channels int: the number of audio channels used during analysis 48 | analysis_sample_rate int: the sample rate used during analysis 49 | bars list of dicts: timing of each measure 50 | beats list of dicts: timing of each beat 51 | codestring ENMFP code string 52 | code_version version of ENMFP code generator 53 | decoder audio decoder used by the analysis (e.g. ffmpeg) 54 | echoprintstring fingerprint string using Echoprint (http://echoprint.me) 55 | echoprint_version version of Echoprint code generator 56 | end_of_fade_in float: time in seconds track where fade-in ends 57 | key_confidence float: confidence that key detection was accurate 58 | meta dict: other track metainfo (bitrate, album, genre, etc.) 59 | mode_confidence float: confidence that mode detection was accurate 60 | num_samples int: total samples in the decoded track 61 | offset_seconds unused, always 0 62 | sample_md5 str: 32-character checksum of the decoded audio file 63 | samplerate the audio sample rate detected in the file 64 | sections list of dicts: larger sections of song (chorus, bridge, solo, etc.) 65 | segments list of dicts: timing, pitch, loudness and timbre for each segment 66 | start_of_fade_out float: time in seconds where fade out begins 67 | synchstring string providing synchronization points throughout the track 68 | synch_version version of the synch string algorithm 69 | tatums list of dicts: the smallest metrical unit (subdivision of a beat) 70 | tempo_confidence float: confidence that tempo detection was accurate 71 | time_signature_confidence float: confidence that time_signature detection was accurate 72 | 73 | Each bar, beat, section, segment and tatum has a start time, a duration, and a confidence, 74 | in addition to whatever other data is given. 75 | 76 | Examples: 77 | 78 | >>> t = track.track_from_id('TRJSEBQ1390EC0B548') 79 | >>> t 80 | 81 | 82 | >>> t = track.track_from_md5('96fa0180d225f14e9f8cbfffbf5eb81d') 83 | >>> t 84 | 85 | >>> 86 | 87 | >>> t = track.track_from_filename('Piano Man.mp3') 88 | >>> t.meta 89 | AttributeError: 'Track' object has no attribute 'meta' 90 | >>> t.get_analysis() 91 | >>> t.meta 92 | {u'album': u'Piano Man', 93 | u'analysis_time': 8.9029500000000006, 94 | u'analyzer_version': u'3.1.3', 95 | u'artist': u'Billy Joel', 96 | u'bitrate': 160, 97 | u'detailed_status': u'OK', 98 | u'filename': u'/tmp/tmphrBQL9/fd2b524958548e7ecbaf758fb675fab1.mp3', 99 | u'genre': u'Soft Rock', 100 | u'sample_rate': 44100, 101 | u'seconds': 339, 102 | u'status_code': 0, 103 | u'timestamp': 1369400122, 104 | u'title': u'Piano Man'} 105 | >>> 106 | """ 107 | def __repr__(self): 108 | try: 109 | return "<%s - %s>" % (self._object_type.encode('utf-8'), self.title.encode('utf-8')) 110 | except AttributeError: 111 | # the title is None 112 | return "< Track >" 113 | 114 | def __str__(self): 115 | return self.title.encode('utf-8') 116 | 117 | def get_analysis(self): 118 | """ Retrieve the detailed analysis for the track, if available. 119 | Raises Exception if unable to create the detailed analysis. """ 120 | if self.analysis_url: 121 | try: 122 | # Try the existing analysis_url first. This expires shortly 123 | # after creation. 124 | try: 125 | json_string = urllib2.urlopen(self.analysis_url).read() 126 | except urllib2.HTTPError: 127 | # Probably the analysis_url link has expired. Refresh it. 128 | param_dict = dict(id = self.id) 129 | new_track = _profile(param_dict, DEFAULT_ASYNC_TIMEOUT) 130 | if new_track and new_track.analysis_url: 131 | self.analysis_url = new_track.analysis_url 132 | json_string = urllib2.urlopen(self.analysis_url).read() 133 | else: 134 | raise Exception("Failed to create track analysis.") 135 | 136 | analysis = json.loads(json_string) 137 | analysis_track = analysis.pop('track', {}) 138 | self.__dict__.update(analysis) 139 | self.__dict__.update(analysis_track) 140 | except Exception: #pylint: disable=W0702 141 | # No detailed analysis found. 142 | raise Exception("Failed to create track analysis.") 143 | else: 144 | raise Exception("Failed to create track analysis.") 145 | 146 | 147 | def _wait_for_pending_track(trid, timeout): 148 | status = 'pending' 149 | param_dict = {'id': trid} 150 | param_dict['format'] = 'json' 151 | param_dict['bucket'] = 'audio_summary' 152 | start_time = time.time() 153 | end_time = start_time + timeout 154 | # counter for seconds to wait before checking track profile again. 155 | timeout_counter = 3 156 | while status == 'pending' and time.time() < end_time: 157 | time.sleep(timeout_counter) 158 | result = util.callm('track/profile', param_dict) 159 | status = result['response']['track']['status'].lower() 160 | # Slowly increment to wait longer each time. 161 | timeout_counter += timeout_counter / 2 162 | return result 163 | 164 | def _track_from_response(result, timeout): 165 | """ 166 | This is the function that actually creates the track object 167 | """ 168 | response = result['response'] 169 | status = response['track']['status'].lower() 170 | 171 | if status == 'pending': 172 | # Need to wait for async upload or analyze call to finish. 173 | result = _wait_for_pending_track(response['track']['id'], timeout) 174 | response = result['response'] 175 | status = response['track']['status'].lower() 176 | 177 | if not status == 'complete': 178 | track_id = response['track']['id'] 179 | if status == 'pending': 180 | raise Exception('%s: the operation didn\'t complete before the timeout (%d secs)' % 181 | (track_id, timeout)) 182 | else: 183 | raise Exception('%s: there was an error analyzing the track, status: %s' % (track_id, status)) 184 | else: 185 | # track_properties starts as the response dictionary. 186 | track_properties = response['track'] 187 | # 'id' and 'md5' are separated to construct the Track object. 188 | identifier = track_properties.pop('id') 189 | md5 = track_properties.pop('md5', None) # tracks from song api calls will not have an md5 190 | # Pop off the audio_summary dict and make those keys attributes 191 | # of the Track. This includes things like tempo, energy, and loudness. 192 | track_properties.update(track_properties.pop('audio_summary')) 193 | return Track(identifier, md5, track_properties) 194 | 195 | def _upload(param_dict, timeout, data): 196 | """ 197 | Calls upload either with a local audio file, 198 | or a url. Returns a track object. 199 | """ 200 | param_dict['format'] = 'json' 201 | param_dict['wait'] = 'true' 202 | param_dict['bucket'] = 'audio_summary' 203 | result = util.callm('track/upload', param_dict, POST = True, socket_timeout = 300, data = data) 204 | return _track_from_response(result, timeout) 205 | 206 | def _profile(param_dict, timeout): 207 | param_dict['format'] = 'json' 208 | param_dict['bucket'] = 'audio_summary' 209 | result = util.callm('track/profile', param_dict) 210 | return _track_from_response(result, timeout) 211 | 212 | 213 | """ Below are convenience functions for creating Track objects, you should use them """ 214 | 215 | def _track_from_data(audio_data, filetype, timeout): 216 | param_dict = {} 217 | param_dict['filetype'] = filetype 218 | return _upload(param_dict, timeout, audio_data) 219 | 220 | def track_from_file(file_object, filetype, timeout=DEFAULT_ASYNC_TIMEOUT, force_upload=False): 221 | """ 222 | Create a track object from a file-like object. 223 | 224 | NOTE: Does not create the detailed analysis for the Track. Call 225 | Track.get_analysis() for that. 226 | 227 | Args: 228 | file_object: a file-like Python object 229 | filetype: the file type. Supported types include mp3, ogg, wav, m4a, mp4, au 230 | force_upload: skip the MD5 shortcut path, force an upload+analysis 231 | Example: 232 | >>> f = open("Miaow-01-Tempered-song.mp3") 233 | >>> t = track.track_from_file(f, 'mp3') 234 | >>> t 235 | < Track > 236 | >>> 237 | """ 238 | if not force_upload: 239 | try: 240 | # Check if this file has already been uploaded. 241 | # This is much faster than uploading. 242 | md5 = hashlib.md5(file_object.read()).hexdigest() 243 | return track_from_md5(md5) 244 | except util.EchoNestAPIError: 245 | # Fall through to do a fresh upload. 246 | pass 247 | 248 | file_object.seek(0) 249 | return _track_from_data(file_object.read(), filetype, timeout) 250 | 251 | def track_from_filename(filename, filetype = None, timeout=DEFAULT_ASYNC_TIMEOUT, force_upload=False): 252 | """ 253 | Create a track object from a filename. 254 | 255 | NOTE: Does not create the detailed analysis for the Track. Call 256 | Track.get_analysis() for that. 257 | 258 | Args: 259 | filename: A string containing the path to the input file. 260 | filetype: A string indicating the filetype; Defaults to None (type determined by file extension). 261 | force_upload: skip the MD5 shortcut path, force an upload+analysis 262 | 263 | Example: 264 | >>> t = track.track_from_filename("Miaow-01-Tempered-song.mp3") 265 | >>> t 266 | < Track > 267 | >>> 268 | """ 269 | filetype = filetype or filename.split('.')[-1] 270 | file_object = open(filename, 'rb') 271 | result = track_from_file(file_object, filetype, timeout, force_upload) 272 | file_object.close() 273 | return result 274 | 275 | def track_from_url(url, timeout=DEFAULT_ASYNC_TIMEOUT): 276 | """ 277 | Create a track object from a public http URL. 278 | 279 | NOTE: Does not create the detailed analysis for the Track. Call 280 | Track.get_analysis() for that. 281 | 282 | Args: 283 | url: A string giving the URL to read from. This must be on a public machine accessible by HTTP. 284 | 285 | Example: 286 | >>> t = track.track_from_url("http://www.miaowmusic.com/mp3/Miaow-01-Tempered-song.mp3") 287 | >>> t 288 | < Track > 289 | >>> 290 | 291 | """ 292 | param_dict = dict(url = url) 293 | return _upload(param_dict, timeout, data=None) 294 | 295 | def track_from_id(identifier, timeout=DEFAULT_ASYNC_TIMEOUT): 296 | """ 297 | Create a track object from an Echo Nest track ID. 298 | 299 | NOTE: Does not create the detailed analysis for the Track. Call 300 | Track.get_analysis() for that. 301 | 302 | Args: 303 | identifier: A string containing the ID of a previously analyzed track. 304 | 305 | Example: 306 | >>> t = track.track_from_id("TRWFIDS128F92CC4CA") 307 | >>> t 308 | 309 | >>> 310 | """ 311 | param_dict = dict(id = identifier) 312 | return _profile(param_dict, timeout) 313 | 314 | def track_from_md5(md5, timeout=DEFAULT_ASYNC_TIMEOUT): 315 | """ 316 | Create a track object from an md5 hash. 317 | 318 | NOTE: Does not create the detailed analysis for the Track. Call 319 | Track.get_analysis() for that. 320 | 321 | Args: 322 | md5: A string 32 characters long giving the md5 checksum of a track already analyzed. 323 | 324 | Example: 325 | >>> t = track.track_from_md5('b8abf85746ab3416adabca63141d8c2d') 326 | >>> t 327 | 328 | >>> 329 | """ 330 | param_dict = dict(md5 = md5) 331 | return _profile(param_dict, timeout) 332 | -------------------------------------------------------------------------------- /pyechonest/playlist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-04-25. 7 | 8 | The Playlist module loosely covers http://developer.echonest.com/docs/v4/playlist.html 9 | Refer to the official api documentation if you are unsure about something. 10 | """ 11 | 12 | import util 13 | from proxies import PlaylistProxy 14 | from song import Song 15 | import catalog 16 | import logging 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | def basic(type='artist-radio', artist_id=None, artist=None, song_id=None, song=None, track_id=None, dmca=False, 21 | results=15, buckets=None, limit=False,genres=None,): 22 | """Get a basic playlist 23 | 24 | Args: 25 | 26 | Kwargs: 27 | type (str): a string representing the playlist type ('artist-radio' or 'song-radio') 28 | 29 | artist_id (str): the artist_id to seed the playlist 30 | 31 | artist (str): the name of an artist to seed the playlist 32 | 33 | song_id (str): a song_id to seed the playlist 34 | 35 | song (str): the name of a song to seed the playlist 36 | 37 | track_id (str): the name of a track to seed the playlist 38 | 39 | dmca (bool): make the playlist dmca-compliant 40 | 41 | results (int): desired length of the playlist 42 | 43 | buckets (list): A list of strings specifying which buckets to retrieve 44 | 45 | limit (bool): Whether results should be restricted to any idspaces given in the buckets parameter 46 | """ 47 | 48 | limit = str(limit).lower() 49 | dmca = str(dmca).lower() 50 | 51 | kwargs = locals() 52 | kwargs['bucket'] = kwargs['buckets'] 53 | del kwargs['buckets'] 54 | kwargs['genre'] = kwargs['genres'] 55 | del kwargs['genres'] 56 | 57 | result = util.callm("%s/%s" % ('playlist', 'basic'), kwargs) 58 | return [Song(**util.fix(s_dict)) for s_dict in result['response']['songs']] 59 | 60 | 61 | def static(type='artist', artist_pick='song_hotttnesss-desc', variety=.5, artist_id=None, artist=None, song_id=None, 62 | track_id=None, description=None, style=None, mood=None, results=15, max_tempo=None, min_tempo=None, 63 | max_duration=None, min_duration=None, max_loudness=None, min_loudness=None, max_danceability=None, 64 | min_danceability=None, max_energy=None, min_energy=None, artist_max_familiarity=None, 65 | artist_min_familiarity=None, artist_max_hotttnesss=None, artist_min_hotttnesss=None, 66 | song_max_hotttnesss=None, song_min_hotttnesss=None, min_longitude=None, max_longitude=None, 67 | min_latitude=None, max_latitude=None, adventurousness=0.2, mode=None, key=None, buckets=None, sort=None, 68 | limit=False, seed_catalog=None, source_catalog=None, rank_type=None, test_new_things=None, 69 | artist_start_year_after=None, artist_start_year_before=None, artist_end_year_after=None, 70 | artist_end_year_before=None, dmca=False, distribution=None, song_type=None, genres=None): 71 | """Get a static playlist 72 | 73 | Args: 74 | 75 | Kwargs: 76 | type (str): a string representing the playlist type ('artist', 'artist-radio', ...) 77 | 78 | artist_pick (str): How songs should be chosen for each artist 79 | 80 | variety (float): A number between 0 and 1 specifying the variety of the playlist 81 | 82 | artist_id (str): the artist_id 83 | 84 | artist (str): the name of an artist 85 | 86 | song_id (str): the song_id 87 | 88 | track_id (str): the track id 89 | 90 | description (str): A string describing the artist and song 91 | 92 | style (str): A string describing the style/genre of the artist and song 93 | 94 | mood (str): A string describing the mood of the artist and song 95 | 96 | results (int): An integer number of results to return 97 | 98 | max_tempo (float): The max tempo of song results 99 | 100 | min_tempo (float): The min tempo of song results 101 | 102 | max_duration (float): The max duration of song results 103 | 104 | min_duration (float): The min duration of song results 105 | 106 | max_loudness (float): The max loudness of song results 107 | 108 | min_loudness (float): The min loudness of song results 109 | 110 | artist_max_familiarity (float): A float specifying the max familiarity of artists to search for 111 | 112 | artist_min_familiarity (float): A float specifying the min familiarity of artists to search for 113 | 114 | artist_max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 115 | 116 | artist_min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 117 | 118 | song_max_hotttnesss (float): A float specifying the max hotttnesss of songs to search for 119 | 120 | song_min_hotttnesss (float): A float specifying the max hotttnesss of songs to search for 121 | 122 | max_energy (float): The max energy of song results 123 | 124 | min_energy (float): The min energy of song results 125 | 126 | max_danceability (float): The max danceability of song results 127 | 128 | min_danceability (float): The min danceability of song results 129 | 130 | mode (int): 0 or 1 (minor or major) 131 | 132 | key (int): 0-11 (c, c-sharp, d, e-flat, e, f, f-sharp, g, a-flat, a, b-flat, b) 133 | 134 | max_latitude (float): A float specifying the max latitude of artists to search for 135 | 136 | min_latitude (float): A float specifying the min latitude of artists to search for 137 | 138 | max_longitude (float): A float specifying the max longitude of artists to search for 139 | 140 | min_longitude (float): A float specifying the min longitude of artists to search for 141 | 142 | adventurousness (float): A float ranging from 0 for old favorites to 1.0 for unheard music according to a seed_catalog 143 | 144 | sort (str): A string indicating an attribute and order for sorting the results 145 | 146 | buckets (list): A list of strings specifying which buckets to retrieve 147 | 148 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 149 | 150 | seed_catalog (str or Catalog): An Artist Catalog object or Artist Catalog id to use as a seed 151 | 152 | source_catalog (str or Catalog): A Catalog object or catalog id 153 | 154 | rank_type (str): A string denoting the desired ranking for description searches, either 'relevance' or 'familiarity' 155 | 156 | artist_start_year_before (int): Returned song's artists will have started recording music before this year. 157 | 158 | artist_start_year_after (int): Returned song's artists will have started recording music after this year. 159 | 160 | artist_end_year_before (int): Returned song's artists will have stopped recording music before this year. 161 | 162 | artist_end_year_after (int): Returned song's artists will have stopped recording music after this year. 163 | 164 | distribution (str): Affects the range of artists returned and how many songs each artist will have in the playlist relative to how similar they are to the seed. (wandering, focused) 165 | 166 | song_type (str): A string or list of strings of the type of songs allowed. The only valid song type at the moment is 'christmas'. 167 | Valid formats are 'song_type', 'song_type:true', 'song_type:false', or 'song_type:any'. 168 | 169 | Returns: 170 | A list of Song objects 171 | 172 | Example: 173 | 174 | >>> p = playlist.static(type='artist-radio', artist=['ida maria', 'florence + the machine']) 175 | >>> p 176 | [, 177 | , 178 | , 179 | , 180 | , 181 | , 182 | , 183 | , 184 | , 185 | , 186 | , 187 | , 188 | , 189 | , 190 | ] 191 | >>> 192 | 193 | """ 194 | limit = str(limit).lower() 195 | 196 | if seed_catalog and isinstance(seed_catalog, catalog.Catalog): 197 | seed_catalog = seed_catalog.id 198 | 199 | if source_catalog and isinstance(source_catalog, catalog.Catalog): 200 | source_catalog = source_catalog.id 201 | dmca = str(dmca).lower() 202 | kwargs = locals() 203 | kwargs['bucket'] = kwargs['buckets'] or [] 204 | del kwargs['buckets'] 205 | kwargs['genre'] = kwargs['genres'] 206 | del kwargs['genres'] 207 | 208 | result = util.callm("%s/%s" % ('playlist', 'static'), kwargs) 209 | return [Song(**util.fix(s_dict)) for s_dict in result['response']['songs']] 210 | 211 | class Playlist(PlaylistProxy): 212 | """ 213 | A Dynamic Playlist object. 214 | http://developer.echonest.com/docs/v4/playlist.html#dynamic-create 215 | 216 | Attributes: 217 | 218 | Example: 219 | """ 220 | 221 | def __init__( 222 | self, session_id=None, type=None, artist_pick=None, variety=None, artist_id=None, artist=None, song_id=None, 223 | track_id=None, description=None, style=None, mood=None, max_tempo=None, min_tempo=None, max_duration=None, 224 | min_duration=None, max_loudness=None, min_loudness=None, max_danceability=None, min_danceability=None, 225 | max_energy=None, min_energy=None, artist_max_familiarity=None, artist_min_familiarity=None, 226 | artist_max_hotttnesss=None, artist_min_hotttnesss=None, song_max_hotttnesss=None, song_min_hotttnesss=None, 227 | min_longitude=None, max_longitude=None, min_latitude=None, max_latitude=None, adventurousness=None, 228 | mode=None, key=None, buckets=None, sort=None, limit=False, seed_catalog=None, source_catalog=None, 229 | rank_type=None, test_new_things=None, artist_start_year_after=None, artist_start_year_before=None, 230 | artist_end_year_after=None, artist_end_year_before=None, dmca=False, distribution=None, song_type=None, 231 | session_catalog=None,genres=None,): 232 | 233 | limit = str(limit).lower() 234 | dmca = str(dmca).lower() 235 | 236 | if isinstance(seed_catalog, catalog.Catalog): 237 | seed_catalog = seed_catalog.id 238 | 239 | super(Playlist, self).__init__( 240 | session_id=session_id, 241 | type=type, 242 | artist_pick=artist_pick, 243 | variety=variety, 244 | artist_id=artist_id, 245 | artist=artist, 246 | song_id=song_id, 247 | track_id=track_id, 248 | description=description, 249 | style=style, 250 | mood=mood, 251 | max_tempo=max_tempo, 252 | min_tempo=min_tempo, 253 | max_duration=max_duration, 254 | min_duration=min_duration, 255 | max_loudness=max_loudness, 256 | min_loudness=min_loudness, 257 | max_danceability=max_danceability, 258 | min_danceability=min_danceability, 259 | max_energy=max_energy, 260 | min_energy=min_energy, 261 | artist_max_familiarity=artist_max_familiarity, 262 | artist_min_familiarity=artist_min_familiarity, 263 | artist_max_hotttnesss=artist_max_hotttnesss, 264 | artist_min_hotttnesss=artist_min_hotttnesss, 265 | song_max_hotttnesss=song_max_hotttnesss, 266 | song_min_hotttnesss=song_min_hotttnesss, 267 | min_longitude=min_longitude, 268 | max_longitude=max_longitude, 269 | min_latitude=min_latitude, 270 | max_latitude=max_latitude, 271 | adventurousness=adventurousness, 272 | mode=mode, 273 | key=key, 274 | buckets=buckets, 275 | sort=sort, 276 | limit=limit, 277 | seed_catalog=seed_catalog, 278 | source_catalog=source_catalog, 279 | rank_type=rank_type, 280 | test_new_things=test_new_things, 281 | artist_start_year_after=artist_start_year_after, 282 | artist_start_year_before=artist_start_year_before, 283 | artist_end_year_after=artist_end_year_after, 284 | artist_end_year_before=artist_end_year_before, 285 | dmca=dmca, 286 | distribution=distribution, 287 | song_type=song_type, 288 | session_catalog=session_catalog, 289 | genres=genres 290 | ) 291 | 292 | 293 | def __repr__(self): 294 | return "" % self.session_id.encode('utf-8') 295 | 296 | def get_next_songs(self, results=None, lookahead=None): 297 | response = self.get_attribute( 298 | method='next', 299 | session_id=self.session_id, 300 | results=results, 301 | lookahead=lookahead 302 | ) 303 | self.cache['songs'] = response['songs'] 304 | self.cache['lookahead'] = response['lookahead'] 305 | if len(self.cache['songs']): 306 | songs = self.cache['songs'][:] 307 | songs = [Song(**util.fix(song)) for song in songs] 308 | return songs 309 | else: 310 | return None 311 | 312 | def get_current_songs(self): 313 | if not 'songs' in self.cache: 314 | self.get_next_songs(results=1) 315 | if len(self.cache['songs']): 316 | songs = self.cache['songs'][:] 317 | songs = [Song(**util.fix(song)) for song in songs] 318 | 319 | return songs 320 | else: 321 | return None 322 | 323 | def get_lookahead_songs(self): 324 | if not 'lookahead' in self.cache: 325 | return None 326 | if len(self.cache['lookahead']): 327 | lookahead = self.cache['lookahead'][:] 328 | lookahead = [Song(**util.fix(song)) for song in lookahead] 329 | 330 | return lookahead 331 | else: 332 | return None 333 | 334 | songs = property(get_current_songs) 335 | 336 | def info(self): 337 | return self.get_attribute("info", session_id=self.session_id) 338 | 339 | def delete(self): 340 | self.get_attribute("delete", session_id=self.session_id) 341 | return True 342 | 343 | def restart( 344 | self, 345 | type=None, 346 | artist_pick=None, 347 | variety=None, 348 | artist_id=None, 349 | artist=None, 350 | song_id=None, 351 | track_id=None, 352 | description=None, 353 | style=None, 354 | mood=None, 355 | max_tempo=None, 356 | min_tempo=None, 357 | max_duration=None, 358 | min_duration=None, 359 | max_loudness=None, 360 | min_loudness=None, 361 | max_danceability=None, 362 | min_danceability=None, 363 | max_energy=None, 364 | min_energy=None, 365 | artist_max_familiarity=None, 366 | artist_min_familiarity=None, 367 | artist_max_hotttnesss=None, 368 | artist_min_hotttnesss=None, 369 | song_max_hotttnesss=None, 370 | song_min_hotttnesss=None, 371 | min_longitude=None, 372 | max_longitude=None, 373 | min_latitude=None, 374 | max_latitude=None, 375 | adventurousness=None, 376 | mode=None, 377 | key=None, 378 | buckets=None, 379 | sort=None, 380 | limit=False, 381 | seed_catalog=None, 382 | source_catalog=None, 383 | rank_type=None, 384 | test_new_things=None, 385 | artist_start_year_after=None, 386 | artist_start_year_before=None, 387 | artist_end_year_after=None, 388 | artist_end_year_before=None, 389 | dmca=False, 390 | distribution=None, 391 | song_type=None, 392 | genres=None, 393 | ): 394 | limit = str(limit).lower() 395 | dmca = str(dmca).lower() 396 | 397 | if isinstance(seed_catalog, catalog.Catalog): 398 | seed_catalog = seed_catalog.id 399 | 400 | 401 | return self.get_attribute( 402 | method='restart', 403 | session_id=self.session_id, 404 | type=type, 405 | artist_pick=artist_pick, 406 | variety=variety, 407 | artist_id=artist_id, 408 | artist=artist, 409 | song_id=song_id, 410 | track_id=track_id, 411 | description=description, 412 | style=style, 413 | mood=mood, 414 | max_tempo=max_tempo, 415 | min_tempo=min_tempo, 416 | max_duration=max_duration, 417 | min_duration=min_duration, 418 | max_loudness=max_loudness, 419 | min_loudness=min_loudness, 420 | max_danceability=max_danceability, 421 | min_danceability=min_danceability, 422 | max_energy=max_energy, 423 | min_energy=min_energy, 424 | artist_max_familiarity=artist_max_familiarity, 425 | artist_min_familiarity=artist_min_familiarity, 426 | artist_max_hotttnesss=artist_max_hotttnesss, 427 | artist_min_hotttnesss=artist_min_hotttnesss, 428 | song_max_hotttnesss=song_max_hotttnesss, 429 | song_min_hotttnesss=song_min_hotttnesss, 430 | min_longitude=min_longitude, 431 | max_longitude=max_longitude, 432 | min_latitude=min_latitude, 433 | max_latitude=max_latitude, 434 | adventurousness=adventurousness, 435 | mode=mode, 436 | key=key, 437 | bucket=buckets, 438 | sort=sort, 439 | limit=limit, 440 | seed_catalog=seed_catalog, 441 | source_catalog=source_catalog, 442 | rank_type=rank_type, 443 | test_new_things=test_new_things, 444 | artist_start_year_after=artist_start_year_after, 445 | artist_start_year_before=artist_start_year_before, 446 | artist_end_year_after=artist_end_year_after, 447 | artist_end_year_before=artist_end_year_before, 448 | dmca=dmca, 449 | distribution=distribution, 450 | song_type=song_type, 451 | genres=genres, 452 | ) 453 | 454 | def steer( 455 | self, 456 | max_tempo=None, 457 | min_tempo=None, 458 | target_tempo=None, 459 | max_duration=None, 460 | min_duration=None, 461 | target_duration=None, 462 | max_loudness=None, 463 | min_loudness=None, 464 | target_loudness=None, 465 | max_danceability=None, 466 | min_danceability=None, 467 | target_danceability=None, 468 | max_energy=None, 469 | min_energy=None, 470 | target_energy=None, 471 | max_artist_familiarity=None, 472 | min_artist_familiarity=None, 473 | target_artist_familiarity=None, 474 | max_artist_hotttnesss=None, 475 | min_artist_hotttnesss=None, 476 | target_artist_hotttnesss=None, 477 | max_song_hotttnesss=None, 478 | min_song_hotttnesss=None, 479 | target_song_hotttnesss=None, 480 | more_like_this=None, 481 | less_like_this=None, 482 | adventurousness=None, 483 | variety=None, 484 | description=None, 485 | style=None, 486 | mood=None, 487 | song_type=None, 488 | genres=None 489 | ): 490 | 491 | response = self.get_attribute( 492 | method='steer', 493 | session_id=self.session_id, 494 | max_tempo=max_tempo, 495 | min_tempo=min_tempo, 496 | target_tempo=target_tempo, 497 | max_duration=max_duration, 498 | min_duration=min_duration, 499 | target_duration=target_duration, 500 | max_loudness=max_loudness, 501 | min_loudness=min_loudness, 502 | target_loudness=target_loudness, 503 | max_danceability=max_danceability, 504 | min_danceability=min_danceability, 505 | target_danceability=target_danceability, 506 | max_energy=max_energy, 507 | min_energy=min_energy, 508 | target_energy=target_energy, 509 | max_artist_familiarity=max_artist_familiarity, 510 | min_artist_familiarity=min_artist_familiarity, 511 | target_artist_familiarity=target_artist_familiarity, 512 | max_artist_hotttnesss=max_artist_hotttnesss, 513 | min_artist_hotttnesss=min_artist_hotttnesss, 514 | target_artist_hotttnesss=target_artist_hotttnesss, 515 | max_song_hotttnesss=max_song_hotttnesss, 516 | min_song_hotttnesss=min_song_hotttnesss, 517 | target_song_hotttnesss=target_song_hotttnesss, 518 | more_like_this=more_like_this, 519 | less_like_this=less_like_this, 520 | adventurousness=adventurousness, 521 | variety=variety, 522 | description=description, 523 | style=style, 524 | mood=mood, 525 | song_type=song_type, 526 | genres=genres, 527 | ) 528 | 529 | self.cache['lookahead'] = [] 530 | return True 531 | 532 | def feedback( 533 | self, 534 | ban_artist=None, 535 | ban_song=None, 536 | skip_song=None, 537 | favorite_artist=None, 538 | favorite_song=None, 539 | play_song=None, 540 | unplay_song=None, 541 | rate_song=None, 542 | invalidate_song=None, 543 | invalidate_artist=None, 544 | ): 545 | 546 | response = self.get_attribute( 547 | session_id=self.session_id, 548 | method='feedback', 549 | ban_artist=ban_artist, 550 | ban_song=ban_song, 551 | skip_song=skip_song, 552 | favorite_artist=favorite_artist, 553 | favorite_song=favorite_song, 554 | play_song=play_song, 555 | unplay_song=unplay_song, 556 | rate_song=rate_song, 557 | invalidate_song=invalidate_song, 558 | invalidate_artist=invalidate_artist, 559 | ) 560 | 561 | self.cache['lookahead'] = [] 562 | return True 563 | 564 | class DeprecationHelper(object): 565 | 566 | def __init__(self, new_target): 567 | self.new_target = new_target 568 | 569 | def _warn(self): 570 | from warnings import warn 571 | warn("BetaPlaylist is no longer in Beta and has been moved to Playlist", DeprecationWarning, stacklevel=2) 572 | logger.warn("BetaPlaylist is no longer in Beta and has been moved to Playlist") 573 | 574 | def __call__(self, *args, **kwargs): 575 | self._warn() 576 | return self.new_target(*args, **kwargs) 577 | 578 | def __getattr__(self, attr): 579 | self._warn() 580 | return getattr(self.new_target, attr) 581 | 582 | BetaPlaylist = DeprecationHelper(Playlist) 583 | -------------------------------------------------------------------------------- /pyechonest/song.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-04-25. 7 | 8 | The Song module loosely covers http://developer.echonest.com/docs/v4/song.html 9 | Refer to the official api documentation if you are unsure about something. 10 | """ 11 | import os 12 | import util 13 | from proxies import SongProxy 14 | 15 | try: 16 | import json 17 | except ImportError: 18 | import simplejson as json 19 | 20 | class 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/TR/RnMKCg47J5LgQZr0SISyoPuRxKVQx3Z_YSuhVa/3/full.json?Signature=KBUbewLiP3sZ2X6rRZzXhrgh8fw%3D&Expires=1349809604&AWSAccessKeyId=AKIAJRDFEY23UEVW42BQ', 96 | u'audio_md5': u'ca3fdfa72eed23d5ad89872c38cecc0e', 97 | u'danceability': 0.33712086491871546, 98 | u'duration': 470.70666999999997, 99 | u'energy': 0.58186979146361684, 100 | u'key': 0, 101 | u'liveness': 0.08676759933615498, 102 | u'loudness': -9.5960000000000001, 103 | u'mode': 1, 104 | u'speechiness': 0.036938896635994867, 105 | u'tempo': 126.949, 106 | u'time_signature': 4} 107 | >>> 108 | 109 | """ 110 | if not (cache and ('audio_summary' in self.cache)): 111 | response = self.get_attribute('profile', bucket='audio_summary') 112 | if response['songs'] and 'audio_summary' in response['songs'][0]: 113 | self.cache['audio_summary'] = response['songs'][0]['audio_summary'] 114 | else: 115 | self.cache['audio_summary'] = {} 116 | return self.cache['audio_summary'] 117 | 118 | audio_summary = property(get_audio_summary) 119 | 120 | def get_song_hotttnesss(self, cache=True): 121 | """Get our numerical description of how hottt a song currently is 122 | 123 | Args: 124 | 125 | Kwargs: 126 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 127 | 128 | Returns: 129 | A float representing hotttnesss. 130 | 131 | Example: 132 | >>> s = song.Song('SOLUHKP129F0698D49') 133 | >>> s.get_song_hotttnesss() 134 | 0.57344379999999995 135 | >>> s.song_hotttnesss 136 | 0.57344379999999995 137 | >>> 138 | 139 | """ 140 | if not (cache and ('song_hotttnesss' in self.cache)): 141 | response = self.get_attribute('profile', bucket='song_hotttnesss') 142 | self.cache['song_hotttnesss'] = response['songs'][0]['song_hotttnesss'] 143 | return self.cache['song_hotttnesss'] 144 | 145 | song_hotttnesss = property(get_song_hotttnesss) 146 | 147 | def get_song_type(self, cache=True): 148 | """Get the types of a song. 149 | 150 | Args: 151 | cache (boolean): A boolean indicating whether or not the cached value should be used 152 | (if available). Defaults to True. 153 | 154 | Returns: 155 | A list of strings, each representing a song type: 'christmas', for example. 156 | 157 | Example: 158 | >>> s = song.Song('SOQKVPH12A58A7AF4D') 159 | >>> s.song_type 160 | [u'christmas'] 161 | >>> 162 | 163 | """ 164 | if not (cache and ('song_type' in self.cache)): 165 | response = self.get_attribute('profile', bucket='song_type') 166 | if response['songs'][0].has_key('song_type'): 167 | self.cache['song_type'] = response['songs'][0]['song_type'] 168 | else: 169 | self.cache['song_type'] = [] 170 | return self.cache['song_type'] 171 | 172 | song_type = property(get_song_type) 173 | 174 | def get_artist_hotttnesss(self, cache=True): 175 | """Get our numerical description of how hottt a song's artist currently is 176 | 177 | Args: 178 | 179 | Kwargs: 180 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 181 | 182 | Returns: 183 | A float representing hotttnesss. 184 | 185 | Example: 186 | >>> s = song.Song('SOOLGAZ127F3E1B87C') 187 | >>> s.artist_hotttnesss 188 | 0.45645633000000002 189 | >>> s.get_artist_hotttnesss() 190 | 0.45645633000000002 191 | >>> 192 | 193 | """ 194 | if not (cache and ('artist_hotttnesss' in self.cache)): 195 | response = self.get_attribute('profile', bucket='artist_hotttnesss') 196 | self.cache['artist_hotttnesss'] = response['songs'][0]['artist_hotttnesss'] 197 | return self.cache['artist_hotttnesss'] 198 | 199 | artist_hotttnesss = property(get_artist_hotttnesss) 200 | 201 | def get_artist_familiarity(self, cache=True): 202 | """Get our numerical estimation of how familiar a song's artist currently is to the world 203 | 204 | Args: 205 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 206 | 207 | Returns: 208 | A float representing familiarity. 209 | 210 | Example: 211 | >>> s = song.Song('SOQKVPH12A58A7AF4D') 212 | >>> s.get_artist_familiarity() 213 | 0.639626025843539 214 | >>> s.artist_familiarity 215 | 0.639626025843539 216 | >>> 217 | """ 218 | if not (cache and ('artist_familiarity' in self.cache)): 219 | response = self.get_attribute('profile', bucket='artist_familiarity') 220 | self.cache['artist_familiarity'] = response['songs'][0]['artist_familiarity'] 221 | return self.cache['artist_familiarity'] 222 | 223 | artist_familiarity = property(get_artist_familiarity) 224 | 225 | def get_artist_location(self, cache=True): 226 | """Get the location of a song's artist. 227 | 228 | Args: 229 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 230 | 231 | Returns: 232 | An artist location object. 233 | 234 | Example: 235 | >>> s = song.Song('SOQKVPH12A58A7AF4D') 236 | >>> s.artist_location 237 | {u'latitude': 34.053489999999996, u'location': u'Los Angeles, CA', u'longitude': -118.24532000000001} 238 | >>> 239 | 240 | """ 241 | if not (cache and ('artist_location' in self.cache)): 242 | response = self.get_attribute('profile', bucket='artist_location') 243 | self.cache['artist_location'] = response['songs'][0]['artist_location'] 244 | return self.cache['artist_location'] 245 | 246 | artist_location = property(get_artist_location) 247 | 248 | def get_foreign_id(self, idspace='', cache=True): 249 | """Get the foreign id for this song for a specific id space 250 | 251 | Args: 252 | 253 | Kwargs: 254 | idspace (str): A string indicating the idspace to fetch a foreign id for. 255 | 256 | Returns: 257 | A foreign ID string 258 | 259 | Example: 260 | 261 | >>> s = song.Song('SOYRVMR12AF729F8DC') 262 | >>> s.get_foreign_id('CAGPXKK12BB06F9DE9') 263 | 264 | >>> 265 | """ 266 | if not (cache and ('foreign_ids' in self.cache) and filter(lambda d: d.get('catalog') == idspace, self.cache['foreign_ids'])): 267 | response = self.get_attribute('profile', bucket=['id:'+idspace]) 268 | rsongs = response['songs'] 269 | if len(rsongs) == 0: 270 | return None 271 | foreign_ids = rsongs[0].get("foreign_ids", []) 272 | self.cache['foreign_ids'] = self.cache.get('foreign_ids', []) + foreign_ids 273 | cval = filter(lambda d: d.get('catalog') == idspace, self.cache.get('foreign_ids')) 274 | return cval[0].get('foreign_id') if cval else None 275 | 276 | def get_song_discovery(self, cache=True): 277 | """ 278 | Args: 279 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 280 | 281 | Returns: 282 | A float representing a song's discovery rank. 283 | 284 | Example: 285 | >>> s = song.Song('SOQKVPH12A58A7AF4D') 286 | >>> s.get_song_discovery() 287 | 0.639626025843539 288 | >>> s.song_discovery 289 | 0.639626025843539 290 | >>> 291 | """ 292 | if not (cache and ('song_discovery' in self.cache)): 293 | response = self.get_attribute('profile', bucket='song_discovery') 294 | self.cache['song_discovery'] = response['songs'][0]['song_discovery'] 295 | return self.cache['song_discovery'] 296 | 297 | song_discovery = property(get_song_discovery) 298 | 299 | def get_song_currency(self, cache=True): 300 | """ 301 | Args: 302 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 303 | 304 | Returns: 305 | A float representing a song's currency rank. 306 | 307 | Example: 308 | >>> s = song.Song('SOQKVPH12A58A7AF4D') 309 | >>> s.get_song_currency() 310 | 0.639626025843539 311 | >>> s.song_currency 312 | 0.639626025843539 313 | >>> 314 | """ 315 | if not (cache and ('song_currency' in self.cache)): 316 | response = self.get_attribute('profile', bucket='song_currency') 317 | self.cache['song_currency'] = response['songs'][0]['song_currency'] 318 | return self.cache['song_currency'] 319 | 320 | song_currency = property(get_song_currency) 321 | 322 | def get_tracks(self, catalog, cache=True): 323 | """Get the tracks for a song given a catalog. 324 | 325 | Args: 326 | catalog (str): a string representing the catalog whose track you want to retrieve. 327 | 328 | Returns: 329 | A list of Track dicts. 330 | 331 | Example: 332 | >>> s = song.Song('SOWDASQ12A6310F24F') 333 | >>> s.get_tracks('7digital')[0] 334 | {u'catalog': u'7digital', 335 | u'foreign_id': u'7digital:track:8445818', 336 | u'id': u'TRJGNNY12903CC625C', 337 | u'preview_url': u'http://previews.7digital.com/clips/34/8445818.clip.mp3', 338 | u'release_image': u'http://cdn.7static.com/static/img/sleeveart/00/007/628/0000762838_200.jpg'} 339 | >>> 340 | 341 | """ 342 | if not (cache and ('tracks' in self.cache) and (catalog in [td['catalog'] for td in self.cache['tracks']])): 343 | kwargs = { 344 | 'bucket':['tracks', 'id:%s' % catalog], 345 | } 346 | 347 | response = self.get_attribute('profile', **kwargs) 348 | if not 'tracks' in self.cache: 349 | self.cache['tracks'] = [] 350 | # don't blow away the cache for other catalogs 351 | potential_tracks = response['songs'][0].get('tracks', []) 352 | existing_track_ids = [tr['foreign_id'] for tr in self.cache['tracks']] 353 | new_tds = filter(lambda tr: tr['foreign_id'] not in existing_track_ids, potential_tracks) 354 | self.cache['tracks'].extend(new_tds) 355 | return filter(lambda tr: tr['catalog']==catalog, self.cache['tracks']) 356 | 357 | 358 | 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): 359 | """Identify a song. 360 | 361 | Args: 362 | 363 | Kwargs: 364 | filename (str): The path of the file you want to analyze (requires codegen binary!) 365 | 366 | query_obj (dict or list): A dict or list of dicts containing a 'code' element with an fp code 367 | 368 | code (str): A fingerprinter code 369 | 370 | artist (str): An artist name 371 | 372 | title (str): A song title 373 | 374 | release (str): A release name 375 | 376 | duration (int): A song duration 377 | 378 | genre (str): A string representing the genre 379 | 380 | buckets (list): A list of strings specifying which buckets to retrieve 381 | 382 | version (str): The version of the code generator used to generate the code 383 | 384 | codegen_start (int): The point (in seconds) where the codegen should start 385 | 386 | codegen_duration (int): The duration (in seconds) the codegen should analyze 387 | 388 | Example: 389 | >>> qo 390 | {'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', 391 | 'code_count': 151, 392 | 'low_rank': 0, 393 | 'metadata': {'artist': 'Harmonic 313', 394 | 'bitrate': 198, 395 | 'codegen_time': 0.57198400000000005, 396 | 'decode_time': 0.37954599999999999, 397 | 'duration': 226, 398 | 'filename': 'koln.mp3', 399 | 'genre': 'Electronic', 400 | 'given_duration': 30, 401 | 'release': 'When Machines Exceed Human Intelligence', 402 | 'sample_rate': 44100, 403 | 'samples_decoded': 661816, 404 | 'start_offset': 0, 405 | 'title': 'kln', 406 | 'version': 3.1499999999999999}, 407 | 'tag': 0} 408 | >>> song.identify(query_obj=qo) 409 | [] 410 | >>> 411 | 412 | 413 | """ 414 | post, has_data, data = False, False, False 415 | 416 | if filename: 417 | if os.path.exists(filename): 418 | query_obj = util.codegen(filename, start=codegen_start, duration=codegen_duration) 419 | if query_obj is None: 420 | raise Exception("The filename specified: %s could not be decoded." % filename) 421 | else: 422 | raise Exception("The filename specified: %s does not exist." % filename) 423 | if query_obj and not isinstance(query_obj, list): 424 | query_obj = [query_obj] 425 | 426 | if filename: 427 | # check codegen results from file in case we had a bad result 428 | for q in query_obj: 429 | if 'error' in q: 430 | raise Exception(q['error'] + ": " + q.get('metadata', {}).get('filename', '')) 431 | 432 | if not (filename or query_obj or code): 433 | raise Exception("Not enough information to identify song.") 434 | 435 | kwargs = {} 436 | if code: 437 | has_data = True 438 | kwargs['code'] = code 439 | if title: 440 | kwargs['title'] = title 441 | if release: 442 | kwargs['release'] = release 443 | if duration: 444 | kwargs['duration'] = duration 445 | if genre: 446 | kwargs['genre'] = genre 447 | if buckets: 448 | kwargs['bucket'] = buckets 449 | if version: 450 | kwargs['version'] = version 451 | 452 | if query_obj and any(query_obj): 453 | has_data = True 454 | data = {'query':json.dumps(query_obj)} 455 | post = True 456 | 457 | if has_data: 458 | result = util.callm("%s/%s" % ('song', 'identify'), kwargs, POST=post, data=data) 459 | return [Song(**util.fix(s_dict)) for s_dict in result['response'].get('songs',[])] 460 | 461 | 462 | def search(title=None, artist=None, artist_id=None, combined=None, description=None, style=None, mood=None, 463 | results=None, start=None, max_tempo=None, min_tempo=None, 464 | max_duration=None, min_duration=None, max_loudness=None, min_loudness=None, 465 | artist_max_familiarity=None, artist_min_familiarity=None, artist_max_hotttnesss=None, 466 | artist_min_hotttnesss=None, song_max_hotttnesss=None, song_min_hotttnesss=None, mode=None, 467 | min_energy=None, max_energy=None, min_danceability=None, max_danceability=None, 468 | key=None, max_latitude=None, min_latitude=None, max_longitude=None, min_longitude=None, 469 | sort=None, buckets=None, limit=False, test_new_things=None, rank_type=None, 470 | artist_start_year_after=None, artist_start_year_before=None, artist_end_year_after=None, 471 | artist_end_year_before=None,song_type=None,min_song_currency=None,max_song_currency=None, 472 | min_song_discovery=None, max_song_discovery=None, max_acousticness=None, min_acousticness=None, 473 | max_liveness=None, min_liveness=None, max_speechiness=None, min_speechiness=None, 474 | max_valence=None, min_valence=None): 475 | """Search for songs by name, description, or constraint. 476 | 477 | Args: 478 | 479 | Kwargs: 480 | title (str): the name of a song 481 | artist (str): the name of an artist 482 | artist_id (str): the artist_id 483 | combined (str): the artist name and song title 484 | description (str): A string describing the artist and song 485 | style (str): A string describing the style/genre of the artist and song 486 | mood (str): A string describing the mood of the artist and song 487 | results (int): An integer number of results to return 488 | max_acousticness (float): The max acousticness of song results 489 | min_acousticness (float): The min acousticness of song results 490 | max_tempo (float): The max tempo of song results 491 | min_tempo (float): The min tempo of song results 492 | max_duration (float): The max duration of song results 493 | min_duration (float): The min duration of song results 494 | max_liveness (float): The max liveness of song results 495 | min_liveness (float): The min liveness of song results 496 | max_loudness (float): The max loudness of song results 497 | min_loudness (float): The min loudness of song results 498 | max_speechiness (float): The max speechiness of song results 499 | min_speechiess (float): The min speechiness of song results 500 | max_valence (float): The max valence of song results 501 | min_valence (float): The min valence of song results 502 | artist_max_familiarity (float): A float specifying the max familiarity of artists to search for 503 | artist_min_familiarity (float): A float specifying the min familiarity of artists to search for 504 | artist_max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 505 | artist_min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 506 | song_max_hotttnesss (float): A float specifying the max hotttnesss of songs to search for 507 | song_min_hotttnesss (float): A float specifying the max hotttnesss of songs to search for 508 | max_energy (float): The max energy of song results 509 | min_energy (float): The min energy of song results 510 | max_dancibility (float): The max dancibility of song results 511 | min_dancibility (float): The min dancibility of song results 512 | mode (int): 0 or 1 (minor or major) 513 | key (int): 0-11 (c, c-sharp, d, e-flat, e, f, f-sharp, g, a-flat, a, b-flat, b) 514 | max_latitude (float): A float specifying the max latitude of artists to search for 515 | min_latitude (float): A float specifying the min latitude of artists to search for 516 | max_longitude (float): A float specifying the max longitude of artists to search for 517 | min_longitude (float): A float specifying the min longitude of artists to search for 518 | sort (str): A string indicating an attribute and order for sorting the results 519 | buckets (list): A list of strings specifying which buckets to retrieve 520 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 521 | rank_type (str): A string denoting the desired ranking for description searches, either 'relevance' or 'familiarity 522 | artist_start_year_before (int): Returned songs's artists will have started recording music before this year. 523 | artist_start_year_after (int): Returned songs's artists will have started recording music after this year. 524 | artist_end_year_before (int): Returned songs's artists will have stopped recording music before this year. 525 | artist_end_year_after (int): Returned songs's artists will have stopped recording music after this year. 526 | song_type (string): A string or list of strings specifiying the type of song to search for. 527 | 528 | Returns: 529 | A list of Song objects 530 | 531 | Example: 532 | 533 | >>> results = song.search(artist='shakira', title='she wolf', buckets=['id:7digital', 'tracks'], limit=True, results=1) 534 | >>> results 535 | [] 536 | >>> results[0].get_tracks('7digital')[0] 537 | {u'catalog': u'7digital', 538 | u'foreign_id': u'7digital:track:7854109', 539 | u'id': u'TRTOBSE12903CACEC4', 540 | u'preview_url': u'http://previews.7digital.com/clips/34/7854109.clip.mp3', 541 | u'release_image': u'http://cdn.7static.com/static/img/sleeveart/00/007/081/0000708184_200.jpg'} 542 | >>> 543 | """ 544 | 545 | limit = str(limit).lower() 546 | kwargs = locals() 547 | kwargs['bucket'] = buckets 548 | del kwargs['buckets'] 549 | 550 | result = util.callm("%s/%s" % ('song', 'search'), kwargs) 551 | return [Song(**util.fix(s_dict)) for s_dict in result['response']['songs']] 552 | 553 | def profile(ids=None, track_ids=None, buckets=None, limit=False): 554 | """get the profiles for multiple songs at once 555 | 556 | Args: 557 | ids (str or list): a song ID or list of song IDs 558 | 559 | Kwargs: 560 | buckets (list): A list of strings specifying which buckets to retrieve 561 | 562 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 563 | 564 | Returns: 565 | A list of term document dicts 566 | 567 | Example: 568 | 569 | >>> song_ids = ['SOBSLVH12A8C131F38', 'SOXMSGY1338A5D5873', 'SOJPHZO1376210AFE5', 'SOBHNKR12AB0186218', 'SOSJAHD13770F4D40C'] 570 | >>> songs = song.profile(song_ids, buckets=['audio_summary']) 571 | [, 572 | , 573 | , 574 | ] 575 | >>> songs[0].audio_summary 576 | {u'analysis_url': u'https://echonest-analysis.s3.amazonaws.com/TR/7VRBNguufpHAQQ4ZjJ0eWsIQWl2S2_lrK-7Bp2azHOvPN4VFV-YnU7uO0dXgYtOKT-MTEa/3/full.json?Signature=hmNghHwfEsA4JKWFXnRi7mVP6T8%3D&Expires=1349809918&AWSAccessKeyId=AKIAJRDFEY23UEVW42BQ', 577 | u'audio_md5': u'b6079b2b88f8265be8bdd5fe9702e05c', 578 | u'danceability': 0.64540643050283253, 579 | u'duration': 255.92117999999999, 580 | u'energy': 0.30711665772260549, 581 | u'key': 8, 582 | u'liveness': 0.088994423525370583, 583 | u'loudness': -9.7799999999999994, 584 | u'mode': 1, 585 | u'speechiness': 0.031970700260699259, 586 | u'tempo': 76.049999999999997, 587 | u'time_signature': 4} 588 | >>> 589 | """ 590 | kwargs = {} 591 | 592 | if ids: 593 | if not isinstance(ids, list): 594 | ids = [ids] 595 | kwargs['id'] = ids 596 | 597 | if track_ids: 598 | if not isinstance(track_ids, list): 599 | track_ids = [track_ids] 600 | kwargs['track_id'] = track_ids 601 | 602 | buckets = buckets or [] 603 | if buckets: 604 | kwargs['bucket'] = buckets 605 | 606 | if limit: 607 | kwargs['limit'] = 'true' 608 | 609 | result = util.callm("%s/%s" % ('song', 'profile'), kwargs) 610 | return [Song(**util.fix(s_dict)) for s_dict in result['response']['songs']] 611 | 612 | -------------------------------------------------------------------------------- /pyechonest/artist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | """ 5 | Copyright (c) 2010 The Echo Nest. All rights reserved. 6 | Created by Tyler Williams on 2010-04-25. 7 | 8 | The Artist module loosely covers http://developer.echonest.com/docs/v4/artist.html 9 | Refer to the official api documentation if you are unsure about something. 10 | """ 11 | import util 12 | from proxies import ArtistProxy, ResultList 13 | from song import Song 14 | 15 | 16 | class Artist(ArtistProxy): 17 | """ 18 | An Artist object 19 | 20 | Attributes: 21 | id (str): Echo Nest Artist ID 22 | 23 | name (str): Artist Name 24 | 25 | audio (list): Artist audio 26 | 27 | biographies (list): Artist biographies 28 | 29 | blogs (list): Artist blogs 30 | 31 | familiarity (float): Artist familiarity 32 | 33 | hotttnesss (float): Artist hotttnesss 34 | 35 | images (list): Artist images 36 | 37 | news (list): Artist news 38 | 39 | reviews (list): Artist reviews 40 | 41 | similar (list): Similar Artists 42 | 43 | songs (list): A list of song objects 44 | 45 | terms (list): Terms for an artist 46 | 47 | urls (list): Artist urls 48 | 49 | video (list): Artist video 50 | 51 | years_active (list): A list of dictionaries containing start and stop years 52 | 53 | You create an artist object like this: 54 | 55 | >>> a = artist.Artist('ARH6W4X1187B99274F') 56 | >>> a = artist.Artist('the national') 57 | >>> a = artist.Artist('musicbrainz:artist:a74b1b7f-71a5-4011-9441-d0b5e4122711') 58 | 59 | """ 60 | 61 | def __init__(self, id, **kwargs): 62 | """ 63 | Artist class 64 | 65 | Args: 66 | id (str): an artistw ID 67 | 68 | Returns: 69 | An artist object 70 | 71 | Example: 72 | 73 | >>> a = artist.Artist('ARH6W4X1187B99274F', buckets=['hotttnesss']) 74 | >>> a.hotttnesss 75 | 0.80098515900997658 76 | >>> 77 | 78 | """ 79 | super(Artist, self).__init__(id, **kwargs) 80 | 81 | def __repr__(self): 82 | return "<%s - %s>" % (self._object_type.encode('utf-8'), self.name.encode('utf-8')) 83 | 84 | def __str__(self): 85 | return self.name.encode('utf-8') 86 | 87 | def __cmp__(self, other): 88 | return cmp(self.id, other.id) 89 | 90 | def get_audio(self, results=15, start=0, cache=True): 91 | """Get a list of audio documents found on the web related to an artist 92 | 93 | Args: 94 | 95 | Kwargs: 96 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 97 | 98 | results (int): An integer number of results to return 99 | 100 | start (int): An integer starting value for the result set 101 | 102 | Returns: 103 | A list of audio document dicts; list contains additional attributes 'start' and 'total' 104 | 105 | Example: 106 | 107 | >>> a = artist.Artist('alphabeat') 108 | >>> a.get_audio()[0] 109 | {u'artist': u'Alphabeat', 110 | u'date': u'2010-04-28T01:40:45', 111 | u'id': u'70be4373fa57ac2eee8c7f30b0580899', 112 | u'length': 210.0, 113 | u'link': u'http://iamthecrime.com', 114 | u'release': u'The Beat Is...', 115 | u'title': u'DJ', 116 | u'url': u'http://iamthecrime.com/wp-content/uploads/2010/04/03_DJ_iatc.mp3'} 117 | >>> 118 | """ 119 | 120 | if cache and ('audio' in self.cache) and results==15 and start==0: 121 | return self.cache['audio'] 122 | else: 123 | response = self.get_attribute('audio', results=results, start=start) 124 | if results==15 and start==0: 125 | self.cache['audio'] = ResultList(response['audio'], 0, response['total']) 126 | return ResultList(response['audio'], start, response['total']) 127 | 128 | audio = property(get_audio) 129 | 130 | def get_biographies(self, results=15, start=0, license=None, cache=True): 131 | """Get a list of artist biographies 132 | 133 | Args: 134 | 135 | Kwargs: 136 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 137 | 138 | results (int): An integer number of results to return 139 | 140 | start (int): An integer starting value for the result set 141 | 142 | license (str): A string specifying the desired license type 143 | 144 | Returns: 145 | A list of biography document dicts; list contains additional attributes 'start' and 'total' 146 | 147 | Example: 148 | 149 | >>> a = artist.Artist('britney spears') 150 | >>> bio = a.get_biographies(results=1)[0] 151 | >>> bio['url'] 152 | u'http://www.mtvmusic.com/spears_britney' 153 | >>> 154 | """ 155 | if cache and ('biographies' in self.cache) and results==15 and start==0 and license==None: 156 | return self.cache['biographies'] 157 | else: 158 | response = self.get_attribute('biographies', results=results, start=start, license=license) 159 | if results==15 and start==0 and license==None: 160 | self.cache['biographies'] = ResultList(response['biographies'], 0, response['total']) 161 | return ResultList(response['biographies'], start, response['total']) 162 | 163 | biographies = property(get_biographies) 164 | 165 | def get_blogs(self, results=15, start=0, cache=True, high_relevance=False): 166 | """Get a list of blog articles related to an artist 167 | 168 | Args: 169 | 170 | Kwargs: 171 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 172 | 173 | results (int): An integer number of results to return 174 | 175 | start (int): An ingteger starting value for the result set 176 | 177 | Returns: 178 | A list of blog document dicts; list contains additional attributes 'start' and 'total' 179 | 180 | Example: 181 | 182 | >>> a = artist.Artist('bob marley') 183 | >>> blogs = a.get_blogs(results=1,start=4) 184 | >>> blogs.total 185 | 4068 186 | >>> blogs[0]['summary'] 187 | But the Kenyans I know relate to music about the same way Americans do. They like their Congolese afropop, 188 | and I've known some to be big fans of international acts like Bob Marley and Dolly Parton. 189 | They rarely talk about music that's indigenous in the way a South African or Malian or Zimbabwean would, and it's 190 | even rarer to actually hear such indigenous music. I do sometimes hear ceremonial chanting from the Maasai, but only 191 | when they're dancing for tourists. If East Africa isn't the most musical part ... " 192 | >>> 193 | """ 194 | 195 | if cache and ('blogs' in self.cache) and results==15 and start==0 and not high_relevance: 196 | return self.cache['blogs'] 197 | else: 198 | high_relevance = 'true' if high_relevance else 'false' 199 | response = self.get_attribute('blogs', results=results, start=start, high_relevance=high_relevance) 200 | if results==15 and start==0: 201 | self.cache['blogs'] = ResultList(response['blogs'], 0, response['total']) 202 | return ResultList(response['blogs'], start, response['total']) 203 | 204 | blogs = property(get_blogs) 205 | 206 | def get_familiarity(self, cache=True): 207 | """Get our numerical estimation of how familiar an artist currently is to the world 208 | 209 | Args: 210 | 211 | Kwargs: 212 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 213 | 214 | Returns: 215 | A float representing familiarity. 216 | 217 | Example: 218 | 219 | >>> a = artist.Artist('frank sinatra') 220 | >>> a.get_familiarity() 221 | 0.65142555825947457 222 | >>> a.familiarity 223 | 0.65142555825947457 224 | >>> 225 | """ 226 | if not (cache and ('familiarity' in self.cache)): 227 | response = self.get_attribute('familiarity') 228 | self.cache['familiarity'] = response['artist']['familiarity'] 229 | return self.cache['familiarity'] 230 | 231 | familiarity = property(get_familiarity) 232 | 233 | def get_foreign_id(self, idspace='musicbrainz', cache=True): 234 | """Get the foreign id for this artist for a specific id space 235 | 236 | Args: 237 | 238 | Kwargs: 239 | idspace (str): A string indicating the idspace to fetch a foreign id for. 240 | 241 | Returns: 242 | A foreign ID string 243 | 244 | Example: 245 | 246 | >>> a = artist.Artist('fabulous') 247 | >>> a.get_foreign_id('7digital') 248 | u'7digital:artist:186042' 249 | >>> 250 | """ 251 | if not (cache and ('foreign_ids' in self.cache) and filter(lambda d: d.get('catalog') == idspace, self.cache['foreign_ids'])): 252 | response = self.get_attribute('profile', bucket=['id:'+idspace]) 253 | foreign_ids = response['artist'].get("foreign_ids", []) 254 | self.cache['foreign_ids'] = self.cache.get('foreign_ids', []) + foreign_ids 255 | cval = filter(lambda d: d.get('catalog') == idspace, self.cache.get('foreign_ids')) 256 | return cval[0].get('foreign_id') if cval else None 257 | 258 | def get_twitter_id(self, cache=True): 259 | """Get the twitter id for this artist if it exists 260 | 261 | Args: 262 | 263 | Kwargs: 264 | 265 | Returns: 266 | A twitter ID string 267 | 268 | Example: 269 | 270 | >>> a = artist.Artist('big boi') 271 | >>> a.get_twitter_id() 272 | u'BigBoi' 273 | >>> 274 | """ 275 | if not (cache and ('twitter' in self.cache)): 276 | response = self.get_attribute('twitter') 277 | self.cache['twitter'] = response['artist'].get('twitter') 278 | return self.cache['twitter'] 279 | 280 | def get_hotttnesss(self, cache=True): 281 | """Get our numerical description of how hottt an artist currently is 282 | 283 | Args: 284 | 285 | Kwargs: 286 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 287 | 288 | Returns: 289 | float: the hotttnesss value 290 | 291 | Example: 292 | 293 | >>> a = artist.Artist('hannah montana') 294 | >>> a.get_hotttnesss() 295 | 0.59906022155998995 296 | >>> a.hotttnesss 297 | 0.59906022155998995 298 | >>> 299 | """ 300 | if not (cache and ('hotttnesss' in self.cache)): 301 | response = self.get_attribute('hotttnesss') 302 | self.cache['hotttnesss'] = response['artist']['hotttnesss'] 303 | return self.cache['hotttnesss'] 304 | 305 | hotttnesss = property(get_hotttnesss) 306 | 307 | def get_images(self, results=15, start=0, license=None, cache=True): 308 | """Get a list of artist images 309 | 310 | Args: 311 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 312 | 313 | results (int): An integer number of results to return 314 | 315 | start (int): An integer starting value for the result set 316 | 317 | license (str): A string specifying the desired license type 318 | 319 | Returns: 320 | A list of image document dicts; list contains additional attributes 'start' and 'total' 321 | 322 | Example: 323 | 324 | >>> a = artist.Artist('Captain Beefheart') 325 | >>> images = a.get_images(results=1) 326 | >>> images.total 327 | 49 328 | >>> images[0]['url'] 329 | u'http://c4.ac-images.myspacecdn.com/images01/5/l_e1a329cdfdb16a848288edc6d578730f.jpg' 330 | >>> 331 | """ 332 | 333 | if cache and ('images' in self.cache) and results==15 and start==0 and license==None: 334 | return self.cache['images'] 335 | else: 336 | response = self.get_attribute('images', results=results, start=start, license=license) 337 | total = response.get('total') or 0 338 | if results==15 and start==0 and license==None: 339 | self.cache['images'] = ResultList(response['images'], 0, total) 340 | return ResultList(response['images'], start, total) 341 | 342 | images = property(get_images) 343 | 344 | def get_news(self, results=15, start=0, cache=True, high_relevance=False): 345 | """Get a list of news articles found on the web related to an artist 346 | 347 | Args: 348 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 349 | 350 | results (int): An integer number of results to return 351 | 352 | start (int): An integer starting value for the result set 353 | 354 | Returns: 355 | A list of news document dicts; list contains additional attributes 'start' and 'total' 356 | 357 | Example: 358 | 359 | >>> a = artist.Artist('Henry Threadgill') 360 | >>> news = a.news 361 | >>> news.total 362 | 41 363 | >>> news[0]['name'] 364 | u'Jazz Journalists Association Announces 2010 Jazz Award Winners' 365 | >>> 366 | """ 367 | if cache and ('news' in self.cache) and results==15 and start==0 and not high_relevance: 368 | return self.cache['news'] 369 | else: 370 | high_relevance = 'true' if high_relevance else 'false' 371 | response = self.get_attribute('news', results=results, start=start, high_relevance=high_relevance) 372 | if results==15 and start==0: 373 | self.cache['news'] = ResultList(response['news'], 0, response['total']) 374 | return ResultList(response['news'], start, response['total']) 375 | 376 | news = property(get_news) 377 | 378 | def get_reviews(self, results=15, start=0, cache=True): 379 | """Get reviews related to an artist's work 380 | 381 | Args: 382 | 383 | Kwargs: 384 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 385 | 386 | results (int): An integer number of results to return 387 | 388 | start (int): An integer starting value for the result set 389 | 390 | Returns: 391 | A list of review document dicts; list contains additional attributes 'start' and 'total' 392 | 393 | Example: 394 | 395 | >>> a = artist.Artist('Ennio Morricone') 396 | >>> reviews = a.reviews 397 | >>> reviews.total 398 | 17 399 | >>> reviews[0]['release'] 400 | u'For A Few Dollars More' 401 | >>> 402 | """ 403 | 404 | 405 | 406 | if cache and ('reviews' in self.cache) and results==15 and start==0: 407 | return self.cache['reviews'] 408 | else: 409 | response = self.get_attribute('reviews', results=results, start=start) 410 | if results==15 and start==0: 411 | self.cache['reviews'] = ResultList(response['reviews'], 0, response['total']) 412 | return ResultList(response['reviews'], start, response['total']) 413 | 414 | reviews = property(get_reviews) 415 | 416 | def get_similar(self, results=15, start=0, buckets=None, limit=False, cache=True, max_familiarity=None, min_familiarity=None, \ 417 | max_hotttnesss=None, min_hotttnesss=None, min_results=None, reverse=False, artist_start_year_before=None, \ 418 | artist_start_year_after=None,artist_end_year_before=None,artist_end_year_after=None): 419 | """Return similar artists to this one 420 | 421 | Args: 422 | 423 | Kwargs: 424 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 425 | 426 | results (int): An integer number of results to return 427 | 428 | start (int): An integer starting value for the result set 429 | 430 | max_familiarity (float): A float specifying the max familiarity of artists to search for 431 | 432 | min_familiarity (float): A float specifying the min familiarity of artists to search for 433 | 434 | max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 435 | 436 | min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 437 | 438 | reverse (bool): A boolean indicating whether or not to return dissimilar artists (wrecommender). Defaults to False. 439 | 440 | Returns: 441 | A list of similar Artist objects 442 | 443 | Example: 444 | 445 | >>> a = artist.Artist('Sleater Kinney') 446 | >>> similars = a.similar[:5] 447 | >>> similars 448 | [, , , , ] 449 | >>> 450 | """ 451 | buckets = buckets or [] 452 | kwargs = {} 453 | if max_familiarity: 454 | kwargs['max_familiarity'] = max_familiarity 455 | if min_familiarity: 456 | kwargs['min_familiarity'] = min_familiarity 457 | if max_hotttnesss: 458 | kwargs['max_hotttnesss'] = max_hotttnesss 459 | if min_hotttnesss: 460 | kwargs['min_hotttnesss'] = min_hotttnesss 461 | if min_results: 462 | kwargs['min_results'] = min_results 463 | if buckets: 464 | kwargs['bucket'] = buckets 465 | if limit: 466 | kwargs['limit'] = 'true' 467 | if reverse: 468 | kwargs['reverse'] = 'true' 469 | if artist_start_year_before: 470 | kwargs['artist_start_year_before'] = artist_start_year_before 471 | if artist_start_year_after: 472 | kwargs['artist_start_year_after'] = artist_start_year_after 473 | if artist_end_year_before: 474 | kwargs['artist_end_year_before'] = artist_end_year_before 475 | if artist_end_year_after: 476 | kwargs['artist_end_year_after'] = artist_end_year_after 477 | 478 | 479 | if cache and ('similar' in self.cache) and results==15 and start==0 and (not kwargs): 480 | return [Artist(**util.fix(a)) for a in self.cache['similar']] 481 | else: 482 | response = self.get_attribute('similar', results=results, start=start, **kwargs) 483 | if results==15 and start==0 and (not kwargs): 484 | self.cache['similar'] = response['artists'] 485 | return [Artist(**util.fix(a)) for a in response['artists']] 486 | 487 | similar = property(get_similar) 488 | 489 | def get_songs(self, cache=True, results=15, start=0): 490 | """Get the songs associated with an artist 491 | 492 | Args: 493 | 494 | Kwargs: 495 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 496 | 497 | results (int): An integer number of results to return 498 | 499 | start (int): An integer starting value for the result set 500 | 501 | Results: 502 | A list of Song objects; list contains additional attributes 'start' and 'total' 503 | 504 | Example: 505 | 506 | >>> a = artist.Artist('Strokes') 507 | >>> a.get_songs(results=5) 508 | [, , , , ] 509 | >>> 510 | """ 511 | 512 | if cache and ('songs' in self.cache) and results==15 and start==0: 513 | if not isinstance(self.cache['songs'][0], Song): 514 | song_objects = [] 515 | for s in self.cache["songs"]: 516 | song_objects.append(Song(id=s['id'], 517 | title=s['title'], 518 | artist_name=self.name, 519 | artist_id=self.id)) 520 | self.cache['songs'] = song_objects 521 | return self.cache['songs'] 522 | else: 523 | response = self.get_attribute('songs', results=results, start=start) 524 | for s in response['songs']: 525 | s.update({'artist_id':self.id, 'artist_name':self.name}) 526 | songs = [Song(**util.fix(s)) for s in response['songs']] 527 | if results==15 and start==0: 528 | self.cache['songs'] = ResultList(songs, 0, response['total']) 529 | return ResultList(songs, start, response['total']) 530 | 531 | songs = property(get_songs) 532 | 533 | def get_terms(self, sort='weight', cache=True): 534 | """Get the terms associated with an artist 535 | 536 | Args: 537 | 538 | Kwargs: 539 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 540 | 541 | sort (str): A string specifying the desired sorting type (weight or frequency) 542 | 543 | Results: 544 | A list of term document dicts 545 | 546 | Example: 547 | 548 | >>> a = artist.Artist('tom petty') 549 | >>> a.terms 550 | [{u'frequency': 1.0, u'name': u'heartland rock', u'weight': 1.0}, 551 | {u'frequency': 0.88569401860168606, 552 | u'name': u'jam band', 553 | u'weight': 0.9116501862732439}, 554 | {u'frequency': 0.9656145118557401, 555 | u'name': u'pop rock', 556 | u'weight': 0.89777934440040685}, 557 | {u'frequency': 0.8414744288140491, 558 | u'name': u'southern rock', 559 | u'weight': 0.8698567153186606}, 560 | {u'frequency': 0.9656145118557401, 561 | u'name': u'hard rock', 562 | u'weight': 0.85738022655218893}, 563 | {u'frequency': 0.88569401860168606, 564 | u'name': u'singer-songwriter', 565 | u'weight': 0.77427243392312772}, 566 | {u'frequency': 0.88569401860168606, 567 | u'name': u'rock', 568 | u'weight': 0.71158718989399083}, 569 | {u'frequency': 0.60874110500110956, 570 | u'name': u'album rock', 571 | u'weight': 0.69758668733499629}, 572 | {u'frequency': 0.74350792060935744, 573 | u'name': u'psychedelic', 574 | u'weight': 0.68457367494207944}, 575 | {u'frequency': 0.77213698386292873, 576 | u'name': u'pop', 577 | u'weight': 0.65039556639337293}, 578 | {u'frequency': 0.41747136183050298, 579 | u'name': u'bar band', 580 | u'weight': 0.54974975024767025}] 581 | >>> 582 | 583 | """ 584 | if cache and ('terms' in self.cache) and sort=='weight': 585 | return self.cache['terms'] 586 | else: 587 | response = self.get_attribute('terms', sort=sort) 588 | if sort=='weight': 589 | self.cache['terms'] = response['terms'] 590 | return response['terms'] 591 | 592 | terms = property(get_terms) 593 | 594 | def get_urls(self, cache=True): 595 | """Get the urls for an artist 596 | 597 | Args: 598 | 599 | Kwargs: 600 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 601 | 602 | Results: 603 | A url document dict 604 | 605 | Example: 606 | 607 | >>> a = artist.Artist('the unicorns') 608 | >>> a.get_urls() 609 | {u'amazon_url': u'http://www.amazon.com/gp/search?ie=UTF8&keywords=The Unicorns&tag=httpechonecom-20&index=music', 610 | u'aolmusic_url': u'http://music.aol.com/artist/the-unicorns', 611 | u'itunes_url': u'http://itunes.com/TheUnicorns', 612 | u'lastfm_url': u'http://www.last.fm/music/The+Unicorns', 613 | u'mb_url': u'http://musicbrainz.org/artist/603c5f9f-492a-4f21-9d6f-1642a5dbea2d.html', 614 | u'myspace_url': u'http://www.myspace.com/iwasbornunicorn'} 615 | >>> 616 | 617 | """ 618 | if not (cache and ('urls' in self.cache)): 619 | response = self.get_attribute('urls') 620 | self.cache['urls'] = response['urls'] 621 | return self.cache['urls'] 622 | 623 | urls = property(get_urls) 624 | 625 | def get_video(self, results=15, start=0, cache=True): 626 | """Get a list of video documents found on the web related to an artist 627 | 628 | Args: 629 | 630 | Kwargs: 631 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 632 | 633 | results (int): An integer number of results to return 634 | 635 | start (int): An integer starting value for the result set 636 | 637 | Returns: 638 | A list of video document dicts; list contains additional attributes 'start' and 'total' 639 | 640 | Example: 641 | 642 | >>> a = artist.Artist('the vapors') 643 | >>> a.get_video(results=1, start=2) 644 | [{u'date_found': u'2009-12-28T08:27:48', 645 | u'id': u'd02f9e6dc7904f70402d4676516286b9', 646 | u'image_url': u'http://i1.ytimg.com/vi/p6c0wOFL3Us/default.jpg', 647 | u'site': u'youtube', 648 | u'title': u'The Vapors-Turning Japanese (rectangular white vinyl promo)', 649 | u'url': u'http://youtube.com/watch?v=p6c0wOFL3Us'}] 650 | >>> 651 | 652 | """ 653 | if cache and ('video' in self.cache) and results==15 and start==0: 654 | return self.cache['video'] 655 | else: 656 | response = self.get_attribute('video', results=results, start=start) 657 | if results==15 and start==0: 658 | self.cache['video'] = ResultList(response['video'], 0, response['total']) 659 | return ResultList(response['video'], start, response['total']) 660 | 661 | video = property(get_video) 662 | 663 | def get_years_active(self, cache=True): 664 | """Get a list of years active dictionaries for an artist 665 | 666 | Args: 667 | 668 | Kwargs: 669 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). Defaults to True. 670 | 671 | Returns: 672 | A list of years active dictionaries; list contains additional attributes 'start' and 'total' 673 | 674 | Example: 675 | 676 | >>> a = artist.Artist('yelle') 677 | >>> a.get_years_active() 678 | [{ start: 2005 }] 679 | >>> 680 | 681 | """ 682 | if cache and ('years_active' in self.cache): 683 | return self.cache['years_active'] 684 | else: 685 | response = self.get_attribute('profile', bucket=['years_active']) 686 | self.cache['years_active'] = response['artist']['years_active'] 687 | return response['artist']['years_active'] 688 | 689 | years_active = property(get_years_active) 690 | 691 | def get_doc_counts(self, cache=True): 692 | """ 693 | Get the number of related documents of various types for the artist. 694 | The types include audio, biographies, blogs, images, news, reviews, songs, videos. 695 | 696 | Note that these documents can be retrieved by calling artist., for example, 697 | artist.biographies. 698 | 699 | Args: 700 | 701 | Kwargs: 702 | cache (bool): A boolean indicating whether or not the cached value should be used (if available). 703 | Defaults to True. 704 | 705 | Returns: 706 | A dictionary with one key for each document type, mapped to an integer count of documents. 707 | 708 | Example: 709 | 710 | >>> a = artist.Artist("The Kinks") 711 | 712 | >>> a.get_doc_counts() 713 | {u'audio': 194, 714 | u'biographies': 9, 715 | u'blogs': 379, 716 | u'images': 177, 717 | u'news': 84, 718 | u'reviews': 110, 719 | u'songs': 499, 720 | u'videos': 340} 721 | >>> 722 | """ 723 | if not cache or not ('doc_counts' in self.cache): 724 | response = self.get_attribute("profile", bucket='doc_counts') 725 | self.cache['doc_counts'] = response['artist']['doc_counts'] 726 | return self.cache['doc_counts'] 727 | 728 | doc_counts = property(get_doc_counts) 729 | 730 | def search(name=None, description=None, style=None, mood=None, start=0, \ 731 | results=15, buckets=None, limit=False, \ 732 | fuzzy_match=False, sort=None, max_familiarity=None, min_familiarity=None, \ 733 | max_hotttnesss=None, min_hotttnesss=None, test_new_things=None, rank_type=None, \ 734 | artist_start_year_after=None, artist_start_year_before=None,artist_end_year_after=None,artist_end_year_before=None): 735 | """Search for artists by name, description, or constraint. 736 | 737 | Args: 738 | 739 | Kwargs: 740 | name (str): the name of an artist 741 | 742 | description (str): A string describing the artist 743 | 744 | style (str): A string describing the style/genre of the artist 745 | 746 | mood (str): A string describing the mood of the artist 747 | 748 | start (int): An integer starting value for the result set 749 | 750 | results (int): An integer number of results to return 751 | 752 | buckets (list): A list of strings specifying which buckets to retrieve 753 | 754 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 755 | 756 | fuzzy_match (bool): A boolean indicating whether or not to search for similar sounding matches (only works with name) 757 | 758 | max_familiarity (float): A float specifying the max familiarity of artists to search for 759 | 760 | min_familiarity (float): A float specifying the min familiarity of artists to search for 761 | 762 | max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 763 | 764 | min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 765 | 766 | artist_start_year_before (int): Returned artists will have started recording music before this year. 767 | 768 | artist_start_year_after (int): Returned artists will have started recording music after this year. 769 | 770 | artist_end_year_before (int): Returned artists will have stopped recording music before this year. 771 | 772 | artist_end_year_after (int): Returned artists will have stopped recording music after this year. 773 | 774 | rank_type (str): A string denoting the desired ranking for description searches, either 'relevance' or 'familiarity' 775 | 776 | Returns: 777 | A list of Artist objects 778 | 779 | Example: 780 | 781 | >>> results = artist.search(name='t-pain') 782 | >>> results 783 | [, , , , , , ] 784 | >>> 785 | 786 | """ 787 | limit = str(limit).lower() 788 | fuzzy_match = str(fuzzy_match).lower() 789 | kwargs = locals() 790 | kwargs['bucket'] = buckets or [] 791 | del kwargs['buckets'] 792 | """Search for artists""" 793 | result = util.callm("%s/%s" % ('artist', 'search'), kwargs) 794 | return [Artist(**util.fix(a_dict)) for a_dict in result['response']['artists']] 795 | 796 | def top_hottt(start=0, results=15, buckets = None, limit=False): 797 | """Get the top hotttest artists, according to The Echo Nest 798 | 799 | Args: 800 | 801 | Kwargs: 802 | results (int): An integer number of results to return 803 | 804 | start (int): An integer starting value for the result set 805 | 806 | buckets (list): A list of strings specifying which buckets to retrieve 807 | 808 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 809 | 810 | Returns: 811 | A list of hottt Artist objects 812 | 813 | Example: 814 | 815 | >>> hot_stuff = artist.top_hottt() 816 | >>> hot_stuff 817 | [, , , , , , , , , , , , , , ] 818 | >>> 819 | 820 | """ 821 | buckets = buckets or [] 822 | kwargs = {} 823 | if start: 824 | kwargs['start'] = start 825 | if results: 826 | kwargs['results'] = results 827 | if buckets: 828 | kwargs['bucket'] = buckets 829 | if limit: 830 | kwargs['limit'] = 'true' 831 | 832 | """Get top hottt artists""" 833 | result = util.callm("%s/%s" % ('artist', 'top_hottt'), kwargs) 834 | return [Artist(**util.fix(a_dict)) for a_dict in result['response']['artists']] 835 | 836 | 837 | def top_terms(results=15): 838 | """Get a list of the top overall terms 839 | 840 | Args: 841 | 842 | Kwargs: 843 | results (int): An integer number of results to return 844 | 845 | Returns: 846 | A list of term document dicts 847 | 848 | Example: 849 | 850 | >>> terms = artist.top_terms(results=5) 851 | >>> terms 852 | [{u'frequency': 1.0, u'name': u'rock'}, 853 | {u'frequency': 0.99054710039307992, u'name': u'electronic'}, 854 | {u'frequency': 0.96131624654034398, u'name': u'hip hop'}, 855 | {u'frequency': 0.94358477322411127, u'name': u'jazz'}, 856 | {u'frequency': 0.94023302416455468, u'name': u'pop rock'}] 857 | >>> 858 | """ 859 | 860 | kwargs = {} 861 | if results: 862 | kwargs['results'] = results 863 | 864 | """Get top terms""" 865 | result = util.callm("%s/%s" % ('artist', 'top_terms'), kwargs) 866 | return result['response']['terms'] 867 | 868 | def list_terms(type): 869 | """Get a list of best terms to use with search 870 | 871 | Args: 872 | 873 | Kwargs: 874 | type (str): the type of term to return, either 'mood' or 'style' 875 | 876 | Example: 877 | 878 | >>> best_terms = artist.list_terms('mood') 879 | >>> best_terms 880 | [{u'name': u'aggressive'}, 881 | {u'name': u'ambient'}, 882 | {u'name': u'angry'}, 883 | {u'name': u'angst-ridden'}, 884 | {u'name': u'bouncy'}, 885 | {u'name': u'calming'}, 886 | {u'name': u'carefree'}, etc.] 887 | """ 888 | 889 | kwargs = {'type': type} 890 | result = util.callm("%s/%s" % ('artist', 'list_terms'), kwargs) 891 | return result['response']['terms'] 892 | 893 | def list_genres(): 894 | """Get a list of best genres to use with genre-radio playlisting 895 | 896 | Args: 897 | 898 | Example: 899 | 900 | >>> best_terms = artist.list_genres() 901 | >>> best_terms 902 | [{u'name': u'pop'}, 903 | {u'name': u'rock'}, 904 | {u'name': u'country'}, 905 | """ 906 | kwargs = {} 907 | result = util.callm("%s/%s" % ('artist', 'list_genres'), kwargs) 908 | return result['response']['genres'] 909 | 910 | def similar(names=None, ids=None, start=0, results=15, buckets=None, limit=False, max_familiarity=None, min_familiarity=None, 911 | max_hotttnesss=None, min_hotttnesss=None, seed_catalog=None,artist_start_year_before=None, \ 912 | artist_start_year_after=None,artist_end_year_before=None,artist_end_year_after=None): 913 | """Return similar artists to this one 914 | 915 | Args: 916 | 917 | Kwargs: 918 | ids (str/list): An artist id or list of ids 919 | 920 | names (str/list): An artist name or list of names 921 | 922 | results (int): An integer number of results to return 923 | 924 | buckets (list): A list of strings specifying which buckets to retrieve 925 | 926 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 927 | 928 | start (int): An integer starting value for the result set 929 | 930 | max_familiarity (float): A float specifying the max familiarity of artists to search for 931 | 932 | min_familiarity (float): A float specifying the min familiarity of artists to search for 933 | 934 | max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 935 | 936 | min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 937 | 938 | seed_catalog (str): A string specifying the catalog similar artists are restricted to 939 | 940 | Returns: 941 | A list of similar Artist objects 942 | 943 | Example: 944 | 945 | >>> some_dudes = [artist.Artist('weezer'), artist.Artist('radiohead')] 946 | >>> some_dudes 947 | [, ] 948 | >>> sims = artist.similar(ids=[art.id for art in some_dudes], results=5) 949 | >>> sims 950 | [, , , , ] 951 | >>> 952 | 953 | """ 954 | 955 | buckets = buckets or [] 956 | kwargs = {} 957 | 958 | if ids: 959 | if not isinstance(ids, list): 960 | ids = [ids] 961 | kwargs['id'] = ids 962 | if names: 963 | if not isinstance(names, list): 964 | names = [names] 965 | kwargs['name'] = names 966 | if max_familiarity is not None: 967 | kwargs['max_familiarity'] = max_familiarity 968 | if min_familiarity is not None: 969 | kwargs['min_familiarity'] = min_familiarity 970 | if max_hotttnesss is not None: 971 | kwargs['max_hotttnesss'] = max_hotttnesss 972 | if min_hotttnesss is not None: 973 | kwargs['min_hotttnesss'] = min_hotttnesss 974 | if seed_catalog is not None: 975 | kwargs['seed_catalog'] = seed_catalog 976 | if start: 977 | kwargs['start'] = start 978 | if results: 979 | kwargs['results'] = results 980 | if buckets: 981 | kwargs['bucket'] = buckets 982 | if limit: 983 | kwargs['limit'] = 'true' 984 | if artist_start_year_before: 985 | kwargs['artist_start_year_before'] = artist_start_year_before 986 | if artist_start_year_after: 987 | kwargs['artist_start_year_after'] = artist_start_year_after 988 | if artist_end_year_before: 989 | kwargs['artist_end_year_before'] = artist_end_year_before 990 | if artist_end_year_after: 991 | kwargs['artist_end_year_after'] = artist_end_year_after 992 | 993 | 994 | result = util.callm("%s/%s" % ('artist', 'similar'), kwargs) 995 | return [Artist(**util.fix(a_dict)) for a_dict in result['response']['artists']] 996 | 997 | def extract(text='', start=0, results=15, buckets=None, limit=False, max_familiarity=None, min_familiarity=None, 998 | max_hotttnesss=None, min_hotttnesss=None): 999 | """Extract artist names from a block of text. 1000 | 1001 | Args: 1002 | 1003 | Kwargs: 1004 | text (str): The text to extract artists from 1005 | 1006 | start (int): An integer starting value for the result set 1007 | 1008 | results (int): An integer number of results to return 1009 | 1010 | buckets (list): A list of strings specifying which buckets to retrieve 1011 | 1012 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 1013 | 1014 | max_familiarity (float): A float specifying the max familiarity of artists to search for 1015 | 1016 | min_familiarity (float): A float specifying the min familiarity of artists to search for 1017 | 1018 | max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 1019 | 1020 | min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 1021 | 1022 | Returns: 1023 | A list of Artist objects 1024 | 1025 | Example: 1026 | 1027 | >>> results = artist.extract(text='i saw beyonce at burger king, she was eatin, she was eatin') 1028 | >>> results 1029 | 1030 | >>> 1031 | 1032 | """ 1033 | 1034 | buckets = buckets or [] 1035 | kwargs = {} 1036 | 1037 | kwargs['text'] = text 1038 | 1039 | if max_familiarity is not None: 1040 | kwargs['max_familiarity'] = max_familiarity 1041 | if min_familiarity is not None: 1042 | kwargs['min_familiarity'] = min_familiarity 1043 | if max_hotttnesss is not None: 1044 | kwargs['max_hotttnesss'] = max_hotttnesss 1045 | if min_hotttnesss is not None: 1046 | kwargs['min_hotttnesss'] = min_hotttnesss 1047 | if start: 1048 | kwargs['start'] = start 1049 | if results: 1050 | kwargs['results'] = results 1051 | if buckets: 1052 | kwargs['bucket'] = buckets 1053 | if limit: 1054 | kwargs['limit'] = 'true' 1055 | 1056 | result = util.callm("%s/%s" % ('artist', 'extract'), kwargs) 1057 | 1058 | return [Artist(**util.fix(a_dict)) for a_dict in result['response']['artists']] 1059 | 1060 | 1061 | def suggest(q='', results=15, buckets=None, limit=False, max_familiarity=None, min_familiarity=None, 1062 | max_hotttnesss=None, min_hotttnesss=None): 1063 | """Suggest artists based upon partial names. 1064 | 1065 | Args: 1066 | 1067 | Kwargs: 1068 | q (str): The text to suggest artists from 1069 | 1070 | results (int): An integer number of results to return 1071 | 1072 | buckets (list): A list of strings specifying which buckets to retrieve 1073 | 1074 | limit (bool): A boolean indicating whether or not to limit the results to one of the id spaces specified in buckets 1075 | 1076 | max_familiarity (float): A float specifying the max familiarity of artists to search for 1077 | 1078 | min_familiarity (float): A float specifying the min familiarity of artists to search for 1079 | 1080 | max_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 1081 | 1082 | min_hotttnesss (float): A float specifying the max hotttnesss of artists to search for 1083 | 1084 | Returns: 1085 | A list of Artist objects 1086 | 1087 | Example: 1088 | 1089 | >>> results = artist.suggest(text='rad') 1090 | >>> results 1091 | 1092 | >>> 1093 | 1094 | """ 1095 | 1096 | buckets = buckets or [] 1097 | kwargs = {} 1098 | 1099 | kwargs['q'] = q 1100 | 1101 | if max_familiarity is not None: 1102 | kwargs['max_familiarity'] = max_familiarity 1103 | if min_familiarity is not None: 1104 | kwargs['min_familiarity'] = min_familiarity 1105 | if max_hotttnesss is not None: 1106 | kwargs['max_hotttnesss'] = max_hotttnesss 1107 | if min_hotttnesss is not None: 1108 | kwargs['min_hotttnesss'] = min_hotttnesss 1109 | if results: 1110 | kwargs['results'] = results 1111 | if buckets: 1112 | kwargs['bucket'] = buckets 1113 | if limit: 1114 | kwargs['limit'] = 'true' 1115 | 1116 | result = util.callm("%s/%s" % ('artist', 'suggest'), kwargs) 1117 | 1118 | return [Artist(**util.fix(a_dict)) for a_dict in result['response']['artists']] 1119 | --------------------------------------------------------------------------------