From cb13d21ad69cfe957aacb27722a5591bea4de20f Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Sat, 9 May 2015 11:31:39 +0200 Subject: [PATCH] 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 --- beetsplug/metasync/__init__.py | 108 +++++++++++++++++++++------------ beetsplug/metasync/amarok.py | 26 +++++++- beetsplug/metasync/itunes.py | 28 ++++++--- docs/plugins/metasync.rst | 8 +-- 4 files changed, 116 insertions(+), 54 deletions(-) diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 67086481c..22e96edca 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -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) diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py index acfcca6a7..b63b322ec 100644 --- a/beetsplug/metasync/amarok.py +++ b/beetsplug/metasync/amarok.py @@ -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' \ \ \ @@ -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') diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py index 9ddf03776..7ae1e46ef 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -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') diff --git a/docs/plugins/metasync.rst b/docs/plugins/metasync.rst index 4f6fa2a39..6703d3c19 100644 --- a/docs/plugins/metasync.rst +++ b/docs/plugins/metasync.rst @@ -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.