pyacoustid-1.1.2/0000755000175000017500000000000012723350075015367 5ustar asampsonasampson00000000000000pyacoustid-1.1.2/chromaprint.py0000644000175000017500000001304712667632345020306 0ustar asampsonasampson00000000000000# Copyright (C) 2011 Lukas Lalinsky # (Minor modifications by Adrian Sampson.) # Distributed under the MIT license, see the LICENSE file for details. """Low-level ctypes wrapper from the chromaprint library.""" 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(): if sys.platform == 'darwin': return ('libchromaprint.1.dylib', 'libchromaprint.0.dylib') elif sys.platform == 'win32': return ('chromaprint.dll', 'libchromaprint.dll') elif sys.platform == 'cygwin': return ('libchromaprint.dll.a', 'cygchromaprint-1.dll', 'cygchromaprint-0.dll') return ('libchromaprint.so.1', 'libchromaprint.so.0') for name in _guess_lib_name(): try: _libchromaprint = ctypes.cdll.LoadLibrary(name) break except OSError: pass else: raise ImportError("couldn't find libchromaprint") _libchromaprint.chromaprint_get_version.argtypes = () _libchromaprint.chromaprint_get_version.restype = ctypes.c_char_p _libchromaprint.chromaprint_new.argtypes = (ctypes.c_int,) _libchromaprint.chromaprint_new.restype = ctypes.c_void_p _libchromaprint.chromaprint_free.argtypes = (ctypes.c_void_p,) _libchromaprint.chromaprint_free.restype = None _libchromaprint.chromaprint_start.argtypes = \ (ctypes.c_void_p, ctypes.c_int, ctypes.c_int) _libchromaprint.chromaprint_start.restype = ctypes.c_int _libchromaprint.chromaprint_feed.argtypes = \ (ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_int) _libchromaprint.chromaprint_feed.restype = ctypes.c_int _libchromaprint.chromaprint_finish.argtypes = (ctypes.c_void_p,) _libchromaprint.chromaprint_finish.restype = ctypes.c_int _libchromaprint.chromaprint_get_fingerprint.argtypes = \ (ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p)) _libchromaprint.chromaprint_get_fingerprint.restype = ctypes.c_int _libchromaprint.chromaprint_decode_fingerprint.argtypes = \ (ctypes.POINTER(ctypes.c_char), ctypes.c_int, ctypes.POINTER(ctypes.POINTER(ctypes.c_int32)), ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int), ctypes.c_int) _libchromaprint.chromaprint_decode_fingerprint.restype = ctypes.c_int _libchromaprint.chromaprint_encode_fingerprint.argtypes = \ (ctypes.POINTER(ctypes.c_int32), ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.POINTER(ctypes.c_char)), ctypes.POINTER(ctypes.c_int), ctypes.c_int) _libchromaprint.chromaprint_encode_fingerprint.restype = ctypes.c_int _libchromaprint.chromaprint_dealloc.argtypes = (ctypes.c_void_p,) _libchromaprint.chromaprint_dealloc.restype = None # Main interface. class FingerprintError(Exception): """Raised when a call to the underlying library fails.""" def _check(res): """Check the result of a library call, raising an error if the call failed. """ if res != 1: raise FingerprintError() class Fingerprinter(object): ALGORITHM_TEST1 = 0 ALGORITHM_TEST2 = 1 ALGORITHM_TEST3 = 2 ALGORITHM_DEFAULT = ALGORITHM_TEST2 def __init__(self, algorithm=ALGORITHM_DEFAULT): self._ctx = _libchromaprint.chromaprint_new(algorithm) def __del__(self): _libchromaprint.chromaprint_free(self._ctx) del self._ctx def start(self, sample_rate, num_channels): """Initialize the fingerprinter with the given audio parameters. """ _check(_libchromaprint.chromaprint_start( self._ctx, sample_rate, num_channels )) def feed(self, data): """Send raw PCM audio data to the fingerprinter. Data may be either a bytestring or a buffer object. """ if isinstance(data, BUFFER_TYPES): data = str(data) elif not isinstance(data, bytes): raise TypeError('data must be bytes, buffer, or memoryview') _check(_libchromaprint.chromaprint_feed( self._ctx, data, len(data) // 2 )) def finish(self): """Finish the fingerprint generation process and retrieve the resulting fignerprint as a bytestring. """ _check(_libchromaprint.chromaprint_finish(self._ctx)) fingerprint_ptr = ctypes.c_char_p() _check(_libchromaprint.chromaprint_get_fingerprint( self._ctx, ctypes.byref(fingerprint_ptr) )) fingerprint = fingerprint_ptr.value _libchromaprint.chromaprint_dealloc(fingerprint_ptr) return fingerprint def decode_fingerprint(data, base64=True): result_ptr = ctypes.POINTER(ctypes.c_int32)() result_size = ctypes.c_int() algorithm = ctypes.c_int() _check(_libchromaprint.chromaprint_decode_fingerprint( data, len(data), ctypes.byref(result_ptr), ctypes.byref(result_size), ctypes.byref(algorithm), 1 if base64 else 0 )) result = result_ptr[:result_size.value] _libchromaprint.chromaprint_dealloc(result_ptr) return result, algorithm.value def encode_fingerprint(fingerprint, algorithm, base64=True): fp_array = (ctypes.c_int * len(fingerprint))() for i in range(len(fingerprint)): fp_array[i] = fingerprint[i] result_ptr = ctypes.POINTER(ctypes.c_char)() result_size = ctypes.c_int() _check(_libchromaprint.chromaprint_encode_fingerprint( fp_array, len(fingerprint), algorithm, ctypes.byref(result_ptr), ctypes.byref(result_size), 1 if base64 else 0 )) result = result_ptr[:result_size.value] _libchromaprint.chromaprint_dealloc(result_ptr) return result pyacoustid-1.1.2/aidmatch.py0000644000175000017500000000425412667634352017532 0ustar asampsonasampson00000000000000from __future__ import print_function # This file is part of pyacoustid. # Copyright 2011, Adrian Sampson. # # 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. """Example script that identifies metadata for files specified on the command line. """ import acoustid import sys # API key for this demo script only. Get your own API key at the # Acoustid Web for your application. # http://acoustid.org/ API_KEY = 'cSpUJKpD' # Python 2/3 Unicode compatibility: this `print_` function forces a # unicode string into a byte string for printing on Python 2, avoiding # errors in the process, and does nothing on Python 3, where # stdout/stderr are text streams (and there's not much we can do about # that). if sys.version_info[0] < 3: def print_(s): print(s.encode(sys.stdout.encoding, 'replace')) else: def print_(s): print(s) def aidmatch(filename): try: results = acoustid.match(API_KEY, filename) except acoustid.NoBackendError: print("chromaprint library/tool not found", file=sys.stderr) sys.exit(1) except acoustid.FingerprintGenerationError: print("fingerprint could not be calculated", file=sys.stderr) sys.exit(1) except acoustid.WebServiceError as exc: print("web service request failed:", exc.message, file=sys.stderr) sys.exit(1) first = True for score, rid, title, artist in results: if first: first = False else: 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]) pyacoustid-1.1.2/MANIFEST.in0000644000175000017500000000013512667632345017136 0ustar asampsonasampson00000000000000# Documentation. include README.rst # Example scripts. include aidmatch.py include fpcalc.py pyacoustid-1.1.2/setup.cfg0000644000175000017500000000007312723350075017210 0ustar asampsonasampson00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pyacoustid-1.1.2/README.rst0000644000175000017500000001457312667634426017104 0ustar asampsonasampson00000000000000Chromaprint and Acoustid for Python =================================== `Chromaprint`_ and its associated `Acoustid`_ Web service make up a high-quality, open-source acoustic fingerprinting system. This package provides Python bindings for both the fingerprinting algorithm library, which is written in C but portable, and the Web service, which provides fingerprint lookups. .. _Chromaprint: http://acoustid.org/ .. _Acoustid: http://acoustid.org/chromaprint 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 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`_:: $ pip install pyacoustid 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 ------- You can run the included demonstration script, ``aidmatch.py``, to test your installation:: $ python aidmatch.py mysterious_music.mp3 This will show the top metadata match from Acoustid's database. The script uses `audioread`_ to decode music, so it should transparently use a media library available on your system (GStreamer, FFmpeg, MAD, or Core Audio). Using in Your Code ------------------ The simplest way to use pyacoustid to identify audio files is to call the ``match`` function:: >>> import acoustid >>> for score, recording_id, title, artist in acoustid.match(apikey, path): >>> ... This convenience function uses `audioread`_ to decode audio and parses the response for you, pulling out the most important track metadata. It returns in iterable over tuples of relevant information. Everything happens in one fell swoop. There are also a number of "smaller" functions you can use to perform parts of the process: - ``fingerprint(samplerate, channels, pcmiter)``: Generate a fingerprint for raw audio data. Specify the audio parameters and give an iterable containing blocks of PCM data. - ``fingerprint_file(path)``: Using either the Chromaprint dynamic library or the ``fpcalc`` command-line tool, fingerprint an audio file. Returns a pair consisting of the file's duration and its fingerprint. - ``lookup(apikey, fingerprint, duration)``: Make a request to the `Acoustid`_ API to look up the fingerprint returned by the previous function. An API key is required, as is the length, in seconds, of the source audio. Returns a parsed JSON response. - ``parse_lookup_result(data)``: Given a parsed JSON response, return an iterator over tuples containing the match score (a float between 0 and 1), the MusicBrainz recording ID, title, and artist name for each match. The module internally performs thread-safe API rate limiting to 3 queries per second whenever the Web API is called, in accordance with the `Web service documentation`_. If you're running your own Acoustid database server, you can set the base URL for all API calls with the ``set_base_url`` function. Calls to the library can raise ``AcoustidError`` exceptions of two subtypes: ``FingerprintGenerationError`` and ``WebServiceError``. Catch these exceptions if you want to proceed when audio can't be decoded or no match is found on the server. ``NoBackendError``, a subclass of ``FingerprintGenerationError``, is used when the Chromaprint library or fpcalc command-line tool cannot be found. .. _Web service documentation: http://acoustid.org/webservice Version History --------------- 1.1.2 Fix a possible crash on Unicode text in Python 2 in a non-Unicode locale. Look for version "1" of the Chromaprint shared library file. 1.1.1 Fix a possible setup error on Python 3 (thanks to Simon Chopin). 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. Handle a ``UnicodeDecodeError`` raised when using the ``fpcalc`` backend on Windows with Unicode filenames. Standard error output from ``fpcalc`` is suppressed. 0.7 Properly encode Unicode parameters (resolves a ``UnicodeEncodeError`` in fingerprint submission). Parse all recordings for each Acoustid lookup result. 0.6 Add a new function, ``fingerprint_file``, that automatically selects a backend for fingerprinting a single file. 0.5 Fix response parsing when recording has no artists or title. Fix compatibility with Python < 2.7. Add specific ``NoBackendError`` exception. 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. Find the chromaprint library on Cygwin. New module names: ``chromaprint`` and ``acoustid`` (no package). 0.2 Compress HTTP requests and responses. Limit audio decoding to 120 seconds. Return score from convenience function. 0.1 Initial release. Credits ------- This library is by Adrian Sampson. Chromaprint and Acoustid are by `Lukáš Lalinský`__. This package includes the original `ctypes`_-based bindings written by Lukáš. The entire library is made available under the `MIT license`_. pyacoustid was written to be used with `beets`_, which you should probably check out. __ lukas_ .. _ctypes: http://docs.python.org/library/ctypes.html .. _beets: http://beets.radbox.org/ .. _MIT license: http://www.opensource.org/licenses/mit-license.php pyacoustid-1.1.2/fpcalc.py0000755000175000017500000000430712667632345017212 0ustar asampsonasampson00000000000000#!/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() pyacoustid-1.1.2/pyacoustid.egg-info/0000755000175000017500000000000012723350075021245 5ustar asampsonasampson00000000000000pyacoustid-1.1.2/pyacoustid.egg-info/dependency_links.txt0000644000175000017500000000000112723350075025313 0ustar asampsonasampson00000000000000 pyacoustid-1.1.2/pyacoustid.egg-info/requires.txt0000644000175000017500000000002312723350075023640 0ustar asampsonasampson00000000000000audioread requests pyacoustid-1.1.2/pyacoustid.egg-info/top_level.txt0000644000175000017500000000002512723350075023774 0ustar asampsonasampson00000000000000acoustid chromaprint pyacoustid-1.1.2/pyacoustid.egg-info/PKG-INFO0000644000175000017500000002027712723350075022352 0ustar asampsonasampson00000000000000Metadata-Version: 1.1 Name: pyacoustid Version: 1.1.2 Summary: bindings for Chromaprint acoustic fingerprinting and the Acoustid API Home-page: https://github.com/sampsyo/pyacoustid Author: Adrian Sampson Author-email: adrian@radbox.org License: MIT Description: Chromaprint and Acoustid for Python =================================== `Chromaprint`_ and its associated `Acoustid`_ Web service make up a high-quality, open-source acoustic fingerprinting system. This package provides Python bindings for both the fingerprinting algorithm library, which is written in C but portable, and the Web service, which provides fingerprint lookups. .. _Chromaprint: http://acoustid.org/ .. _Acoustid: http://acoustid.org/chromaprint 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 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`_:: $ pip install pyacoustid 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 ------- You can run the included demonstration script, ``aidmatch.py``, to test your installation:: $ python aidmatch.py mysterious_music.mp3 This will show the top metadata match from Acoustid's database. The script uses `audioread`_ to decode music, so it should transparently use a media library available on your system (GStreamer, FFmpeg, MAD, or Core Audio). Using in Your Code ------------------ The simplest way to use pyacoustid to identify audio files is to call the ``match`` function:: >>> import acoustid >>> for score, recording_id, title, artist in acoustid.match(apikey, path): >>> ... This convenience function uses `audioread`_ to decode audio and parses the response for you, pulling out the most important track metadata. It returns in iterable over tuples of relevant information. Everything happens in one fell swoop. There are also a number of "smaller" functions you can use to perform parts of the process: - ``fingerprint(samplerate, channels, pcmiter)``: Generate a fingerprint for raw audio data. Specify the audio parameters and give an iterable containing blocks of PCM data. - ``fingerprint_file(path)``: Using either the Chromaprint dynamic library or the ``fpcalc`` command-line tool, fingerprint an audio file. Returns a pair consisting of the file's duration and its fingerprint. - ``lookup(apikey, fingerprint, duration)``: Make a request to the `Acoustid`_ API to look up the fingerprint returned by the previous function. An API key is required, as is the length, in seconds, of the source audio. Returns a parsed JSON response. - ``parse_lookup_result(data)``: Given a parsed JSON response, return an iterator over tuples containing the match score (a float between 0 and 1), the MusicBrainz recording ID, title, and artist name for each match. The module internally performs thread-safe API rate limiting to 3 queries per second whenever the Web API is called, in accordance with the `Web service documentation`_. If you're running your own Acoustid database server, you can set the base URL for all API calls with the ``set_base_url`` function. Calls to the library can raise ``AcoustidError`` exceptions of two subtypes: ``FingerprintGenerationError`` and ``WebServiceError``. Catch these exceptions if you want to proceed when audio can't be decoded or no match is found on the server. ``NoBackendError``, a subclass of ``FingerprintGenerationError``, is used when the Chromaprint library or fpcalc command-line tool cannot be found. .. _Web service documentation: http://acoustid.org/webservice Version History --------------- 1.1.2 Fix a possible crash on Unicode text in Python 2 in a non-Unicode locale. Look for version "1" of the Chromaprint shared library file. 1.1.1 Fix a possible setup error on Python 3 (thanks to Simon Chopin). 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. Handle a ``UnicodeDecodeError`` raised when using the ``fpcalc`` backend on Windows with Unicode filenames. Standard error output from ``fpcalc`` is suppressed. 0.7 Properly encode Unicode parameters (resolves a ``UnicodeEncodeError`` in fingerprint submission). Parse all recordings for each Acoustid lookup result. 0.6 Add a new function, ``fingerprint_file``, that automatically selects a backend for fingerprinting a single file. 0.5 Fix response parsing when recording has no artists or title. Fix compatibility with Python < 2.7. Add specific ``NoBackendError`` exception. 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. Find the chromaprint library on Cygwin. New module names: ``chromaprint`` and ``acoustid`` (no package). 0.2 Compress HTTP requests and responses. Limit audio decoding to 120 seconds. Return score from convenience function. 0.1 Initial release. Credits ------- This library is by Adrian Sampson. Chromaprint and Acoustid are by `Lukas Lalinsky`__. This package includes the original `ctypes`_-based bindings written by Lukas. The entire library is made available under the `MIT license`_. pyacoustid was written to be used with `beets`_, which you should probably check out. __ lukas_ .. _ctypes: http://docs.python.org/library/ctypes.html .. _beets: http://beets.radbox.org/ .. _MIT license: http://www.opensource.org/licenses/mit-license.php Platform: ALL Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 pyacoustid-1.1.2/pyacoustid.egg-info/SOURCES.txt0000644000175000017500000000037112723350075023132 0ustar asampsonasampson00000000000000MANIFEST.in README.rst acoustid.py aidmatch.py chromaprint.py fpcalc.py setup.py pyacoustid.egg-info/PKG-INFO pyacoustid.egg-info/SOURCES.txt pyacoustid.egg-info/dependency_links.txt pyacoustid.egg-info/requires.txt pyacoustid.egg-info/top_level.txtpyacoustid-1.1.2/PKG-INFO0000644000175000017500000002027712723350075016474 0ustar asampsonasampson00000000000000Metadata-Version: 1.1 Name: pyacoustid Version: 1.1.2 Summary: bindings for Chromaprint acoustic fingerprinting and the Acoustid API Home-page: https://github.com/sampsyo/pyacoustid Author: Adrian Sampson Author-email: adrian@radbox.org License: MIT Description: Chromaprint and Acoustid for Python =================================== `Chromaprint`_ and its associated `Acoustid`_ Web service make up a high-quality, open-source acoustic fingerprinting system. This package provides Python bindings for both the fingerprinting algorithm library, which is written in C but portable, and the Web service, which provides fingerprint lookups. .. _Chromaprint: http://acoustid.org/ .. _Acoustid: http://acoustid.org/chromaprint 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 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`_:: $ pip install pyacoustid 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 ------- You can run the included demonstration script, ``aidmatch.py``, to test your installation:: $ python aidmatch.py mysterious_music.mp3 This will show the top metadata match from Acoustid's database. The script uses `audioread`_ to decode music, so it should transparently use a media library available on your system (GStreamer, FFmpeg, MAD, or Core Audio). Using in Your Code ------------------ The simplest way to use pyacoustid to identify audio files is to call the ``match`` function:: >>> import acoustid >>> for score, recording_id, title, artist in acoustid.match(apikey, path): >>> ... This convenience function uses `audioread`_ to decode audio and parses the response for you, pulling out the most important track metadata. It returns in iterable over tuples of relevant information. Everything happens in one fell swoop. There are also a number of "smaller" functions you can use to perform parts of the process: - ``fingerprint(samplerate, channels, pcmiter)``: Generate a fingerprint for raw audio data. Specify the audio parameters and give an iterable containing blocks of PCM data. - ``fingerprint_file(path)``: Using either the Chromaprint dynamic library or the ``fpcalc`` command-line tool, fingerprint an audio file. Returns a pair consisting of the file's duration and its fingerprint. - ``lookup(apikey, fingerprint, duration)``: Make a request to the `Acoustid`_ API to look up the fingerprint returned by the previous function. An API key is required, as is the length, in seconds, of the source audio. Returns a parsed JSON response. - ``parse_lookup_result(data)``: Given a parsed JSON response, return an iterator over tuples containing the match score (a float between 0 and 1), the MusicBrainz recording ID, title, and artist name for each match. The module internally performs thread-safe API rate limiting to 3 queries per second whenever the Web API is called, in accordance with the `Web service documentation`_. If you're running your own Acoustid database server, you can set the base URL for all API calls with the ``set_base_url`` function. Calls to the library can raise ``AcoustidError`` exceptions of two subtypes: ``FingerprintGenerationError`` and ``WebServiceError``. Catch these exceptions if you want to proceed when audio can't be decoded or no match is found on the server. ``NoBackendError``, a subclass of ``FingerprintGenerationError``, is used when the Chromaprint library or fpcalc command-line tool cannot be found. .. _Web service documentation: http://acoustid.org/webservice Version History --------------- 1.1.2 Fix a possible crash on Unicode text in Python 2 in a non-Unicode locale. Look for version "1" of the Chromaprint shared library file. 1.1.1 Fix a possible setup error on Python 3 (thanks to Simon Chopin). 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. Handle a ``UnicodeDecodeError`` raised when using the ``fpcalc`` backend on Windows with Unicode filenames. Standard error output from ``fpcalc`` is suppressed. 0.7 Properly encode Unicode parameters (resolves a ``UnicodeEncodeError`` in fingerprint submission). Parse all recordings for each Acoustid lookup result. 0.6 Add a new function, ``fingerprint_file``, that automatically selects a backend for fingerprinting a single file. 0.5 Fix response parsing when recording has no artists or title. Fix compatibility with Python < 2.7. Add specific ``NoBackendError`` exception. 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. Find the chromaprint library on Cygwin. New module names: ``chromaprint`` and ``acoustid`` (no package). 0.2 Compress HTTP requests and responses. Limit audio decoding to 120 seconds. Return score from convenience function. 0.1 Initial release. Credits ------- This library is by Adrian Sampson. Chromaprint and Acoustid are by `Lukas Lalinsky`__. This package includes the original `ctypes`_-based bindings written by Lukas. The entire library is made available under the `MIT license`_. pyacoustid was written to be used with `beets`_, which you should probably check out. __ lukas_ .. _ctypes: http://docs.python.org/library/ctypes.html .. _beets: http://beets.radbox.org/ .. _MIT license: http://www.opensource.org/licenses/mit-license.php Platform: ALL Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 pyacoustid-1.1.2/setup.py0000644000175000017500000000350512667634424017116 0ustar asampsonasampson00000000000000# This file is part of pyacoustid. # Copyright 2011, Adrian Sampson. # # 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. import os import sys from setuptools import setup def _read(fn): path = os.path.join(os.path.dirname(__file__), fn) if sys.version_info[0] < 3: data = open(path).read().decode('utf8') else: data = open(path, encoding='utf8').read() # 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') data = data.replace(u'\xfd', u'y') return data setup(name='pyacoustid', version='1.1.2', description= 'bindings for Chromaprint acoustic fingerprinting and the ' 'Acoustid API', author='Adrian Sampson', author_email='adrian@radbox.org', url='https://github.com/sampsyo/pyacoustid', license='MIT', platforms='ALL', long_description=_read('README.rst'), install_requires = ['audioread', 'requests'], py_modules=[ 'chromaprint', 'acoustid', ], classifiers=[ 'Topic :: Multimedia :: Sound/Audio :: Conversion', 'Intended Audience :: Developers', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', ], ) pyacoustid-1.1.2/acoustid.py0000644000175000017500000003046312667632345017574 0ustar asampsonasampson00000000000000# This file is part of pyacoustid. # 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 # "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. from __future__ import division from __future__ import absolute_import import os import json import requests import contextlib import errno 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 io import BytesIO 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' # 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 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 # 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 constant; set the value to zero to disable rate limiting. The limiting is thread-safe; only one thread may be in the function at a time (acts like a monitor in this sense). """ def __init__(self, fun): self.fun = fun self.last_call = 0.0 self.lock = threading.Lock() def __call__(self, *args, **kwargs): with self.lock: # Wait until request_rate time has passed since last_call, # then update last_call. since_last_call = time.time() - self.last_call if since_last_call < REQUEST_INTERVAL: time.sleep(REQUEST_INTERVAL - since_last_call) self.last_call = time.time() # Call the original function. return self.fun(*args, **kwargs) @_rate_limit 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. """ headers = { 'Accept-Encoding': 'gzip', "Content-Type": "application/x-www-form-urlencoded" } 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 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 data as byte strings. Raises a FingerprintGenerationError if anything goes wrong. """ # Maximum number of samples to decode. endposition = samplerate * channels * maxlength try: fper = chromaprint.Fingerprinter() fper.start(samplerate, channels) position = 0 # Samples of audio fed to the fingerprinter. for block in pcmiter: fper.feed(block) position += len(block) // 2 # 2 bytes/sample. if position >= endposition: break return fper.finish() 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. """ params = { 'format': 'json', 'client': apikey, 'duration': int(duration), 'fingerprint': fingerprint, 'meta': 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 the name of the recording's first artist. (If an artist is not available, the last item is None.) If the response is incomplete, raises a WebServiceError. """ if data['status'] != 'ok': raise WebServiceError("status: %s" % data['status']) if 'results' not in data: raise WebServiceError("results not included") for result in data['results']: score = result['score'] if 'recordings' not in result: # No recording attached. This result is not very useful. continue for recording in result['recordings']: # Get the artist if available. if recording.get('artists'): names = [artist['name'] for artist in recording['artists']] artist_name = '; '.join(names) else: artist_name = None yield score, recording['id'], recording.get('title'), artist_name def _fingerprint_file_audioread(path, maxlength): """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), maxlength) except audioread.DecodeError: 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) command = [fpcalc, "-length", str(maxlength), path] try: with open(os.devnull, 'wb') as devnull: proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=devnull) output, _ = proc.communicate() except OSError as exc: if exc.errno == errno.ENOENT: raise NoBackendError("fpcalc not found") else: raise FingerprintGenerationError("fpcalc invocation failed: %s" % str(exc)) except UnicodeEncodeError: # Due to a bug in Python 2's subprocess on Windows, Unicode # filenames can fail to encode on that platform. See: # http://bugs.python.org/issue1759845 raise FingerprintGenerationError("argument encoding failed") retcode = proc.poll() if retcode: raise FingerprintGenerationError("fpcalc exited with status %i" % retcode) duration = fp = None for line in output.splitlines(): try: parts = line.split(b'=', 1) except ValueError: raise FingerprintGenerationError("malformed fpcalc output") if parts[0] == b'DURATION': try: duration = int(parts[1]) except ValueError: raise FingerprintGenerationError("fpcalc duration not numeric") 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 duration and the fingerprint. """ path = os.path.abspath(os.path.expanduser(path)) if have_audioread and have_chromaprint: return _fingerprint_file_audioread(path, maxlength) 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 small tuple of relevant information; otherwise, the full parsed JSON response is returned. """ duration, fp = fingerprint_file(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'])