Commit 5dc805df authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 1.1.0

parent ade6b5d0
# Documentation.
include README.rst
# Example script.
# Example scripts.
include aidmatch.py
include fpcalc.py
Metadata-Version: 1.1
Name: pyacoustid
Version: 1.0.0
Version: 1.1.0
Summary: bindings for Chromaprint acoustic fingerprinting and the Acoustid API
Home-page: https://github.com/sampsyo/pyacoustid
Author: Adrian Sampson
......@@ -21,6 +21,9 @@ Description: Chromaprint and Acoustid for Python
Installation
------------
This library works with Python 2 (2.7+, possibly also 2.6) and Python 3
(3.3+).
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
......@@ -37,13 +40,14 @@ Description: Chromaprint and Acoustid for Python
$ pip install pyacoustid
This library uses `audioread`_ to do audio decoding (pip should automatically
install this dependency), but it's not really necessary if you already have
decoded audio.
This library uses `audioread`_ to do audio decoding when not using ``fpcalc``
and `requests`_ to talk to the HTTP API (pip should automatically install
these dependencies).
.. _pip: http://www.pip-installer.org/
.. _PyPI: http://pypi.python.org/
.. _audioread: https://github.com/sampsyo/audioread
.. _requests: http://python-requests.org
Running
......@@ -108,6 +112,10 @@ Description: Chromaprint and Acoustid for Python
Version History
---------------
1.1.0
Include ``fpcalc.py`` script in source distributions.
Add Python 3 support (thanks to Igor Tsarev).
1.0.0
Include ``fpcalc.py``, a script mimicking the ``fpcalc`` program from the
Chromaprint package.
......@@ -169,3 +177,5 @@ Description: Chromaprint and Acoustid for Python
Platform: ALL
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
......@@ -13,6 +13,9 @@ in C but portable, and the Web service, which provides fingerprint lookups.
Installation
------------
This library works with Python 2 (2.7+, possibly also 2.6) and Python 3
(3.3+).
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
......@@ -29,13 +32,14 @@ Then you can install this library from `PyPI`_ using `pip`_::
$ pip install pyacoustid
This library uses `audioread`_ to do audio decoding (pip should automatically
install this dependency), but it's not really necessary if you already have
decoded audio.
This library uses `audioread`_ to do audio decoding when not using ``fpcalc``
and `requests`_ to talk to the HTTP API (pip should automatically install
these dependencies).
.. _pip: http://www.pip-installer.org/
.. _PyPI: http://pypi.python.org/
.. _audioread: https://github.com/sampsyo/audioread
.. _requests: http://python-requests.org
Running
......@@ -100,6 +104,10 @@ used when the Chromaprint library or fpcalc command-line tool cannot be found.
Version History
---------------
1.1.0
Include ``fpcalc.py`` script in source distributions.
Add Python 3 support (thanks to Igor Tsarev).
1.0.0
Include ``fpcalc.py``, a script mimicking the ``fpcalc`` program from the
Chromaprint package.
......
# This file is part of pyacoustid.
# Copyright 2012, Adrian Sampson.
# Copyright 2014, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
......@@ -8,15 +8,16 @@
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division
from __future__ import absolute_import
import os
import json
import urllib
import urllib2
import httplib
import requests
import contextlib
import errno
try:
......@@ -33,7 +34,8 @@ import subprocess
import threading
import time
import gzip
from StringIO import StringIO
from io import BytesIO
API_BASE_URL = 'http://api.acoustid.org/v2/'
DEFAULT_META = 'recordings'
......@@ -42,20 +44,27 @@ MAX_AUDIO_LENGTH = 120 # Seconds.
FPCALC_COMMAND = 'fpcalc'
FPCALC_ENVVAR = 'FPCALC'
# Exceptions.
class AcoustidError(Exception):
"""Base for exceptions in this module."""
class FingerprintGenerationError(AcoustidError):
"""The audio could not be fingerprinted."""
class NoBackendError(FingerprintGenerationError):
"""The audio could not be fingerprinted because neither the
Chromaprint library nor the fpcalc command-line tool is installed.
"""
class FingerprintSubmissionError(AcoustidError):
"""Missing required data for a fingerprint submission."""
class WebServiceError(AcoustidError):
"""The Web service request failed. The field ``message`` contains a
description of the error. If this is an error that was specifically
......@@ -83,6 +92,51 @@ class WebServiceError(AcoustidError):
super(WebServiceError, self).__init__(message)
self.message = message
# Endpoint configuration.
def set_base_url(url):
"""Set the URL of the API server to query."""
if not url.endswith('/'):
url += '/'
global API_BASE_URL
API_BASE_URL = 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'
# Compressed HTTP request bodies.
def _compress(data):
"""Compress a bytestring to a gzip archive."""
sio = BytesIO()
with contextlib.closing(gzip.GzipFile(fileobj=sio, mode='wb')) as f:
f.write(data)
return sio.getvalue()
class CompressedHTTPAdapter(requests.adapters.HTTPAdapter):
"""An `HTTPAdapter` that compresses request bodies with gzip. The
Content-Encoding header is set accordingly.
"""
def add_headers(self, request, **kwargs):
body = request.body
if not isinstance(body, bytes):
body = body.encode('utf8')
request.prepare_body(_compress(body), None)
request.headers['Content-Encoding'] = 'gzip'
# Utilities.
class _rate_limit(object):
"""A decorator that limits the rate at which the function may be
called. The rate is controlled by the REQUEST_INTERVAL module-level
......@@ -107,80 +161,33 @@ class _rate_limit(object):
# Call the original function.
return self.fun(*args, **kwargs)
def _compress(data):
"""Compress a string to a gzip archive."""
sio = StringIO()
with contextlib.closing(gzip.GzipFile(fileobj=sio, mode='wb')) as f:
f.write(data)
return sio.getvalue()
def _decompress(data):
"""Decompress a gzip archive contained in a string."""
sio = StringIO(data)
with contextlib.closing(gzip.GzipFile(fileobj=sio)) as f:
return f.read()
def set_base_url(url):
"""Set the URL of the API server to query."""
if not url.endswith('/'):
url += '/'
global API_BASE_URL
API_BASE_URL = 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
tuple containing the response data and headers.
"""
try:
with contextlib.closing(urllib2.urlopen(req)) as f:
return f.read(), f.info()
except urllib2.HTTPError as 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 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.
"""
# Encode any Unicode values in parameters. (urllib.urlencode in
# Python 2.x operates on bytestrings, so a Unicode error is raised
# if non-ASCII characters are passed in a Unicode string.)
byte_params = {}
for key, value in params.iteritems():
if isinstance(key, unicode):
key = key.encode('utf8')
if isinstance(value, unicode):
value = value.encode('utf8')
byte_params[key] = value
body = _compress(urllib.urlencode(byte_params))
req = urllib2.Request(url, body, {
'Content-Encoding': 'gzip',
headers = {
'Accept-Encoding': 'gzip',
})
"Content-Type": "application/x-www-form-urlencoded"
}
data, headers = _send_request(req)
if headers.get('Content-Encoding') == 'gzip':
data = _decompress(data)
session = requests.Session()
session.mount('http://', CompressedHTTPAdapter())
try:
response = session.post(url, data=params, headers=headers)
except requests.exceptions.RequestException as exc:
raise WebServiceError("HTTP request failed: {0}".format(exc))
try:
return json.loads(data)
return response.json()
except ValueError:
raise WebServiceError('response is not valid JSON')
# Main API.
def fingerprint(samplerate, channels, pcmiter, maxlength=MAX_AUDIO_LENGTH):
"""Fingerprint audio data given its sample rate and number of
channels. pcmiter should be an iterable containing blocks of PCM
......@@ -205,6 +212,7 @@ def fingerprint(samplerate, channels, pcmiter, maxlength=MAX_AUDIO_LENGTH):
except chromaprint.FingerprintError:
raise FingerprintGenerationError("fingerprint calculation failed")
def lookup(apikey, fingerprint, duration, meta=DEFAULT_META):
"""Look up a fingerprint with the Acoustid Web service. Returns the
Python object reflecting the response JSON data.
......@@ -218,6 +226,7 @@ def lookup(apikey, fingerprint, duration, meta=DEFAULT_META):
}
return _api_request(_get_lookup_url(), params)
def parse_lookup_result(data):
"""Given a parsed JSON response, generate tuples containing the match
score, the MusicBrainz recording ID, the title of the recording, and
......@@ -246,6 +255,7 @@ def parse_lookup_result(data):
yield score, recording['id'], recording.get('title'), artist_name
def _fingerprint_file_audioread(path, maxlength):
"""Fingerprint a file by using audioread and chromaprint."""
try:
......@@ -256,6 +266,7 @@ def _fingerprint_file_audioread(path, maxlength):
raise FingerprintGenerationError("audio could not be decoded")
return duration, fp
def _fingerprint_file_fpcalc(path, maxlength):
"""Fingerprint a file by calling the fpcalc application."""
fpcalc = os.environ.get(FPCALC_ENVVAR, FPCALC_COMMAND)
......@@ -284,21 +295,22 @@ def _fingerprint_file_fpcalc(path, maxlength):
duration = fp = None
for line in output.splitlines():
try:
parts = line.split('=', 1)
parts = line.split(b'=', 1)
except ValueError:
raise FingerprintGenerationError("malformed fpcalc output")
if parts[0] == 'DURATION':
if parts[0] == b'DURATION':
try:
duration = int(parts[1])
except ValueError:
raise FingerprintGenerationError("fpcalc duration not numeric")
elif parts[0] == 'FINGERPRINT':
elif parts[0] == b'FINGERPRINT':
fp = parts[1]
if duration is None or fp is None:
raise FingerprintGenerationError("missing fpcalc output")
return duration, fp
def fingerprint_file(path, maxlength=MAX_AUDIO_LENGTH):
"""Fingerprint a file either using the Chromaprint dynamic library
or the fpcalc command-line tool, whichever is available. Returns the
......@@ -310,6 +322,7 @@ def fingerprint_file(path, maxlength=MAX_AUDIO_LENGTH):
else:
return _fingerprint_file_fpcalc(path, maxlength)
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
......@@ -323,6 +336,7 @@ def match(apikey, path, meta=DEFAULT_META, parse=True):
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
......
from __future__ import print_function
# This file is part of pyacoustid.
# Copyright 2011, Adrian Sampson.
#
......@@ -27,13 +28,13 @@ def aidmatch(filename):
try:
results = acoustid.match(API_KEY, filename)
except acoustid.NoBackendError:
print >>sys.stderr, "chromaprint library/tool not found"
print("chromaprint library/tool not found", file=sys.stderr)
sys.exit(1)
except acoustid.FingerprintGenerationError:
print >>sys.stderr, "fingerprint could not be calculated"
print("fingerprint could not be calculated", file=sys.stderr)
sys.exit(1)
except acoustid.WebServiceError, exc:
print >>sys.stderr, "web service request failed:", exc.message
except acoustid.WebServiceError as exc:
print("web service request failed:", exc.message, file=sys.stderr)
sys.exit(1)
first = True
......@@ -41,10 +42,10 @@ def aidmatch(filename):
if first:
first = False
else:
print
print '%s - %s' % (artist, title)
print 'http://musicbrainz.org/recording/%s' % rid
print 'Score: %i%%' % (int(score * 100))
print()
print('%s - %s' % (artist, title))
print('http://musicbrainz.org/recording/%s' % rid)
print('Score: %i%%' % (int(score * 100)))
if __name__ == '__main__':
aidmatch(sys.argv[1])
# Copyright (C) 2011 Lukas Lalinsky
# (Minor modifications by Adrian Sampson.)
# Distributed under the MIT license, see the LICENSE file for details.
# Distributed under the MIT license, see the LICENSE file for details.
"""Low-level ctypes wrapper from the chromaprint library."""
......@@ -8,6 +8,14 @@ import sys
import ctypes
if sys.version_info[0] >= 3:
BUFFER_TYPES = (memoryview,)
elif sys.version_info[1] >= 7:
BUFFER_TYPES = (buffer, memoryview,)
else:
BUFFER_TYPES = (buffer,)
# Find the base library and declare prototypes.
def _guess_lib_name():
......@@ -105,10 +113,10 @@ class Fingerprinter(object):
"""Send raw PCM audio data to the fingerprinter. Data may be
either a bytestring or a buffer object.
"""
if isinstance(data, buffer):
if isinstance(data, BUFFER_TYPES):
data = str(data)
elif not isinstance(data, str):
raise TypeError('data must be str or buffer')
elif not isinstance(data, bytes):
raise TypeError('data must be bytes, buffer, or memoryview')
_check(_libchromaprint.chromaprint_feed(
self._ctx, data, len(data) // 2
))
......
#!/usr/bin/env python
# This file is part of pyacoustid.
# Copyright 2012, Lukas Lalinsky.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Simple script for calculating audio fingerprints, using the same
arguments/output as the fpcalc utility from Chromaprint."""
from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
import sys
import argparse
import acoustid
import chromaprint
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-length', metavar='SECS', type=int, default=120,
help='length of the audio data used for fingerprint '
'calculation (default 120)')
parser.add_argument('-raw', action='store_true',
help='output the raw uncompressed fingerprint')
parser.add_argument('paths', metavar='FILE', nargs='+',
help='audio file to be fingerprinted')
args = parser.parse_args()
del sys.argv[1:] # to make gst not try to parse the args
first = True
for i, path in enumerate(args.paths):
try:
duration, fp = acoustid.fingerprint_file(path, args.length)
except Exception:
print("ERROR: unable to calculate fingerprint "
"for file %s, skipping" % path, file=sys.stderr)
continue
if args.raw:
raw_fp = chromaprint.decode_fingerprint(fp)[0]
fp = ','.join(map(str, raw_fp))
if not first:
print
first = False
print('FILE=%s' % path)
print('DURATION=%d' % duration)
print('FINGERPRINT=%s' % fp.decode('utf8'))
if __name__ == '__main__':
main()
Metadata-Version: 1.1
Name: pyacoustid
Version: 1.0.0
Version: 1.1.0
Summary: bindings for Chromaprint acoustic fingerprinting and the Acoustid API
Home-page: https://github.com/sampsyo/pyacoustid
Author: Adrian Sampson
......@@ -21,6 +21,9 @@ Description: Chromaprint and Acoustid for Python
Installation
------------
This library works with Python 2 (2.7+, possibly also 2.6) and Python 3
(3.3+).
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
......@@ -37,13 +40,14 @@ Description: Chromaprint and Acoustid for Python
$ pip install pyacoustid
This library uses `audioread`_ to do audio decoding (pip should automatically
install this dependency), but it's not really necessary if you already have
decoded audio.
This library uses `audioread`_ to do audio decoding when not using ``fpcalc``
and `requests`_ to talk to the HTTP API (pip should automatically install
these dependencies).
.. _pip: http://www.pip-installer.org/
.. _PyPI: http://pypi.python.org/
.. _audioread: https://github.com/sampsyo/audioread
.. _requests: http://python-requests.org
Running
......@@ -108,6 +112,10 @@ Description: Chromaprint and Acoustid for Python
Version History
---------------
1.1.0
Include ``fpcalc.py`` script in source distributions.
Add Python 3 support (thanks to Igor Tsarev).
1.0.0
Include ``fpcalc.py``, a script mimicking the ``fpcalc`` program from the
Chromaprint package.
......@@ -169,3 +177,5 @@ Description: Chromaprint and Acoustid for Python
Platform: ALL
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
......@@ -3,6 +3,7 @@ README.rst
acoustid.py
aidmatch.py
chromaprint.py
fpcalc.py
setup.py
pyacoustid.egg-info/PKG-INFO
pyacoustid.egg-info/SOURCES.txt
......
audioread
\ No newline at end of file
audioread
requests
\ No newline at end of file
......@@ -13,11 +13,14 @@
# included in all copies or substantial portions of the Software.
import os
import sys
from setuptools import setup
def _read(fn):
path = os.path.join(os.path.dirname(__file__), fn)
data = open(path).read().decode('utf8')
data = open(path).read()
if sys.version_info[0] < 3:
data = data.decode('utf8')
# Special case some Unicode characters; PyPI seems to only like ASCII.
data = data.replace(u'\xe1', u'a')
data = data.replace(u'\u0161', u's')
......@@ -25,7 +28,7 @@ def _read(fn):
return data
setup(name='pyacoustid',
version='1.0.0',
version='1.1.0',
description=
'bindings for Chromaprint acoustic fingerprinting and the '
'Acoustid API',
......@@ -36,7 +39,7 @@ setup(name='pyacoustid',
platforms='ALL',
long_description=_read('README.rst'),
install_requires = ['audioread'],
install_requires = ['audioread', 'requests'],
py_modules=[
'chromaprint',
......@@ -46,5 +49,7 @@ setup(name='pyacoustid',
classifiers=[
'Topic :: Multimedia :: Sound/Audio :: Conversion',
'Intended Audience :: Developers',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
],
)
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