-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement the basic AcousticBrainz Submit plugin #2342
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
fd3ff91
Implement the basic AcousticBrainz Submit plugin
inytar f61aa7a
Move extractor sha calculation to pluging init.
inytar b861870
Update errors and logging of ABSubmit plugin
inytar 1465167
Update comments for clarification
inytar 90ea842
Add documentation for the absubmit plugin
inytar d0dd0b2
Add absubmit plugins dependencies to the extras_require field
inytar cfe9c0f
Remove fixed TODO comment
inytar 9ab67d5
Fix typos in absubmit documentation
inytar 1a4c74a
More spelling corrections for the absubmit plugin documentation
inytar b57b3f7
Add header to the absubmit plugin
inytar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
# -*- coding: utf-8 -*- | ||
# This file is part of beets. | ||
# Copyright 2016, Pieter Mulder. | ||
# | ||
# 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. | ||
|
||
"""Calculate acoustic information and submit to AcousticBrainz. | ||
""" | ||
|
||
from __future__ import division, absolute_import, print_function | ||
|
||
import hashlib | ||
import json | ||
import os | ||
import subprocess | ||
import tempfile | ||
|
||
import distutils | ||
import requests | ||
|
||
from beets import plugins | ||
from beets import util | ||
from beets import ui | ||
|
||
|
||
class ABSubmitError(Exception): | ||
"""Raised when failing to analyse file with extractor.""" | ||
|
||
|
||
def call(args): | ||
"""Execute the command and return its output. | ||
|
||
Raise a AnalysisABSubmitError on failure. | ||
""" | ||
try: | ||
return util.command_output(args) | ||
except subprocess.CalledProcessError as e: | ||
raise ABSubmitError( | ||
u'{0} exited with status {1}'.format(args[0], e.returncode) | ||
) | ||
|
||
|
||
class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): | ||
|
||
def __init__(self): | ||
super(AcousticBrainzSubmitPlugin, self).__init__() | ||
|
||
self.config.add({'extractor': u''}) | ||
|
||
self.extractor = self.config['extractor'].as_str() | ||
if self.extractor: | ||
self.extractor = util.normpath(self.extractor) | ||
# Expicit path to extractor | ||
if not os.path.isfile(self.extractor): | ||
raise ui.UserError( | ||
u'Extractor command does not exist: {0}.'. | ||
format(self.extractor) | ||
) | ||
else: | ||
# Implicit path to extractor, search for it in path | ||
self.extractor = 'streaming_extractor_music' | ||
try: | ||
call([self.extractor]) | ||
except OSError: | ||
raise ui.UserError( | ||
u'No extractor command found: please install the ' | ||
u'extractor binary from http://acousticbrainz.org/download' | ||
) | ||
except ABSubmitError: | ||
# Extractor found, will exit with an error if not called with | ||
# the correct amount of arguments. | ||
pass | ||
# Get the executable location on the system, | ||
# needed to calculate the sha1 hash. | ||
self.extractor = distutils.spawn.find_executable(self.extractor) | ||
|
||
# Calculate extractor hash. | ||
self.extractor_sha = hashlib.sha1() | ||
with open(self.extractor, 'rb') as extractor: | ||
self.extractor_sha.update(extractor.read()) | ||
self.extractor_sha = self.extractor_sha.hexdigest() | ||
|
||
supported_formats = {'mp3', 'ogg', 'oga', 'flac', 'mp4', 'm4a', 'm4r', | ||
'm4b', 'm4p', 'aac', 'wma', 'asf', 'mpc', 'wv', | ||
'spx', 'tta', '3g2', 'aif', 'aiff', 'ape'} | ||
|
||
base_url = 'https://acousticbrainz.org/api/v1/{mbid}/low-level' | ||
|
||
def commands(self): | ||
cmd = ui.Subcommand( | ||
'absubmit', | ||
help=u'calculate and submit AcousticBrainz analysis' | ||
) | ||
cmd.func = self.command | ||
return [cmd] | ||
|
||
def command(self, lib, opts, args): | ||
# Get items from arguments | ||
items = lib.items(ui.decargs(args)) | ||
for item in items: | ||
analysis = self._get_analysis(item) | ||
if analysis: | ||
self._submit_data(item, analysis) | ||
|
||
def _get_analysis(self, item): | ||
mbid = item['mb_trackid'] | ||
# If file has no mbid skip it. | ||
if not mbid: | ||
self._log.info(u'Not analysing {}, missing ' | ||
u'musicbrainz track id.', item) | ||
return None | ||
# If file format is not supported skip it. | ||
if item['format'].lower() not in self.supported_formats: | ||
self._log.info(u'Not analysing {}, file not in ' | ||
u'supported format.', item) | ||
return None | ||
|
||
# Temporary file to save extractor output to, extractor only works | ||
# if an output file is given. Here we use a temporary file to copy | ||
# the data into a python object and then remove the file from the | ||
# system. | ||
tmp_file, filename = tempfile.mkstemp(suffix='.json') | ||
try: | ||
# Close the file, so the extractor can overwrite it. | ||
try: | ||
call([self.extractor, util.syspath(item.path), filename]) | ||
except ABSubmitError as e: | ||
self._log.error( | ||
u'Failed to analyse {item} for AcousticBrainz: {error}', | ||
item=item, error=e | ||
) | ||
return None | ||
with open(filename) as tmp_file: | ||
analysis = json.loads(tmp_file.read()) | ||
# Add the hash to the output. | ||
analysis['metadata']['version']['essentia_build_sha'] = \ | ||
self.extractor_sha | ||
return analysis | ||
finally: | ||
try: | ||
os.remove(filename) | ||
except OSError as e: | ||
# errno 2 means file does not exist, just ignore this error. | ||
if e.errno != 2: | ||
raise | ||
|
||
def _submit_data(self, item, data): | ||
mbid = item['mb_trackid'] | ||
headers = {'Content-Type': 'application/json'} | ||
response = requests.post(self.base_url.format(mbid=mbid), | ||
json=data, headers=headers) | ||
# Test that request was successful and raise an error on failure. | ||
if response.status_code != 200: | ||
try: | ||
message = response.json()['message'] | ||
except (ValueError, KeyError) as e: | ||
message = u'unable to get error message: {}'.format(e) | ||
self._log.error( | ||
u'Failed to submit AcousticBrainz analysis of {item}: ' | ||
u'{message}).', item=item, message=message | ||
) | ||
else: | ||
self._log.debug(u'Successfully submitted AcousticBrainz analysis ' | ||
u'for {}.', item) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
AcousticBrainz Submit Plugin | ||
============================ | ||
|
||
The `absubmit` plugin uses the `streaming_extractor_music`_ program to analyze an audio file and calculate different acoustic properties of the audio. The plugin then uploads this metadata to the AcousticBrainz server. The plugin does this when calling the ``beet absumbit [QUERY]`` command or on importing if the `auto` configuration option is set to ``yes``. | ||
|
||
Installation | ||
------------ | ||
|
||
The `absubmit` plugin requires the `streaming_extractor_music`_ program to run. Its source can be found on `GitHub`_, and while it is possible to compile the extractor from source, AcousticBrainz would prefer if you used their binary (see the AcousticBrainz `FAQ`_). | ||
|
||
The `absubmit` also plugin requires `requests`_, which you can install using `pip_` by typing: | ||
|
||
pip install requests | ||
|
||
After installing both the extractor binary and requests you can enable the plugin ``absubmit`` in your configuration (see :ref:`using-plugins`). | ||
|
||
Configuration | ||
------------- | ||
|
||
To configure the plugin, make a ``absubmit:`` section in your configuration file. The available options are: | ||
|
||
- **auto**: Analyze every file on import. Otherwise, you need to use the ``beet absubmit`` command explicitly. | ||
Default: ``no`` | ||
- **extractor**: The path to the `streaming_extractor_music`_ binary. | ||
Default: search for the program in your ``$PATH`` | ||
|
||
.. _streaming_extractor_music: http://acousticbrainz.org/download | ||
.. _FAQ: http://acousticbrainz.org/faq | ||
.. _pip: http://www.pip-installer.org/ | ||
.. _requests: http://docs.python-requests.org/en/master/ | ||
.. _github: https://github.com/MTG/essentia |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
options is => option is