Add keyfinder plugin

Closes #662 and #279.
This commit is contained in:
Thomas Scholtes 2014-04-12 18:44:38 +02:00
parent 0f689d344a
commit 650b49795b
6 changed files with 184 additions and 2 deletions

65
beetsplug/keyfinder.py Normal file
View file

@ -0,0 +1,65 @@
# This file is part of beets.
# Copyright 2014, Thomas Scholtes.
#
# 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.
"""Uses the `KeyFinder` program to add the `initial_key` field.
"""
import logging
from beets import ui
from beets import util
from beets.plugins import BeetsPlugin
log = logging.getLogger('beets')
class KeyFinderPlugin(BeetsPlugin):
def __init__(self):
super(KeyFinderPlugin, self).__init__()
self.config.add({
u'bin': u'KeyFinder',
u'auto': True,
u'overwrite': False,
})
self.config['auto'].get(bool)
self.import_stages = [self.imported]
def commands(self):
cmd = ui.Subcommand('keyfinder',
help='detect and add initial key from audio')
cmd.func = self.command
return [cmd]
def command(self, lib, opts, args):
self.find_key(lib.items(ui.decargs(args)))
def imported(self, session, task):
if self.config['auto'].get(bool):
self.find_key(task.items)
def find_key(self, items):
overwrite = self.config['overwrite'].get(bool)
bin = util.bytestring_path(self.config['bin'].get(unicode))
for item in items:
if item['initial_key'] and not overwrite:
continue
key = util.command_output([bin, '-f', item.path])
item['initial_key'] = key
log.debug('added computed initial key {0} for {1}'
.format(key, util.displayable_path(item.path)))
item.try_write()
item.store()

View file

@ -18,6 +18,9 @@ New stuff:
initial_key=Am`` the media files will reflect this in their tags. The
:doc:`/plugins/echonest` plugin also sets this field if the data is
available.
* There is also a new :doc:`/plugins/keyfinder` that runs a command line
program to get the key from audio data and store it in the
`initial_key` field.
Fixes:

View file

@ -56,6 +56,7 @@ by typing ``beet version``.
beatport
fromfilename
ftintitle
keyfinder
Autotagger Extensions
---------------------
@ -86,9 +87,12 @@ Metadata
field.
* :doc:`mpdstats`: Connect to `MPD`_ and update the beets library with play
statistics (last_played, play_count, skip_count, rating).
* :doc:`keyfinder`: Use the `KeyFinder`_ program to detect the musical
key from the audio.
.. _Acoustic Attributes: http://developer.echonest.com/acoustic-attributes.html
.. _the Echo Nest: http://www.echonest.com
.. _KeyFinder: http://www.ibrahimshaath.co.uk/keyfinder/
Path Formats
------------

View file

@ -0,0 +1,25 @@
Key Finder Plugin
=================
The `keyfinder` plugin uses the `KeyFinder`_ program to detect the
musical key of track from its audio data and store it in the
`initial_key` field of you database. If enabled, it does so
automatically when importing music or through the ``beet keyfinder
[QUERY]`` command.
There are a couple of configuration options to customize the behavior of
the plugin. By default they are::
keyfinder:
bin: KeyFinder
auto: yes
overwrite: no
* ``bin`` This is the name of the `KeyFinder` program on your system or
a path to the binary.
* ``auto`` If set to `yes`, the plugin will analyze every file on
import.
* ``overwrite`` If set to `no` the import hook and the command will skip
any file that already has an `initial_key` in the database.
.. _KeyFinder: http://www.ibrahimshaath.co.uk/keyfinder/

View file

@ -154,8 +154,12 @@ class TestHelper(object):
import_dir = os.path.join(self.temp_dir, 'import')
if not os.path.isdir(import_dir):
os.mkdir(import_dir)
for fixture in glob(os.path.join(_common.RSRC, '*.mp3'))[0:file_count]:
shutil.copy(fixture, import_dir)
for i in range(file_count):
title = 'track {0}'.format(i)
src = os.path.join(_common.RSRC, 'full.mp3')
dest = os.path.join(import_dir, '{0}.mp3'.format(title))
shutil.copy(src, dest)
config['import']['quiet'] = True
config['import']['autotag'] = False

81
test/test_keyfinder.py Normal file
View file

@ -0,0 +1,81 @@
# This file is part of beets.
# Copyright 2014, Thomas Scholtes.
#
# 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 mock import patch
from _common import unittest
from helper import TestHelper
from beets.library import Item
class KeyFinderTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.load_plugins('keyfinder')
self.patcher = patch('beets.util.command_output')
self.command_output = self.patcher.start()
def tearDown(self):
self.teardown_beets()
self.unload_plugins()
self.patcher.stop()
def test_add_key(self):
item = Item(path='/file')
item.add(self.lib)
self.command_output.return_value = 'dbm'
self.run_command('keyfinder')
item.load()
self.assertEqual(item['initial_key'], 'C#m')
self.command_output.assert_called_with(
['KeyFinder', '-f', item.path])
def test_add_key_on_import(self):
self.command_output.return_value = 'dbm'
importer = self.create_importer()
importer.run()
item = self.lib.items().get()
self.assertEqual(item['initial_key'], 'C#m')
def test_force_overwrite(self):
self.config['keyfinder']['overwrite'] = True
item = Item(path='/file', initial_key='F')
item.add(self.lib)
self.command_output.return_value = 'C#m'
self.run_command('keyfinder')
item.load()
self.assertEqual(item['initial_key'], 'C#m')
def test_do_not_overwrite(self):
item = Item(path='/file', initial_key='F')
item.add(self.lib)
self.command_output.return_value = 'dbm'
self.run_command('keyfinder')
item.load()
self.assertEqual(item['initial_key'], 'F')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')