Commit d94514f1 authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 0.4

parent 7e4bf59e
Metadata-Version: 1.0
Name: pyacoustid
Version: 0.3
Version: 0.4
Summary: bindings for Chromaprint acoustic fingerprinting and the Acoustid API
Home-page: https://github.com/sampsyo/pyacoustid
Author: Adrian Sampson
......@@ -24,10 +24,14 @@ Description: Chromaprint and Acoustid for Python
First, install the `Chromaprint`_ fingerprinting library by `Lukas Lalinsky`__.
(The library itself depends on an FFT library, but it's smart enough to use an
algorithm from software you probably already have installed; see the Chromaprint
page for details.)
page for details.) This module can use either the Chromaprint dynamic library or
the ``fpcalc`` command-line tool, which itself depends on `libavcodec`_. If you
use ``fpcalc``, either ensure that it is on your ``$PATH`` or set the ``FPCALC``
environment variable to its location.
__ lukas_
.. _lukas: http://oxygene.sk/lukas/
.. _libavcodec: http://ffmpeg.org/
Then you can install this library from `PyPI`_ using `pip`_::
......@@ -100,6 +104,14 @@ Description: Chromaprint and Acoustid for Python
Version History
---------------
0.4
Fingerprinting can now fall back to using the ``fpcalc`` command-line tool
instead of the Chromaprint dynamic library so the library can be used with
the binary distributions (thanks to Lukas Lalinsky).
Fingerprint submission (thanks to Alastair Porter).
Data chunks can now be buffers as well as bytestrings (fixes compatibility
with pymad).
0.3
Configurable API base URL.
Result parser now generates all results instead of returning just one.
......
......@@ -16,10 +16,14 @@ Installation
First, install the `Chromaprint`_ fingerprinting library by `Lukáš Lalinský`__.
(The library itself depends on an FFT library, but it's smart enough to use an
algorithm from software you probably already have installed; see the Chromaprint
page for details.)
page for details.) This module can use either the Chromaprint dynamic library or
the ``fpcalc`` command-line tool, which itself depends on `libavcodec`_. If you
use ``fpcalc``, either ensure that it is on your ``$PATH`` or set the ``FPCALC``
environment variable to its location.
__ lukas_
.. _lukas: http://oxygene.sk/lukas/
.. _libavcodec: http://ffmpeg.org/
Then you can install this library from `PyPI`_ using `pip`_::
......@@ -92,6 +96,14 @@ server.
Version History
---------------
0.4
Fingerprinting can now fall back to using the ``fpcalc`` command-line tool
instead of the Chromaprint dynamic library so the library can be used with
the binary distributions (thanks to Lukáš Lalinský).
Fingerprint submission (thanks to Alastair Porter).
Data chunks can now be buffers as well as bytestrings (fixes compatibility
with pymad).
0.3
Configurable API base URL.
Result parser now generates all results instead of returning just one.
......
# This file is part of pyacoustid.
# Copyright 2011, Adrian Sampson.
# Copyright 2012, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
......@@ -18,17 +18,28 @@ import urllib
import urllib2
import httplib
import contextlib
import audioread
try:
import audioread
have_audioread = True
except ImportError:
have_audioread = False
try:
import chromaprint
have_chromaprint = True
except ImportError:
have_chromaprint = False
import subprocess
import threading
import time
import gzip
from StringIO import StringIO
import chromaprint
API_BASE_URL = 'http://api.acoustid.org/v2/'
DEFAULT_META = 'recordings'
REQUEST_INTERVAL = 0.33 # 3 requests/second.
MAX_AUDIO_LENGTH = 120 # Seconds.
FPCALC_COMMAND = 'fpcalc'
FPCALC_ENVVAR = 'FPCALC'
class AcoustidError(Exception):
"""Base for exceptions in this module."""
......@@ -36,8 +47,35 @@ class AcoustidError(Exception):
class FingerprintGenerationError(AcoustidError):
"""The audio could not be fingerprinted."""
class FingerprintSubmissionError(AcoustidError):
"""Missing required data for a fingerprint submission."""
class WebServiceError(AcoustidError):
"""The Web service request failed."""
"""The Web service request failed. The field ``message`` contains a
description of the error. If this is an error that was specifically
sent by the acoustid server, then the ``code`` field contains the
acoustid error code.
"""
def __init__(self, message, response=None):
"""Create an error for the given HTTP response body, if
provided, with the ``message`` as a fallback.
"""
if response:
# Try to parse the JSON error response.
try:
data = json.loads(response)
except ValueError:
pass
else:
if isinstance(data.get('error'), dict):
error = data['error']
if 'message' in error:
message = error['message']
if 'code' in error:
self.code = error['code']
super(WebServiceError, self).__init__(message)
self.message = message
class _rate_limit(object):
"""A decorator that limits the rate at which the function may be
......@@ -83,10 +121,14 @@ def set_base_url(url):
global API_BASE_URL
API_BASE_URL = url
def get_lookup_url():
def _get_lookup_url():
"""Get the URL of the lookup API endpoint."""
return API_BASE_URL + 'lookup'
def _get_submit_url():
"""Get the URL of the submission API endpoint."""
return API_BASE_URL + 'submit'
@_rate_limit
def _send_request(req):
"""Given a urllib2 Request object, make the request and return a
......@@ -95,17 +137,17 @@ def _send_request(req):
try:
with contextlib.closing(urllib2.urlopen(req)) as f:
return f.read(), f.info()
except urllib2.HTTPError:
raise WebServiceError('HTTP request error')
except urllib2.HTTPError, exc:
raise WebServiceError('HTTP status %i' % exc.code, exc.read())
except httplib.BadStatusLine:
raise WebServiceError('bad HTTP status line')
except IOError:
raise WebServiceError('connection failed')
def _api_request(url, params):
"""Makes a GET request for the URL with the given form parameters
and returns a parsed JSON response. May raise a WebServiceError if
the request fails.
"""Makes a POST request for the URL with the given form parameters,
which are encoded as compressed form data, and returns a parsed JSON
response. May raise a WebServiceError if the request fails.
"""
body = _compress(urllib.urlencode(params))
req = urllib2.Request(url, body, {
......@@ -157,7 +199,7 @@ def lookup(apikey, fingerprint, duration, meta=DEFAULT_META):
'fingerprint': fingerprint,
'meta': meta,
}
return _api_request(get_lookup_url(), params)
return _api_request(_get_lookup_url(), params)
def parse_lookup_result(data):
"""Given a parsed JSON response, generate tuples containing the match
......@@ -173,7 +215,7 @@ def parse_lookup_result(data):
for result in data['results']:
score = result['score']
if not result['recordings']:
if not result.get('recordings'):
# No recording attached. This result is not very useful.
continue
recording = result['recordings'][0]
......@@ -187,6 +229,43 @@ def parse_lookup_result(data):
yield score, recording['id'], recording['title'], artist_name
def _fingerprint_file_audioread(path):
"""Fingerprint a file by using audioread and chromaprint."""
try:
with audioread.audio_open(path) as f:
duration = f.duration
fp = fingerprint(f.samplerate, f.channels, iter(f))
except audioread.DecodeError:
raise FingerprintGenerationError("audio could not be decoded")
return duration, fp
def _fingerprint_file_fpcalc(path):
"""Fingerprint a file by calling the fpcalc application."""
fpcalc = os.environ.get(FPCALC_ENVVAR, FPCALC_COMMAND)
command = [fpcalc, "-length", str(MAX_AUDIO_LENGTH), path]
try:
output = subprocess.check_output(command)
except (OSError, subprocess.CalledProcessError):
raise FingerprintGenerationError("fpcalc invocation failed")
duration = fp = None
for line in output.splitlines():
try:
parts = line.split('=', 1)
except ValueError:
raise FingerprintGenerationError("malformed fpcalc output")
if parts[0] == 'DURATION':
try:
duration = int(parts[1])
except ValueError:
raise FingerprintGenerationError("fpcalc duration not numeric")
elif parts[0] == 'FINGERPRINT':
fp = parts[1]
if duration is None or fp is None:
raise FingerprintGenerationError("missing fpcalc output")
return duration, fp
def match(apikey, path, meta=DEFAULT_META, parse=True):
"""Look up the metadata for an audio file. If ``parse`` is true,
then ``parse_lookup_result`` is used to return an iterator over
......@@ -194,14 +273,48 @@ def match(apikey, path, meta=DEFAULT_META, parse=True):
response is returned.
"""
path = os.path.abspath(os.path.expanduser(path))
try:
with audioread.audio_open(path) as f:
duration = f.duration
fp = fingerprint(f.samplerate, f.channels, iter(f))
except audioread.DecodeError:
raise FingerprintGenerationError("audio could not be decoded")
if have_audioread and have_chromaprint:
duration, fp = _fingerprint_file_audioread(path)
else:
duration, fp = _fingerprint_file_fpcalc(path)
response = lookup(apikey, fp, duration, meta)
if parse:
return parse_lookup_result(response)
else:
return response
def submit(apikey, userkey, data):
"""Submit a fingerprint to the acoustid server. The ``apikey`` and
``userkey`` parameters are API keys for the application and the
submitting user, respectively.
``data`` may be either a single dictionary or a list of
dictionaries. In either case, each dictionary must contain a
``fingerprint`` key and a ``duration`` key and may include the
following: ``puid``, ``mbid``, ``track``, ``artist``, ``album``,
``albumartist``, ``year``, ``trackno``, ``discno``, ``fileformat``,
``bitrate``
If the required keys are not present in a dictionary, a
FingerprintSubmissionError is raised.
"""
if isinstance(data, dict):
data = [data]
args = {
'format': 'json',
'client': apikey,
'user': userkey,
}
# Build up "field.#" parameters corresponding to the parameters
# given in each dictionary.
for i, d in enumerate(data):
if "duration" not in d or "fingerprint" not in d:
raise FingerprintSubmissionError("missing required parameters")
for k, v in d.iteritems():
args["%s.%s" % (k, i)] = v
response = _api_request(_get_submit_url(), args)
if response['status'] != 'ok':
raise WebServiceError("status: %s" % data['status'])
......@@ -29,8 +29,8 @@ def aidmatch(filename):
except acoustid.FingerprintGenerationError:
print >>sys.stderr, "fingerprint could not be calculated"
sys.exit(1)
except acoustid.WebServiceError:
print >>sys.stderr, "web service request failed"
except acoustid.WebServiceError, exc:
print >>sys.stderr, "web service request failed:", exc.message
sys.exit(1)
first = True
......
......@@ -102,7 +102,13 @@ class Fingerprinter(object):
))
def feed(self, data):
"""Send raw PCM audio data to the fingerprinter."""
"""Send raw PCM audio data to the fingerprinter. Data may be
either a bytestring or a buffer object.
"""
if isinstance(data, buffer):
data = str(data)
elif not isinstance(data, str):
raise TypeError('data must be str or buffer')
_check(_libchromaprint.chromaprint_feed(
self._ctx, data, len(data) // 2
))
......
Metadata-Version: 1.0
Name: pyacoustid
Version: 0.3
Version: 0.4
Summary: bindings for Chromaprint acoustic fingerprinting and the Acoustid API
Home-page: https://github.com/sampsyo/pyacoustid
Author: Adrian Sampson
......@@ -24,10 +24,14 @@ Description: Chromaprint and Acoustid for Python
First, install the `Chromaprint`_ fingerprinting library by `Lukas Lalinsky`__.
(The library itself depends on an FFT library, but it's smart enough to use an
algorithm from software you probably already have installed; see the Chromaprint
page for details.)
page for details.) This module can use either the Chromaprint dynamic library or
the ``fpcalc`` command-line tool, which itself depends on `libavcodec`_. If you
use ``fpcalc``, either ensure that it is on your ``$PATH`` or set the ``FPCALC``
environment variable to its location.
__ lukas_
.. _lukas: http://oxygene.sk/lukas/
.. _libavcodec: http://ffmpeg.org/
Then you can install this library from `PyPI`_ using `pip`_::
......@@ -100,6 +104,14 @@ Description: Chromaprint and Acoustid for Python
Version History
---------------
0.4
Fingerprinting can now fall back to using the ``fpcalc`` command-line tool
instead of the Chromaprint dynamic library so the library can be used with
the binary distributions (thanks to Lukas Lalinsky).
Fingerprint submission (thanks to Alastair Porter).
Data chunks can now be buffers as well as bytestrings (fixes compatibility
with pymad).
0.3
Configurable API base URL.
Result parser now generates all results instead of returning just one.
......
......@@ -25,7 +25,7 @@ def _read(fn):
return data
setup(name='pyacoustid',
version='0.3',
version='0.4',
description=
'bindings for Chromaprint acoustic fingerprinting and the '
'Acoustid API',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment