From 5e43b07128c84939c1354951a6e8cf5598cc0748 Mon Sep 17 00:00:00 2001 From: Heinz Wiesinger Date: Sat, 28 Mar 2015 22:00:03 +0100 Subject: [PATCH] Initial version of a plugin that syncs metadata from other applications. --- beetsplug/psync/__init__.py | 74 +++++++++++++++++++++++++++++++++++++ beetsplug/psync/amarok.py | 73 ++++++++++++++++++++++++++++++++++++ setup.py | 2 + 3 files changed, 149 insertions(+) create mode 100644 beetsplug/psync/__init__.py create mode 100644 beetsplug/psync/amarok.py diff --git a/beetsplug/psync/__init__.py b/beetsplug/psync/__init__.py new file mode 100644 index 000000000..1d752f134 --- /dev/null +++ b/beetsplug/psync/__init__.py @@ -0,0 +1,74 @@ +# 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 +from beets.plugins import BeetsPlugin +from beets.dbcore import types +from beets.library import DateType + + +class PSyncPlugin(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(PSyncPlugin, self).__init__() + + def commands(self): + cmd = ui.Subcommand('psync', + 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 psync function. + """ + pretend = opts.pretend + source = opts.source + query = ui.decargs(args) + + sources = {} + + for player in source: + if player == u'amarok': + from beetsplug.psync import amarok + + sources[u'amarok'] = amarok.Amarok() + 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/psync/amarok.py b/beetsplug/psync/amarok.py new file mode 100644 index 000000000..30b6e1b6f --- /dev/null +++ b/beetsplug/psync/amarok.py @@ -0,0 +1,73 @@ +# 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 +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): + # 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 % basename(item.path)) + for result in results: + if result['xesam:url'] != item.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://', '') + + # 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] + ) + + 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] + ) + + item.amarok_firstplayed = mktime(first_played.timetuple()) + item.amarok_lastplayed = mktime(last_played.timetuple()) diff --git a/setup.py b/setup.py index 8071d4d09..bc0bc6057 100755 --- a/setup.py +++ b/setup.py @@ -76,6 +76,7 @@ setup( 'beetsplug.bpd', 'beetsplug.web', 'beetsplug.lastgenre', + 'beetsplug.psync', ], entry_points={ 'console_scripts': [ @@ -117,6 +118,7 @@ setup( 'web': ['flask', 'flask-cors'], 'import': ['rarfile'], 'thumbnails': ['pathlib', 'pyxdg'], + 'psync': ['dbus-python'], }, # Non-Python/non-PyPI plugin dependencies: # replaygain: mp3gain || aacgain