MetaSync: automatic load of sources and item_types

- MetaSources get loaded from the modules automatically
- The MetaSources can define their own item_types, that get loaded for the plugin
- __init__ doesn't need any changes to accept new metasources
- Fix the --sources option to actually accept sources
  (it was being interpreted as boolean flag before, crashing the plugin)
- More safety w.r.t. external dependencies
This commit is contained in:
Tom Jaspers 2015-05-09 11:31:39 +02:00
parent de5db7068b
commit cb13d21ad6
4 changed files with 116 additions and 54 deletions

View file

@ -14,40 +14,70 @@
"""Synchronize information from music player libraries
"""
from abc import abstractmethod, ABCMeta
from beets import ui
from beets.plugins import BeetsPlugin
from beets.dbcore import types
from beets.library import DateType
from sys import modules
import inspect
import pkgutil
from importlib import import_module
METASYNC_MODULE = 'beetsplug.metasync'
class MetaSource(object):
__metaclass__ = ABCMeta
def __init__(self, config, log):
self.item_types = {}
self.config = config
self._log = log
@abstractmethod
def sync_data(self, item):
raise NotImplementedError()
pass
def load_meta_sources():
""" Returns a dictionary of all the MetaSources
E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true
"""
def is_meta_source_implementation(c):
return inspect.isclass(c) and \
not inspect.isabstract(c) and \
issubclass(c, MetaSource)
meta_sources = {}
module_names = [name for _, name, _ in pkgutil.walk_packages(
import_module(METASYNC_MODULE).__path__)]
for module_name in module_names:
module = import_module(METASYNC_MODULE + '.' + module_name)
classes = inspect.getmembers(module, is_meta_source_implementation)
for cls_name, cls in classes:
meta_sources[cls_name.lower()] = cls
return meta_sources
META_SOURCES = load_meta_sources()
def load_item_types():
""" Returns a dictionary containing the item_types of all the MetaSources
"""
item_types = {}
for meta_source in META_SOURCES.values():
item_types.update(meta_source.item_types)
return item_types
class MetaSyncPlugin(BeetsPlugin):
item_types = {
'amarok_rating': types.INTEGER,
'amarok_score': types.FLOAT,
'amarok_uid': types.STRING,
'amarok_playcount': types.INTEGER,
'amarok_firstplayed': DateType(),
'amarok_lastplayed': DateType(),
'itunes_rating': types.INTEGER, # 0..100 scale
'itunes_playcount': types.INTEGER,
'itunes_skipcount': types.INTEGER,
'itunes_lastplayed': DateType(),
'itunes_lastskipped': DateType(),
}
item_types = load_item_types()
def __init__(self):
super(MetaSyncPlugin, self).__init__()
@ -57,9 +87,9 @@ class MetaSyncPlugin(BeetsPlugin):
help='update metadata from music player libraries')
cmd.parser.add_option('-p', '--pretend', action='store_true',
help='show all changes but do nothing')
cmd.parser.add_option('-s', '--source', action='store_false',
default=self.config['source'].as_str_seq(),
help="select specific sources to import from")
cmd.parser.add_option('-s', '--source', default=[],
action='append', dest='sources',
help='comma-separated list of sources to sync')
cmd.parser.add_format_option()
cmd.func = self.func
return [cmd]
@ -68,32 +98,32 @@ class MetaSyncPlugin(BeetsPlugin):
"""Command handler for the metasync function.
"""
pretend = opts.pretend
source = opts.source
query = ui.decargs(args)
sources = {}
sources = []
for source in opts.sources:
sources.extend(source.split(','))
for player in source:
__import__('beetsplug.metasync', fromlist=[str(player)])
sources = sources or self.config['source'].as_str_seq()
module = 'beetsplug.metasync.' + player
meta_sources = {}
if module not in modules.keys():
# Instantiate the meta sources
for player in sources:
try:
meta_sources[player] = \
META_SOURCES[player](self.config, self._log)
except KeyError:
self._log.error(u'Unknown metadata source \'{0}\''.format(
player))
continue
classes = inspect.getmembers(modules[module], inspect.isclass)
for entry in classes:
if entry[0].lower() == player:
sources[player] = entry[1](self.config, self._log)
else:
continue
except ImportError as e:
self._log.error(u'Failed to instantiate metadata source '
u'\'{0}\': {1}'.format(player, e))
# Sync the items with all of the meta sources
for item in lib.items(query):
for player in sources.values():
player.sync_data(item)
for meta_source in meta_sources.values():
meta_source.sync_data(item)
changed = ui.show_model_changes(item)

View file

@ -18,15 +18,34 @@
from os.path import basename
from datetime import datetime
from time import mktime
from beets.util import displayable_path
from xml.sax.saxutils import escape
from beets.util import displayable_path
from beets.dbcore import types
from beets.library import DateType
from beetsplug.metasync import MetaSource
import dbus
def import_dbus():
try:
return __import__('dbus')
except ImportError:
return None
dbus = import_dbus()
class Amarok(MetaSource):
item_types = {
'amarok_rating': types.INTEGER,
'amarok_score': types.FLOAT,
'amarok_uid': types.STRING,
'amarok_playcount': types.INTEGER,
'amarok_firstplayed': DateType(),
'amarok_lastplayed': DateType(),
}
queryXML = u'<query version="1.0"> \
<filters> \
<and><include field="filename" value="%s" /></and> \
@ -36,6 +55,9 @@ class Amarok(MetaSource):
def __init__(self, config, log):
super(Amarok, self).__init__(config, log)
if not dbus:
raise ImportError('failed to import dbus')
self.collection = \
dbus.SessionBus().get_object('org.kde.amarok', '/Collection')

View file

@ -18,10 +18,12 @@ from contextlib import contextmanager
import os
import shutil
import tempfile
import plistlib
from time import mktime
import plistlib
from beets import util
from beets.dbcore import types
from beets.library import DateType
from beets.util.confit import ConfigValueError
from beetsplug.metasync import MetaSource
@ -37,10 +39,18 @@ def create_temporary_copy(path):
shutil.rmtree(temp_dir)
class ITunes(MetaSource):
class Itunes(MetaSource):
item_types = {
'itunes_rating': types.INTEGER, # 0..100 scale
'itunes_playcount': types.INTEGER,
'itunes_skipcount': types.INTEGER,
'itunes_lastplayed': DateType(),
'itunes_lastskipped': DateType(),
}
def __init__(self, config, log):
super(ITunes, self).__init__(config, log)
super(Itunes, self).__init__(config, log)
# Load the iTunes library, which has to be the .xml one (not the .itl)
library_path = util.normpath(config['itunes']['library'].get(str))
@ -51,15 +61,15 @@ class ITunes(MetaSource):
with create_temporary_copy(library_path) as library_copy:
raw_library = plistlib.readPlist(library_copy)
except IOError as e:
raise ConfigValueError(u"invalid iTunes library: " + e.strerror)
except Exception as e:
raise ConfigValueError(u'invalid iTunes library: ' + e.strerror)
except Exception:
# It's likely the user configured their '.itl' library (<> xml)
if os.path.splitext(library_path)[1].lower() != '.xml':
hint = u": please ensure that the configured path" \
u" points to the .XML library"
hint = u': please ensure that the configured path' \
u' points to the .XML library'
else:
hint = ''
raise ConfigValueError(u"invalid iTunes library" + hint)
raise ConfigValueError(u'invalid iTunes library' + hint)
# Convert the library in to something we can query more easily
self.collection = {
@ -71,7 +81,7 @@ class ITunes(MetaSource):
result = self.collection.get(key)
if not all(key) or not result:
self._log.warning(u"no iTunes match found for {0}".format(item))
self._log.warning(u'no iTunes match found for {0}'.format(item))
return
item.itunes_rating = result.get('Rating')

View file

@ -31,8 +31,8 @@ Configuration
To configure the plugin, make a ``metasync:`` section in your configuration
file. The available options are:
- **source**: A list of sources to fetch metadata from. Set this to "amarok" or
"itunes" to enable synchronization with that player.
- **source**: A list of comma-separated sources to fetch metadata from.
Set this to "amarok" or "itunes" to enable synchronization with that player.
Default: empty
The follow subsections describe additional configure required for some players.
@ -59,5 +59,5 @@ The command has a few command-line options:
* To preview the changes that would be made without applying them, use the
``-p`` (``--pretend``) flag.
* To specify a temporary source to fetch metadata from, use the ``-s``
(``--source``) flag.
* To specify temporary sources to fetch metadata from, use the ``-s``
(``--source``) flag with a comma-separated list of a sources.