mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
Implement the basic AcousticBrainz Submit plugin
This commit is contained in:
parent
8cc2ac5b39
commit
fd3ff917d2
1 changed files with 159 additions and 0 deletions
159
beetsplug/absubmit.py
Normal file
159
beetsplug/absubmit.py
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
"""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):
|
||||||
|
"""Base exception for all excpetions this plugin can raise."""
|
||||||
|
|
||||||
|
|
||||||
|
class FatalABSubmitError(ABSubmitError):
|
||||||
|
"""Raised if the plugin is not able to start."""
|
||||||
|
|
||||||
|
|
||||||
|
class AnalysisABSubmitError(ABSubmitError):
|
||||||
|
"""Raised if analysis of file fails."""
|
||||||
|
|
||||||
|
|
||||||
|
class SubmitABSubmitError(ABSubmitError):
|
||||||
|
"""Raised if submitting data fails."""
|
||||||
|
|
||||||
|
|
||||||
|
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 AnalysisABSubmitError(
|
||||||
|
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 FatalABSubmitError(
|
||||||
|
u'extractor command does not exist: {0}'.
|
||||||
|
format(self.extractor)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Implicit path to extractor, search for it in path
|
||||||
|
# TODO how to check for on Windows?
|
||||||
|
self.extractor = 'streaming_extractor_music'
|
||||||
|
try:
|
||||||
|
call([self.extractor])
|
||||||
|
except OSError:
|
||||||
|
raise FatalABSubmitError(
|
||||||
|
u'no extractor command found: install "{0}"'.
|
||||||
|
format(self.extractor)
|
||||||
|
)
|
||||||
|
except AnalysisABSubmitError:
|
||||||
|
# Extractor found, will exit with an error if not called with
|
||||||
|
# the correct amount of arguments.
|
||||||
|
pass
|
||||||
|
# Get the executable needed to calculate the sha1 hash.
|
||||||
|
self.extractor = distutils.spawn.find_executable(self.extractor)
|
||||||
|
|
||||||
|
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))
|
||||||
|
# Get no_submit option.
|
||||||
|
# TODO get a should submit option from the command line.
|
||||||
|
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('Not analysing {}, missing '
|
||||||
|
'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('Not analysing {}, file not in '
|
||||||
|
'supported format.', item)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Temporary file to save extractor output to.
|
||||||
|
tmp_file, filename = tempfile.mkstemp(suffix='.json')
|
||||||
|
try:
|
||||||
|
# Close the file, so the extractor can overwrite it.
|
||||||
|
call([self.extractor, util.syspath(item.path), filename])
|
||||||
|
with open(filename) as tmp_file:
|
||||||
|
analysis = json.loads(tmp_file.read())
|
||||||
|
# Calculate extractor hash.
|
||||||
|
m = hashlib.sha1()
|
||||||
|
with open(self.extractor, 'rb') as extractor:
|
||||||
|
m.update(extractor.read())
|
||||||
|
# Add the hash to the output.
|
||||||
|
analysis['metadata']['version']['essentia_build_sha'] = \
|
||||||
|
m.hexdigest()
|
||||||
|
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 Exception as e:
|
||||||
|
message = 'unable to get error message: {}'.format(e)
|
||||||
|
raise ABSubmitError(
|
||||||
|
'Failed to submit analysis: {message})'.
|
||||||
|
format(status_code=response.status_code, message=message)
|
||||||
|
)
|
||||||
|
self._log.debug('Successfully submitted AcousticBrainz analysis '
|
||||||
|
'for {}.', item)
|
||||||
Loading…
Reference in a new issue