-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Refactoring
I've started writing a new config system, based on YAML, called confit.
- Move config and state files into a directory:
~/.config/beets
on Unix,~/Application Support/beets
on Mac OS X, something else on Windows. - Move to YAML for the config file. Probably at the same time, build a nice, declarative system for configuration including the following:
- Declare defaults in a straightforward way (probably using a standalone "default.yaml").
- Declaratively specify command-line options (probably using argparse) that override config file options. Make it easy to add both at once.
- Find a reasonable way to just pass a big config object around instead of messing around with all these Library() parameters and the ImportConfig object.
- It would be great if the config file could be loaded before the command line is fully parsed. This would let plugins'
configure()
methods run with debug mode enabled.
A number of warts have developed on the plugin API.
Stop using a namespace package for plugins. This is causing headaches because pip-installed packages have problems with namespace packages. Flask has moved away from a flaskext package, so it might be wise to use Armin's example there. Plugins should be called beets_X
or, for plugins distributed as part of beets, beets.plugin.X
.
__import__('beets.plug.{}'.format(...)) # built-in
modname = 'beets_{}'.format(...)
import imp
imp.find_module(modname, pluginpaths) # on path
imp.find_module(modname) # installed in Python tree
Use instances instead of classes. It's very confusing that so many aspects of plugins happen at the level of the class itself. It would be much more natural for plugins to be treated as singletons, using self
to store data. Use Trac's trick to force singleton usage -- __init__
called twice returns an existing instance.
class BeetsPlugin(object):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = type.__new__(cls)
return _instance
plugin_classes = []
...
for obj in mod.__dict__.values():
if BeetsPlugin in obj.__mro__:
plugin_classes.append(obj)
plugin_instances = [cls() for cls in plugin_classes]
Now we can abandon decorators for events and the like. Everything should be an ordinary method on the plugin class. And stuff like commands()
should not be a method; it should be a field on the object.
... still not sure about stuff like item_fields
and template_funcs
. Decorators seem alright for them, and assigning to self.* is somewhat cumbersome... all of that would have to go in __init__
?
- Remove or fix mb_artist_id tagging (which currently arbitrarily picks the first credit). A user mentioned that other taggers (Jaikoz and MediaMonkey) fill this field with a null-separated list of IDs, which seems much more useful.
- Shrink the interface size of the metadata backend API by unifying ID lookups with string-based lookups. Currently,
beets.autotag.hooks
has four functions per that abstract DB lookups:_album_for_id
,_track_for_id
,_album_candidates
, and_item_candidates
. It would be nice to collapse this into just two functions, especially as we look at adding new (non-MB) backends via a plugin API. - Is
difflib.ratio()
a Levenshtein distance implementation? If so, replace our hand-rolled one and see if it's faster. - We should make better use of the
logging
module -- for instance, by using the hierarchy of loggers to differentiate between messages from the core vs. from plugins, etc.
- Resolve some strange behavior w.r.t. album-level fields and the update command. When there's an item with an inconsistent item-level field (i.e., two different items in the album have different values for the same album-level field) that is not written by the autotagger, it will get clobbered back to the uniform value even after
beet update
. The effect is that just importing and then immediately runningupdate
shows changes! That's confusing. The importer should probably enforce that all album-level fields are uniform. - Strict typing and coercions for
Item
fields.library.py
should specify the desired Python type of eachItem
field; when setting an attribute, the value should be converted to the given type (e.g., the string'0'
becomesFalse
when assigned). This will, for instance, make themodify
command work correctly for non-string fields (see #323). Additionally, this will put an end to confusing None values, which would be coerced to a "null" value of the appropriate type, solving some weirdness with theupdate
command. Currently, we're being saved by SQLite's weak typing, which does these coercions for us. - Find a way to simplify the user prompts in the importer. They're currently running the risk of becoming "alphabet soup" from the point of view of the user: the increasing number of choices has made it less obvious what should be done at each step.
- Asynchronous import decisions. This is an idea due to martian on IRC. When importing, don't necessarily force a user's decision immediately. Importing is a multi-step process: add to library, look up in MB, make decisions, and copy/write files. This way, a fast process could index everything and then decisions could be made offline. Possibly using an alternate interface, maybe a GUI.
- Experiment with a NoSQL-style database. Two good candidates include sqlitedict and sqlite3dbm (or, eventually, a LeveDB binding).