mirror of
https://github.com/beetbox/beets.git
synced 2025-12-15 04:55:10 +01:00
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:
parent
de5db7068b
commit
cb13d21ad6
4 changed files with 116 additions and 54 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue