diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index bf765001c..d20f6ff3c 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -17,6 +17,8 @@ autotagger. Requires the pyacoustid library. """ from __future__ import with_statement from beets import plugins +from beets import ui +from beets import util from beets.autotag import hooks import acoustid import logging @@ -42,6 +44,9 @@ _matches = {} _fingerprints = {} _acoustids = {} +# The user's Acoustid API key, if provided. +_userkey = None + def acoustid_match(path): """Gets metadata for a file from Acoustid and populates the @@ -87,6 +92,9 @@ def acoustid_match(path): log.debug('chroma: matched recording {}'.format(recording_id)) _matches[path] = recording_id, release_ids + +# Plugin structure and autotagging logic. + def _all_releases(items): """Given an iterable of Items, determines (according to Acoustid) which releases the items have in common. Generates release IDs. @@ -141,6 +149,23 @@ class AcoustidPlugin(plugins.BeetsPlugin): log.debug('no acoustid item candidate found') return [] + def configure(self, config): + global _userkey + _userkey = ui.config_val(config, 'acoustid', 'apikey', None) + + def commands(self): + submit_cmd = ui.Subcommand('submit', + help='submit Acoustid fingerprints') + def submit_cmd_func(lib, config, opts, args): + if not _userkey: + raise ui.UserError('no Acoustid user API key provided') + submit_items(_userkey, lib.items(ui.decargs(args))) + submit_cmd.func = submit_cmd_func + return [submit_cmd] + + +# Hooks into import process. + @AcoustidPlugin.listen('import_task_start') def fingerprint_task(config=None, task=None): """Fingerprint each item in the task for later use during the @@ -158,3 +183,66 @@ def apply_acoustid_metadata(config=None, task=None): item.acoustid_fingerprint = _fingerprints[item.path] if item.path in _acoustids: item.acoustid_id = _acoustids[item.path] + + +# UI commands. + +def submit_items(userkey, items, chunksize=64): + """Submit fingerprints for the items to the Acoustid server. + """ + data = [] # The running list of dictionaries to submit. + def submit_chunk(): + """Submit the current accumulated fingerprint data.""" + log.info('submitting {} fingerprints'.format(len(data))) + acoustid.submit(API_KEY, userkey, data) + del data[:] + + for item in items: + # Get a fingerprint and length for this track. + if not item.length: + log.info(u'{}: no duration available'.format( + util.displayable_path(item.path) + )) + continue + elif item.acoustid_fingerprint: + log.info(u'{}: using existing fingerprint'.format( + util.displayable_path(item.path) + )) + fp = item.acoustid_fingerprint + else: + log.info(u'{}: fingerprinting'.format( + util.displayable_path(item.path) + )) + try: + _, fp = acoustid.fingerprint_file(item.path) + except acoustid.FingerprintGenerationError, exc: + log.info('fingerprint generation failed') + continue + + # Construct a submission dictionary for this item. + item_data = { + 'duration': int(item.length), + 'fingerprint': fp, + } + if item.mb_trackid: + item_data['mbid'] = item.mb_trackid + log.debug('submitting MBID') + else: + item_data.update({ + 'track': item.title, + 'artist': item.artist, + 'album': item.album, + 'albumartist': item.albumartist, + 'year': item.year, + 'trackno': item.track, + 'discno': item.disc, + }) + log.debug('submitting textual metadata') + data.append(item_data) + + # If we have enough data, submit a chunk. + if len(data) > chunksize: + submit_chunk() + + # Submit remaining data in a final chunk. + submit_chunk() diff --git a/docs/changelog.rst b/docs/changelog.rst index b505cfca2..22bf64bb6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,9 +14,12 @@ Changelog fields, ``artist_sort`` and ``albumartist_sort``, that contain sortable artist names like "Beatles, The". These fields are also used to sort albums and items when using the ``list`` command. Thanks to Paul Provost. -* :doc:`/plugins/chroma`: The Chromaprint fingerprint and Acoustid ID are now - stored for all fingerprinted tracks. This version of beets *requires* at least - version 0.6 of `pyacoustid`_ for fingerprinting to work. +* :doc:`/plugins/chroma`: A new command, ``beet submit``, will **submit + fingerprints** to the Acoustid database. Submitting your library helps + increase the coverage and accuracy of Acoustid fingerprinting. The Chromaprint + fingerprint and Acoustid ID are also now stored for all fingerprinted tracks. + This version of beets *requires* at least version 0.6 of `pyacoustid`_ for + fingerprinting to work. * New :doc:`/plugins/rdm`: Randomly select albums and tracks from your library. Thanks to Philippe Mongeau. * The :doc:`/plugins/mbcollection` by Jeffrey Aylesworth was added to the core diff --git a/docs/plugins/chroma.rst b/docs/plugins/chroma.rst index 347a5963f..9733b4160 100644 --- a/docs/plugins/chroma.rst +++ b/docs/plugins/chroma.rst @@ -75,3 +75,24 @@ line. Your config file should contain something like this:: plugins: chroma With that, beets will use fingerprinting the next time you run ``beet import``. + + +.. _submitfp: + +Submitting Fingerprints +''''''''''''''''''''''' + +You can help expand the `Acoustid`_ database by submitting fingerprints for the +music in your collection. To do this, first `get an API key`_ from the Acoustid +service. Just use an OpenID or MusicBrainz account to log in and you'll get a +short token string. Then, add the key to your :doc:`/reference/config` as the +value ``apikey`` in a section called ``acoustid`` like so:: + + [acoustid] + apikey=AbCd1234 + +Then, run ``beet submit``. (You can also provide a query to submit a subset of +your library.) The command will use stored fingerprints if they're available; +otherwise it will fingerprint each file before submitting it. + +.. _get an API key: http://acoustid.org/api-key