diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py new file mode 100644 index 000000000..743bc8277 --- /dev/null +++ b/beetsplug/metasync/__init__.py @@ -0,0 +1,88 @@ +# This file is part of beets. +# Copyright 2015, Heinz Wiesinger. +# +# 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. + +"""Synchronize information from music player libraries +""" + +from beets import ui, logging +from beets.plugins import BeetsPlugin +from beets.dbcore import types +from beets.library import DateType +from sys import modules +import inspect + +# Loggers. +log = logging.getLogger('beets.metasync') + + +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() + } + + def __init__(self): + super(MetaSyncPlugin, self).__init__() + + def commands(self): + cmd = ui.Subcommand('metasync', + 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_format_option() + cmd.func = self.func + return [cmd] + + def func(self, lib, opts, args): + """Command handler for the metasync function. + """ + pretend = opts.pretend + source = opts.source + query = ui.decargs(args) + + sources = {} + + for player in source: + __import__('beetsplug.metasync', fromlist=[str(player)]) + + module = 'beetsplug.metasync.' + player + + if module not in modules.keys(): + log.error(u'Unknown metadata source \'' + player + '\'') + continue + + classes = inspect.getmembers(modules[module], inspect.isclass) + + for entry in classes: + if entry[0].lower() == player: + sources[player] = entry[1]() + else: + continue + + for item in lib.items(query): + for player in sources.values(): + player.get_data(item) + + changed = ui.show_model_changes(item) + + if changed and not pretend: + item.store() diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py new file mode 100644 index 000000000..3ffeaeac9 --- /dev/null +++ b/beetsplug/metasync/amarok.py @@ -0,0 +1,81 @@ +# This file is part of beets. +# Copyright 2015, Heinz Wiesinger. +# +# 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. + +"""Synchronize information from amarok's library via dbus +""" + +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 +import dbus + + +class Amarok(object): + + queryXML = u' \ + \ + \ + \ + ' + + def __init__(self): + self.collection = \ + dbus.SessionBus().get_object('org.kde.amarok', '/Collection') + + def get_data(self, item): + path = displayable_path(item.path) + + # amarok unfortunately doesn't allow searching for the full path, only + # for the patch relative to the mount point. But the full path is part + # of the result set. So query for the filename and then try to match + # the correct item from the results we get back + results = self.collection.Query(self.queryXML % escape(basename(path))) + for result in results: + if result['xesam:url'] != path: + continue + + item.amarok_rating = result['xesam:userRating'] + item.amarok_score = result['xesam:autoRating'] + item.amarok_playcount = result['xesam:useCount'] + item.amarok_uid = \ + result['xesam:id'].replace('amarok-sqltrackuid://', '') + + if result['xesam:firstUsed'][0][0] != 0: + # These dates are stored as timestamps in amarok's db, but + # exposed over dbus as fixed integers in the current timezone. + first_played = datetime( + result['xesam:firstUsed'][0][0], + result['xesam:firstUsed'][0][1], + result['xesam:firstUsed'][0][2], + result['xesam:firstUsed'][1][0], + result['xesam:firstUsed'][1][1], + result['xesam:firstUsed'][1][2] + ) + + if result['xesam:lastUsed'][0][0] != 0: + last_played = datetime( + result['xesam:lastUsed'][0][0], + result['xesam:lastUsed'][0][1], + result['xesam:lastUsed'][0][2], + result['xesam:lastUsed'][1][0], + result['xesam:lastUsed'][1][1], + result['xesam:lastUsed'][1][2] + ) + else: + last_played = first_played + + item.amarok_firstplayed = mktime(first_played.timetuple()) + item.amarok_lastplayed = mktime(last_played.timetuple()) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index e301d3a7c..4c801645e 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -56,6 +56,7 @@ Each plugin has its own set of options that can be defined in a section bearing lyrics mbcollection mbsync + metasync missing mpdstats mpdupdate @@ -104,6 +105,7 @@ Metadata * :doc:`lastimport`: Collect play counts from Last.fm. * :doc:`lyrics`: Automatically fetch song lyrics. * :doc:`mbsync`: Fetch updated metadata from MusicBrainz +* :doc:`metasync`: Fetch metadata from local or remote sources * :doc:`mpdstats`: Connect to `MPD`_ and update the beets library with play statistics (last_played, play_count, skip_count, rating). * :doc:`replaygain`: Calculate volume normalization for players that support it. diff --git a/docs/plugins/metasync.rst b/docs/plugins/metasync.rst new file mode 100644 index 000000000..6698e086e --- /dev/null +++ b/docs/plugins/metasync.rst @@ -0,0 +1,45 @@ +MetaSync Plugin +=============== + +This plugin provides the ``metasync`` command, which lets you fetch certain +metadata from other local or remote sources, for example your favorite audio +player. + +Currently we support the following list of metadata sources: +- **amarok**: This syncs rating, score, first played, last played, playcount and uid from amarok. + + +Installing Dependencies +----------------------- + +Fetching metadata from amarok requires the dbus-python library. + +There are packages for most major linux distributions, or you can download the +library from its _website. + + _website: http://dbus.freedesktop.org/releases/dbus-python/ + + +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. + Default: empty + + +Usage +----- + +Enable the ``metasync`` plugin in your configuration (see +:ref:`using-plugins`) then run ``beet metasync QUERY`` to fetch updated +metadata from the configured list of sources. + +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. diff --git a/setup.py b/setup.py index 8071d4d09..51a83c881 100755 --- a/setup.py +++ b/setup.py @@ -76,6 +76,7 @@ setup( 'beetsplug.bpd', 'beetsplug.web', 'beetsplug.lastgenre', + 'beetsplug.metasync', ], entry_points={ 'console_scripts': [ @@ -117,6 +118,7 @@ setup( 'web': ['flask', 'flask-cors'], 'import': ['rarfile'], 'thumbnails': ['pathlib', 'pyxdg'], + 'metasync': ['dbus-python'], }, # Non-Python/non-PyPI plugin dependencies: # replaygain: mp3gain || aacgain