mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
parent
0f689d344a
commit
650b49795b
6 changed files with 184 additions and 2 deletions
65
beetsplug/keyfinder.py
Normal file
65
beetsplug/keyfinder.py
Normal 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()
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------------
|
||||
|
|
|
|||
25
docs/plugins/keyfinder.rst
Normal file
25
docs/plugins/keyfinder.rst
Normal 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/
|
||||
|
|
@ -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
81
test/test_keyfinder.py
Normal 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')
|
||||
Loading…
Reference in a new issue