From 3dc2beda4104b220efefed37d745c78e0c58853c Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Wed, 6 May 2015 22:01:00 +0200 Subject: [PATCH 01/11] MetaSync: add iTunes synchronization Uses plistlib to read a temp copy of `iTunes Library.xml` --- beetsplug/metasync/__init__.py | 10 ++++- beetsplug/metasync/amarok.py | 2 +- beetsplug/metasync/itunes.py | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 beetsplug/metasync/itunes.py diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 743bc82776..b02f43c283 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -34,7 +34,13 @@ class MetaSyncPlugin(BeetsPlugin): 'amarok_uid': types.STRING, 'amarok_playcount': types.INTEGER, 'amarok_firstplayed': DateType(), - 'amarok_lastplayed': 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(), } def __init__(self): @@ -74,7 +80,7 @@ def func(self, lib, opts, args): for entry in classes: if entry[0].lower() == player: - sources[player] = entry[1]() + sources[player] = entry[1](self.config) else: continue diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py index 3ffeaeac96..f3ecb5ffbd 100644 --- a/beetsplug/metasync/amarok.py +++ b/beetsplug/metasync/amarok.py @@ -31,7 +31,7 @@ class Amarok(object): \ ' - def __init__(self): + def __init__(self, config=None): self.collection = \ dbus.SessionBus().get_object('org.kde.amarok', '/Collection') diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py new file mode 100644 index 0000000000..9c3b670f7a --- /dev/null +++ b/beetsplug/metasync/itunes.py @@ -0,0 +1,80 @@ +# This file is part of beets. +# Copyright 2015, Tom Jaspers. +# +# 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 iTunes's library +""" +from contextlib import contextmanager +import os +import shutil +import tempfile +from time import mktime + +import plistlib +from beets import util +from beets.util.confit import ConfigValueError + + +@contextmanager +def create_temporary_copy(path): + temp_dir = tempfile.gettempdir() + temp_path = os.path.join(temp_dir, 'temp_itunes_lib') + shutil.copyfile(path, temp_path) + try: + yield temp_path + finally: + shutil.rmtree(temp_dir) + + +class ITunes(object): + + def __init__(self, config=None): + # Load the iTunes library, which has to be the .xml one + library_path = util.normpath(config['itunes']['library'].get(str)) + + try: + with create_temporary_copy(library_path) as library_copy: + raw_library = plistlib.readPlist(library_copy) + except IOError as e: + raise ConfigValueError("invalid iTunes library: " + e.strerror) + except: + # TODO: Tell user to make sure it is the .xml one? + raise ConfigValueError("invalid iTunes library") + + # Convert the library in to something we can query more easily + self.collection = { + (track['Name'], track['Album'], track['Album Artist']): track + for track in raw_library['Tracks'].values()} + + def get_data(self, item): + key = (item.title, item.album, item.albumartist) + result = self.collection.get(key) + + # TODO: Need to investigate behavior for items without title, album, or + # albumartist before allowing them to be queried + if not all(key) or not result: + # TODO: Need to log something here later + # print "No iTunes match found for {0}".format(item) + return + + item.itunes_rating = result.get('Rating') + item.itunes_playcount = result.get('Play Count') + item.itunes_skipcount = result.get('Skip Count') + + if result.get('Play Date UTC'): + item.itunes_lastplayed = mktime( + result.get('Play Date UTC').timetuple()) + + if result.get('Skip Date'): + item.itunes_lastskipped = mktime( + result.get('Skip Date').timetuple()) From 04d7c883d1da8ea96684dc57d728a883a982cfb2 Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Thu, 7 May 2015 10:28:41 +0200 Subject: [PATCH 02/11] MetaSync: docs for itunes synchronization changelog & metasync plugin documentation --- docs/changelog.rst | 6 ++++++ docs/plugins/metasync.rst | 21 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 62189fa379..9b49bdd5fa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,12 @@ Changelog 1.3.14 (in development) ----------------------- +New features: + +* The :doc:`/plugins/metasync` plugin now lets you get metadata from iTunes. + This plugin is still in an experimental phase. + + Fixes: * :doc:`/plugins/mpdstats`: Avoid a crash when the music played is not in the diff --git a/docs/plugins/metasync.rst b/docs/plugins/metasync.rst index cd157eacd1..e7d26ccd9f 100644 --- a/docs/plugins/metasync.rst +++ b/docs/plugins/metasync.rst @@ -4,11 +4,13 @@ MetaSync Plugin This plugin provides the ``metasync`` command, which lets you fetch certain metadata from other sources: for example, your favorite audio player. -Currently, the plugin supports synchronizing with the `Amarok`_ music player. +Currently, the plugin supports synchronizing with the `Amarok`_ music player, +and with `iTunes`_. It can fetch the rating, score, first-played date, last-played date, play count, and track uid from Amarok. .. _Amarok: https://amarok.kde.org/ +.. _iTunes: https://www.apple.com/itunes/ Installation @@ -29,10 +31,23 @@ 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" - to enable synchronization with that player. +- **source**: A list of 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. + +itunes +'''''' + +The path to your iTunes library **xml** file has to be configured, e.g.:: + + metaysnc: + source: itunes + itunes: + library: ~/Music/iTunes Library.xml + +Please note the indentation. Usage ----- From 27aef76ae5667c5d4a01db35da2c945a4c2a3645 Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Fri, 8 May 2015 16:43:02 +0200 Subject: [PATCH 03/11] MetaSync: update changelog & fix typo in doc --- docs/changelog.rst | 2 +- docs/plugins/metasync.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9b49bdd5fa..a2ad4759e5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,7 +7,7 @@ Changelog New features: * The :doc:`/plugins/metasync` plugin now lets you get metadata from iTunes. - This plugin is still in an experimental phase. + This plugin is still in an experimental phase. :bug:`1450` Fixes: diff --git a/docs/plugins/metasync.rst b/docs/plugins/metasync.rst index e7d26ccd9f..4f6fa2a39a 100644 --- a/docs/plugins/metasync.rst +++ b/docs/plugins/metasync.rst @@ -42,7 +42,7 @@ itunes The path to your iTunes library **xml** file has to be configured, e.g.:: - metaysnc: + metasync: source: itunes itunes: library: ~/Music/iTunes Library.xml From de5db7068bb72675b56a436a076c93789d342470 Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Fri, 8 May 2015 16:48:38 +0200 Subject: [PATCH 04/11] MetaSync: sources need to subclass MetaSource - allows for the logging to integrate better - renamed `get_data` to `sync_data`, to better reflect that its not returning anything, but that the item's data is being set by the function --- beetsplug/metasync/__init__.py | 19 +++++++++++++------ beetsplug/metasync/amarok.py | 10 +++++++--- beetsplug/metasync/itunes.py | 31 +++++++++++++++++++------------ 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index b02f43c283..67086481c4 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -15,15 +15,21 @@ """Synchronize information from music player libraries """ -from beets import ui, logging +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 -# Loggers. -log = logging.getLogger('beets.metasync') + +class MetaSource(object): + def __init__(self, config, log): + self.config = config + self._log = log + + def sync_data(self, item): + raise NotImplementedError() class MetaSyncPlugin(BeetsPlugin): @@ -73,20 +79,21 @@ def func(self, lib, opts, args): module = 'beetsplug.metasync.' + player if module not in modules.keys(): - log.error(u'Unknown metadata source \'' + player + '\'') + 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) + sources[player] = entry[1](self.config, self._log) else: continue for item in lib.items(query): for player in sources.values(): - player.get_data(item) + player.sync_data(item) changed = ui.show_model_changes(item) diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py index f3ecb5ffbd..acfcca6a7c 100644 --- a/beetsplug/metasync/amarok.py +++ b/beetsplug/metasync/amarok.py @@ -20,10 +20,12 @@ from time import mktime from beets.util import displayable_path from xml.sax.saxutils import escape +from beetsplug.metasync import MetaSource + import dbus -class Amarok(object): +class Amarok(MetaSource): queryXML = u' \ \ @@ -31,11 +33,13 @@ class Amarok(object): \ ' - def __init__(self, config=None): + def __init__(self, config, log): + super(Amarok, self).__init__(config, log) + self.collection = \ dbus.SessionBus().get_object('org.kde.amarok', '/Collection') - def get_data(self, item): + def sync_data(self, item): path = displayable_path(item.path) # amarok unfortunately doesn't allow searching for the full path, only diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py index 9c3b670f7a..9ddf03776d 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -23,6 +23,7 @@ import plistlib from beets import util from beets.util.confit import ConfigValueError +from beetsplug.metasync import MetaSource @contextmanager @@ -36,35 +37,41 @@ def create_temporary_copy(path): shutil.rmtree(temp_dir) -class ITunes(object): +class ITunes(MetaSource): - def __init__(self, config=None): - # Load the iTunes library, which has to be the .xml one + def __init__(self, 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)) try: + self._log.debug( + u'loading iTunes library from {0}'.format(library_path)) with create_temporary_copy(library_path) as library_copy: raw_library = plistlib.readPlist(library_copy) except IOError as e: - raise ConfigValueError("invalid iTunes library: " + e.strerror) - except: - # TODO: Tell user to make sure it is the .xml one? - raise ConfigValueError("invalid iTunes library") + raise ConfigValueError(u"invalid iTunes library: " + e.strerror) + except Exception as e: + # 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" + else: + hint = '' + raise ConfigValueError(u"invalid iTunes library" + hint) # Convert the library in to something we can query more easily self.collection = { (track['Name'], track['Album'], track['Album Artist']): track for track in raw_library['Tracks'].values()} - def get_data(self, item): + def sync_data(self, item): key = (item.title, item.album, item.albumartist) result = self.collection.get(key) - # TODO: Need to investigate behavior for items without title, album, or - # albumartist before allowing them to be queried if not all(key) or not result: - # TODO: Need to log something here later - # print "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') From cb13d21ad69cfe957aacb27722a5591bea4de20f Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Sat, 9 May 2015 11:31:39 +0200 Subject: [PATCH 05/11] 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 67086481c4..22e96edcaa 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 @@ def commands(self): 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 @@ def func(self, lib, opts, args): """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 acfcca6a7c..b63b322eca 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 9ddf03776d..7ae1e46efe 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -18,10 +18,12 @@ 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 @@ def __init__(self, config, log): 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 @@ def sync_data(self, item): 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 4f6fa2a39a..6703d3c194 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. From bba8647bac78a55474adcc500df19aa270ff745b Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Sat, 9 May 2015 12:22:51 +0200 Subject: [PATCH 06/11] MetaSync: rename 'cls' variable to '_cls' --- beetsplug/metasync/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 22e96edcaa..9d004ea300 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -57,8 +57,8 @@ def is_meta_source_implementation(c): 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 + for cls_name, _cls in classes: + meta_sources[cls_name.lower()] = _cls return meta_sources From abd02052b91b29f1cd9f69d9186af2a613a2ccb8 Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Sun, 10 May 2015 14:31:52 +0200 Subject: [PATCH 07/11] MetaSync: small refactoring + enhancements - `sync_data` -> `sync_from_source` - properly catch ConfigValueError - avoiding iterating through library if we couldn't instantiate any meta sources - fix create_temporary_copy to actually make a tempdir --- beetsplug/metasync/__init__.py | 15 ++++++++++----- beetsplug/metasync/amarok.py | 2 +- beetsplug/metasync/itunes.py | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 9d004ea300..05669fd42c 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -15,12 +15,14 @@ """Synchronize information from music player libraries """ from abc import abstractmethod, ABCMeta -from beets import ui -from beets.plugins import BeetsPlugin import inspect import pkgutil from importlib import import_module +from beets.util.confit import ConfigValueError +from beets import ui +from beets.plugins import BeetsPlugin + METASYNC_MODULE = 'beetsplug.metasync' @@ -34,7 +36,7 @@ def __init__(self, config, log): self._log = log @abstractmethod - def sync_data(self, item): + def sync_from_source(self, item): pass @@ -116,14 +118,17 @@ def func(self, lib, opts, args): except KeyError: self._log.error(u'Unknown metadata source \'{0}\''.format( player)) - except ImportError as e: + except (ImportError, ConfigValueError) as e: self._log.error(u'Failed to instantiate metadata source ' u'\'{0}\': {1}'.format(player, e)) + if not meta_sources: + return + # Sync the items with all of the meta sources for item in lib.items(query): for meta_source in meta_sources.values(): - meta_source.sync_data(item) + meta_source.sync_from_source(item) changed = ui.show_model_changes(item) diff --git a/beetsplug/metasync/amarok.py b/beetsplug/metasync/amarok.py index b63b322eca..b544c22ec4 100644 --- a/beetsplug/metasync/amarok.py +++ b/beetsplug/metasync/amarok.py @@ -61,7 +61,7 @@ def __init__(self, config, log): self.collection = \ dbus.SessionBus().get_object('org.kde.amarok', '/Collection') - def sync_data(self, item): + def sync_from_source(self, item): path = displayable_path(item.path) # amarok unfortunately doesn't allow searching for the full path, only diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py index 7ae1e46efe..279621cf3b 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -30,7 +30,7 @@ @contextmanager def create_temporary_copy(path): - temp_dir = tempfile.gettempdir() + temp_dir = tempfile.mkdtemp() temp_path = os.path.join(temp_dir, 'temp_itunes_lib') shutil.copyfile(path, temp_path) try: @@ -76,7 +76,7 @@ def __init__(self, config, log): (track['Name'], track['Album'], track['Album Artist']): track for track in raw_library['Tracks'].values()} - def sync_data(self, item): + def sync_from_source(self, item): key = (item.title, item.album, item.albumartist) result = self.collection.get(key) From afeedd2e70f729a1ef937092ba9a8df367e40a96 Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Sun, 10 May 2015 14:46:59 +0200 Subject: [PATCH 08/11] MetaSync: basic tests No tests for Amarok, unfortunately --- test/rsrc/itunes_library.xml | 173 +++++++++++++++++++++++++++++++++++ test/test_metasync.py | 91 ++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 test/rsrc/itunes_library.xml create mode 100644 test/test_metasync.py diff --git a/test/rsrc/itunes_library.xml b/test/rsrc/itunes_library.xml new file mode 100644 index 0000000000..a0750251b8 --- /dev/null +++ b/test/rsrc/itunes_library.xml @@ -0,0 +1,173 @@ + + + + + Major Version1 + Minor Version1 + Date2015-05-08T14:36:28Z + Application Version12.1.2.27 + Features5 + Show Content Ratings + Music Folderfile:///beetstests/Music/iTunes/iTunes%20Media/ + Library Persistent ID1ABA8417E4946A32 + Tracks + + 634 + + Track ID634 + NameTessellate + Artistalt-J + Album Artistalt-J + AlbumAn Awesome Wave + GenreAlternative + KindMPEG audio file + Size5525212 + Total Time182674 + Disc Number1 + Disc Count1 + Track Number3 + Track Count13 + Year2012 + Date Modified2015-02-02T15:23:08Z + Date Added2014-04-24T09:28:38Z + Bit Rate238 + Sample Rate44100 + Play Count0 + Play Date3513593824 + Skip Count3 + Skip Date2015-02-05T15:41:04Z + Rating80 + Album Rating80 + Album Rating Computed + Artwork Count1 + Sort AlbumAwesome Wave + Sort Artistalt-J + Persistent ID20E89D1580C31363 + Track TypeFile + Locationfile:///beetstests/Music/Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3 + File Folder Count4 + Library Folder Count2 + + 636 + + Track ID636 + NameBreezeblocks + Artistalt-J + Album Artistalt-J + AlbumAn Awesome Wave + GenreAlternative + KindMPEG audio file + Size6827195 + Total Time227082 + Disc Number1 + Disc Count1 + Track Number4 + Track Count13 + Year2012 + Date Modified2015-02-02T15:23:08Z + Date Added2014-04-24T09:28:38Z + Bit Rate237 + Sample Rate44100 + Play Count31 + Play Date3513594051 + Play Date UTC2015-05-04T12:20:51Z + Skip Count0 + Rating100 + Album Rating80 + Album Rating Computed + Artwork Count1 + Sort AlbumAwesome Wave + Sort Artistalt-J + Persistent IDD7017B127B983D38 + Track TypeFile + Locationfile:///beetstests/Music/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3 + File Folder Count4 + Library Folder Count2 + + + Playlists + + + NameLibrary + Master + Playlist ID11480 + Playlist Persistent IDCD6FF684E7A6A166 + Visible + All Items + Playlist Items + + + Track ID634 + + + Track ID636 + + + + + NameMusic + Playlist ID16906 + Playlist Persistent ID4FB2E64E0971DD45 + Distinguished Kind4 + Music + All Items + Playlist Items + + + Track ID634 + + + Track ID636 + + + + + NameMovies + Playlist ID22338 + Playlist Persistent IDED848683ABD912C5 + Distinguished Kind2 + Movies + All Items + + + NameTV Shows + Playlist ID22344 + Playlist Persistent ID030882163A22E881 + Distinguished Kind3 + TV Shows + All Items + + + NamePodcasts + Playlist ID22347 + Playlist Persistent ID8A8C2A6F094235CF + Distinguished Kind10 + Podcasts + All Items + + + NameiTunes U + Playlist ID22354 + Playlist Persistent ID571BAA51CE17C191 + Distinguished Kind31 + iTunesU + All Items + + + NameAudiobooks + Playlist ID22357 + Playlist Persistent ID2D2BE73BF9612562 + Distinguished Kind5 + Audiobooks + All Items + + + NameGenius + Playlist ID22372 + Playlist Persistent IDF35301460DED0A7A + Distinguished Kind26 + All Items + + + + diff --git a/test/test_metasync.py b/test/test_metasync.py new file mode 100644 index 0000000000..82f8bf17a0 --- /dev/null +++ b/test/test_metasync.py @@ -0,0 +1,91 @@ +# This file is part of beets. +# Copyright 2015, Tom Jaspers. +# +# 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. +import os +from beets.library import Item + +from test import _common +from test._common import unittest +from test.helper import TestHelper + + +class MetaSyncTest(_common.TestCase, TestHelper): + itunes_library = os.path.join(_common.RSRC, 'itunes_library.xml') + + def setUp(self): + self.setup_beets() + self.load_plugins('metasync') + + self.config['metasync']['source'] = 'itunes' + self.config['metasync']['itunes']['library'] = self.itunes_library + + self._set_up_data() + + def _set_up_data(self): + items = [_common.item() for _ in range(2)] + items[0].title = 'Tessellate' + items[0].artist = 'alt-J' + items[0].albumartist = 'alt-J' + items[0].album = 'An Awesome Wave' + items[0].itunes_rating = 60 + + items[1].title = 'Breezeblocks' + items[1].artist = 'alt-J' + items[1].albumartist = 'alt-J' + items[1].album = 'An Awesome Wave' + + for item in items: + self.lib.add(item) + + def tearDown(self): + self.unload_plugins() + self.teardown_beets() + + def test_load_item_types(self): + # This test also verifies that the MetaSources have loaded correctly + self.assertIn('amarok_score', Item._types) + self.assertIn('itunes_rating', Item._types) + + def test_pretend_sync_from_itunes(self): + out = self.run_with_output('metasync', '-p') + + self.assertIn('itunes_rating: 60 -> 80', out) + self.assertIn('itunes_rating: 100', out) + self.assertIn('itunes_playcount: 31', out) + self.assertIn('itunes_skipcount: 3', out) + self.assertIn('itunes_lastplayed: 2015-05-04 12:20:51', out) + self.assertIn('itunes_lastskipped: 2015-02-05 15:41:04', out) + + self.assertEqual(self.lib.items()[0].itunes_rating, 60) + + def test_sync_from_itunes(self): + self.run_command('metasync') + + self.assertEqual(self.lib.items()[0].itunes_rating, 80) + self.assertEqual(self.lib.items()[0].itunes_playcount, 0) + self.assertEqual(self.lib.items()[0].itunes_skipcount, 3) + self.assertFalse(hasattr(self.lib.items()[0], 'itunes_lastplayed')) + self.assertEqual(self.lib.items()[0].itunes_lastskipped, 1423147264.0) + + self.assertEqual(self.lib.items()[1].itunes_rating, 100) + self.assertEqual(self.lib.items()[1].itunes_playcount, 31) + self.assertEqual(self.lib.items()[1].itunes_skipcount, 0) + self.assertEqual(self.lib.items()[1].itunes_lastplayed, 1430734851.0) + self.assertFalse(hasattr(self.lib.items()[1], 'itunes_lastskipped')) + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == b'__main__': + unittest.main(defaultTest='suite') From 16531b6cf8d5d7735c7b48178dc8bcec18abcec0 Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Mon, 11 May 2015 17:42:52 +0200 Subject: [PATCH 09/11] MetaSync: fix tests for Windows --- test/test_metasync.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/test_metasync.py b/test/test_metasync.py index 82f8bf17a0..10b245580b 100644 --- a/test/test_metasync.py +++ b/test/test_metasync.py @@ -12,6 +12,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. import os +import time +from datetime import datetime from beets.library import Item from test import _common @@ -19,6 +21,10 @@ from test.helper import TestHelper +def _parsetime(s): + return time.mktime(datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timetuple()) + + class MetaSyncTest(_common.TestCase, TestHelper): itunes_library = os.path.join(_common.RSRC, 'itunes_library.xml') @@ -75,12 +81,14 @@ def test_sync_from_itunes(self): self.assertEqual(self.lib.items()[0].itunes_playcount, 0) self.assertEqual(self.lib.items()[0].itunes_skipcount, 3) self.assertFalse(hasattr(self.lib.items()[0], 'itunes_lastplayed')) - self.assertEqual(self.lib.items()[0].itunes_lastskipped, 1423147264.0) + self.assertEqual(self.lib.items()[0].itunes_lastskipped, + _parsetime('2015-02-05 15:41:04')) self.assertEqual(self.lib.items()[1].itunes_rating, 100) self.assertEqual(self.lib.items()[1].itunes_playcount, 31) self.assertEqual(self.lib.items()[1].itunes_skipcount, 0) - self.assertEqual(self.lib.items()[1].itunes_lastplayed, 1430734851.0) + self.assertEqual(self.lib.items()[1].itunes_lastplayed, + _parsetime('2015-05-04 12:20:51')) self.assertFalse(hasattr(self.lib.items()[1], 'itunes_lastskipped')) From 02bec1bdd7bba89c033b2480132ba8a64cd8e50b Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Wed, 13 May 2015 11:01:52 +0200 Subject: [PATCH 10/11] MetaSync: Remove auto-discovery of sources In favor of simpler, hard-coded, list of sources, to avoid unneccesary magic. Also: check to see if query has results before instantiating the meta sources --- beetsplug/metasync/__init__.py | 44 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py index 05669fd42c..380c72707a 100644 --- a/beetsplug/metasync/__init__.py +++ b/beetsplug/metasync/__init__.py @@ -15,8 +15,6 @@ """Synchronize information from music player libraries """ from abc import abstractmethod, ABCMeta -import inspect -import pkgutil from importlib import import_module from beets.util.confit import ConfigValueError @@ -26,6 +24,12 @@ METASYNC_MODULE = 'beetsplug.metasync' +# Dictionary to map the MODULE and the CLASS NAME of meta sources +SOURCES = { + 'amarok': 'Amarok', + 'itunes': 'Itunes', +} + class MetaSource(object): __metaclass__ = ABCMeta @@ -44,23 +48,11 @@ 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 + for module_path, class_name in SOURCES.items(): + module = import_module(METASYNC_MODULE + '.' + module_path) + meta_sources[class_name.lower()] = getattr(module, class_name) return meta_sources @@ -108,12 +100,18 @@ def func(self, lib, opts, args): sources = sources or self.config['source'].as_str_seq() - meta_sources = {} + meta_source_instances = {} + items = lib.items(query) + + # Avoid needlessly instantiating meta sources (can be expensive) + if not items: + self._log.info(u'No items found matching query') + return # Instantiate the meta sources for player in sources: try: - meta_sources[player] = \ + meta_source_instances[player] = \ META_SOURCES[player](self.config, self._log) except KeyError: self._log.error(u'Unknown metadata source \'{0}\''.format( @@ -122,12 +120,14 @@ def func(self, lib, opts, args): self._log.error(u'Failed to instantiate metadata source ' u'\'{0}\': {1}'.format(player, e)) - if not meta_sources: + # Avoid needlessly iterating over items + if not meta_source_instances: + self._log.error(u'No valid metadata sources found') return # Sync the items with all of the meta sources - for item in lib.items(query): - for meta_source in meta_sources.values(): + for item in items: + for meta_source in meta_source_instances.values(): meta_source.sync_from_source(item) changed = ui.show_model_changes(item) From 94edc7a2a4724c2ed26b7fa3025105a43687f97b Mon Sep 17 00:00:00 2001 From: Tom Jaspers Date: Wed, 13 May 2015 20:02:34 +0200 Subject: [PATCH 11/11] MetaSync: minor improvements for iTunes source - Use path as an key to find items (over artist/title/album tuples) - Sensible default library location --- beetsplug/metasync/itunes.py | 35 +++- ...es_library.xml => itunes_library_unix.xml} | 94 +++++----- test/rsrc/itunes_library_windows.xml | 167 ++++++++++++++++++ test/test_metasync.py | 30 +++- 4 files changed, 265 insertions(+), 61 deletions(-) rename test/rsrc/{itunes_library.xml => itunes_library_unix.xml} (69%) create mode 100644 test/rsrc/itunes_library_windows.xml diff --git a/beetsplug/metasync/itunes.py b/beetsplug/metasync/itunes.py index 279621cf3b..b2795f9a40 100644 --- a/beetsplug/metasync/itunes.py +++ b/beetsplug/metasync/itunes.py @@ -19,6 +19,8 @@ import shutil import tempfile import plistlib +import urllib +from urlparse import urlparse from time import mktime from beets import util @@ -39,6 +41,21 @@ def create_temporary_copy(path): shutil.rmtree(temp_dir) +def _norm_itunes_path(path): + # Itunes prepends the location with 'file://' on posix systems, + # and with 'file://localhost/' on Windows systems. + # The actual path to the file is always saved as posix form + # E.g., 'file://Users/Music/bar' or 'file://localhost/G:/Music/bar' + + # The entire path will also be capitalized (e.g., '/Music/Alt-J') + # Note that this means the path will always have a leading separator, + # which is unwanted in the case of Windows systems. + # E.g., '\\G:\\Music\\bar' needs to be stripped to 'G:\\Music\\bar' + + return util.bytestring_path(os.path.normpath( + urllib.unquote(urlparse(path).path)).lstrip('\\')).lower() + + class Itunes(MetaSource): item_types = { @@ -52,8 +69,12 @@ class Itunes(MetaSource): def __init__(self, config, log): super(Itunes, self).__init__(config, log) + config.add({'itunes': { + 'library': '~/Music/iTunes/iTunes Library.xml' + }}) + # Load the iTunes library, which has to be the .xml one (not the .itl) - library_path = util.normpath(config['itunes']['library'].get(str)) + library_path = config['itunes']['library'].as_filename() try: self._log.debug( @@ -71,16 +92,14 @@ def __init__(self, config, log): hint = '' raise ConfigValueError(u'invalid iTunes library' + hint) - # Convert the library in to something we can query more easily - self.collection = { - (track['Name'], track['Album'], track['Album Artist']): track - for track in raw_library['Tracks'].values()} + # Make the iTunes library queryable using the path + self.collection = {_norm_itunes_path(track['Location']): track + for track in raw_library['Tracks'].values()} def sync_from_source(self, item): - key = (item.title, item.album, item.albumartist) - result = self.collection.get(key) + result = self.collection.get(util.bytestring_path(item.path).lower()) - if not all(key) or not result: + if not result: self._log.warning(u'no iTunes match found for {0}'.format(item)) return diff --git a/test/rsrc/itunes_library.xml b/test/rsrc/itunes_library_unix.xml similarity index 69% rename from test/rsrc/itunes_library.xml rename to test/rsrc/itunes_library_unix.xml index a0750251b8..c95bb52b47 100644 --- a/test/rsrc/itunes_library.xml +++ b/test/rsrc/itunes_library_unix.xml @@ -8,7 +8,7 @@ Application Version12.1.2.27 Features5 Show Content Ratings - Music Folderfile:///beetstests/Music/iTunes/iTunes%20Media/ + Music Folderfile:////Music/ Library Persistent ID1ABA8417E4946A32 Tracks @@ -44,7 +44,7 @@ Sort Artistalt-J Persistent ID20E89D1580C31363 Track TypeFile - Locationfile:///beetstests/Music/Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3 + Locationfile:///Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3 File Folder Count4 Library Folder Count2 @@ -80,7 +80,42 @@ Sort Artistalt-J Persistent IDD7017B127B983D38 Track TypeFile - Locationfile:///beetstests/Music/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3 + Locationfile://localhost/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3 + File Folder Count4 + Library Folder Count2 + + 638 + + Track ID638 + Name❦ (Ripe & Ruin) + Artistalt-J + Album Artistalt-J + AlbumAn Awesome Wave + KindMPEG audio file + Size2173293 + Total Time72097 + Disc Number1 + Disc Count1 + Track Number2 + Track Count13 + Year2012 + Date Modified2015-05-09T17:04:53Z + Date Added2015-02-02T15:28:39Z + Bit Rate233 + Sample Rate44100 + Play Count8 + Play Date3514109973 + Play Date UTC2015-05-10T11:39:33Z + Skip Count1 + Skip Date2015-02-02T15:29:10Z + Album Rating80 + Album Rating Computed + Artwork Count1 + Sort AlbumAwesome Wave + Sort Artistalt-J + Persistent ID183699FA0554D0E6 + Track TypeFile + Locationfile:///Music/Alt-J/An%20Awesome%20Wave/02%20%E2%9D%A6%20(Ripe%20&%20Ruin).mp3 File Folder Count4 Library Folder Count2 @@ -102,6 +137,9 @@ Track ID636 + + Track ID638 + @@ -119,55 +157,11 @@ Track ID636 + + Track ID638 + - - NameMovies - Playlist ID22338 - Playlist Persistent IDED848683ABD912C5 - Distinguished Kind2 - Movies - All Items - - - NameTV Shows - Playlist ID22344 - Playlist Persistent ID030882163A22E881 - Distinguished Kind3 - TV Shows - All Items - - - NamePodcasts - Playlist ID22347 - Playlist Persistent ID8A8C2A6F094235CF - Distinguished Kind10 - Podcasts - All Items - - - NameiTunes U - Playlist ID22354 - Playlist Persistent ID571BAA51CE17C191 - Distinguished Kind31 - iTunesU - All Items - - - NameAudiobooks - Playlist ID22357 - Playlist Persistent ID2D2BE73BF9612562 - Distinguished Kind5 - Audiobooks - All Items - - - NameGenius - Playlist ID22372 - Playlist Persistent IDF35301460DED0A7A - Distinguished Kind26 - All Items - diff --git a/test/rsrc/itunes_library_windows.xml b/test/rsrc/itunes_library_windows.xml new file mode 100644 index 0000000000..19184c3f23 --- /dev/null +++ b/test/rsrc/itunes_library_windows.xml @@ -0,0 +1,167 @@ + + + + + Major Version1 + Minor Version1 + Date2015-05-11T15:27:14Z + Application Version12.1.2.27 + Features5 + Show Content Ratings + Music Folderfile://localhost/C:/Documents%20and%20Settings/Owner/My%20Documents/My%20Music/iTunes/iTunes%20Media/ + Library Persistent IDB4C9F3EE26EFAF78 + Tracks + + 180 + + Track ID180 + NameTessellate + Artistalt-J + Album Artistalt-J + AlbumAn Awesome Wave + GenreAlternative + KindMPEG audio file + Size5525212 + Total Time182674 + Disc Number1 + Disc Count1 + Track Number3 + Track Count13 + Year2012 + Date Modified2015-02-02T15:23:08Z + Date Added2014-04-24T09:28:38Z + Bit Rate238 + Sample Rate44100 + Play Count0 + Play Date3513593824 + Skip Count3 + Skip Date2015-02-05T15:41:04Z + Rating80 + Album Rating80 + Album Rating Computed + Artwork Count1 + Sort AlbumAwesome Wave + Sort Artistalt-J + Persistent ID20E89D1580C31363 + Track TypeFile + Locationfile://localhost/G:/Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3 + File Folder Count-1 + Library Folder Count-1 + + 183 + + Track ID183 + NameBreezeblocks + Artistalt-J + Album Artistalt-J + AlbumAn Awesome Wave + GenreAlternative + KindMPEG audio file + Size6827195 + Total Time227082 + Disc Number1 + Disc Count1 + Track Number4 + Track Count13 + Year2012 + Date Modified2015-02-02T15:23:08Z + Date Added2014-04-24T09:28:38Z + Bit Rate237 + Sample Rate44100 + Play Count31 + Play Date3513594051 + Play Date UTC2015-05-04T12:20:51Z + Skip Count0 + Rating100 + Album Rating80 + Album Rating Computed + Artwork Count1 + Sort AlbumAwesome Wave + Sort Artistalt-J + Persistent IDD7017B127B983D38 + Track TypeFile + Locationfile://localhost/G:/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3 + File Folder Count-1 + Library Folder Count-1 + + 638 + + Track ID638 + Name❦ (Ripe & Ruin) + Artistalt-J + Album Artistalt-J + AlbumAn Awesome Wave + KindMPEG audio file + Size2173293 + Total Time72097 + Disc Number1 + Disc Count1 + Track Number2 + Track Count13 + Year2012 + Date Modified2015-05-09T17:04:53Z + Date Added2015-02-02T15:28:39Z + Bit Rate233 + Sample Rate44100 + Play Count8 + Play Date3514109973 + Play Date UTC2015-05-10T11:39:33Z + Skip Count1 + Skip Date2015-02-02T15:29:10Z + Album Rating80 + Album Rating Computed + Artwork Count1 + Sort AlbumAwesome Wave + Sort Artistalt-J + Persistent ID183699FA0554D0E6 + Track TypeFile + Locationfile://localhost/G:/Experiments/Alt-J/An%20Awesome%20Wave/02%20%E2%9D%A6%20(Ripe%20&%20Ruin).mp3 + File Folder Count4 + Library Folder Count2 + + + Playlists + + + NameBibliotheek + Master + Playlist ID72 + Playlist Persistent ID728AA5B1D00ED23B + Visible + All Items + Playlist Items + + + Track ID180 + + + Track ID183 + + + Track ID638 + + + + + NameMuziek + Playlist ID103 + Playlist Persistent ID8120A002B0486AD7 + Distinguished Kind4 + Music + All Items + Playlist Items + + + Track ID180 + + + Track ID183 + + + Track ID638 + + + + + + diff --git a/test/test_metasync.py b/test/test_metasync.py index 10b245580b..8e2669c414 100644 --- a/test/test_metasync.py +++ b/test/test_metasync.py @@ -12,6 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. import os +import platform import time from datetime import datetime from beets.library import Item @@ -25,20 +26,34 @@ def _parsetime(s): return time.mktime(datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timetuple()) +def _is_windows(): + return platform.system() == "Windows" + + class MetaSyncTest(_common.TestCase, TestHelper): - itunes_library = os.path.join(_common.RSRC, 'itunes_library.xml') + itunes_library_unix = os.path.join(_common.RSRC, + 'itunes_library_unix.xml') + itunes_library_windows = os.path.join(_common.RSRC, + 'itunes_library_windows.xml') def setUp(self): self.setup_beets() self.load_plugins('metasync') self.config['metasync']['source'] = 'itunes' - self.config['metasync']['itunes']['library'] = self.itunes_library + + if _is_windows(): + self.config['metasync']['itunes']['library'] = \ + self.itunes_library_windows + else: + self.config['metasync']['itunes']['library'] = \ + self.itunes_library_unix self._set_up_data() def _set_up_data(self): items = [_common.item() for _ in range(2)] + items[0].title = 'Tessellate' items[0].artist = 'alt-J' items[0].albumartist = 'alt-J' @@ -50,6 +65,15 @@ def _set_up_data(self): items[1].albumartist = 'alt-J' items[1].album = 'An Awesome Wave' + if _is_windows(): + items[0].path = \ + u'G:\\Music\\Alt-J\\An Awesome Wave\\03 Tessellate.mp3' + items[1].path = \ + u'G:\\Music\\Alt-J\\An Awesome Wave\\04 Breezeblocks.mp3' + else: + items[0].path = u'/Music/Alt-J/An Awesome Wave/03 Tessellate.mp3' + items[1].path = u'/Music/Alt-J/An Awesome Wave/04 Breezeblocks.mp3' + for item in items: self.lib.add(item) @@ -71,7 +95,6 @@ def test_pretend_sync_from_itunes(self): self.assertIn('itunes_skipcount: 3', out) self.assertIn('itunes_lastplayed: 2015-05-04 12:20:51', out) self.assertIn('itunes_lastskipped: 2015-02-05 15:41:04', out) - self.assertEqual(self.lib.items()[0].itunes_rating, 60) def test_sync_from_itunes(self): @@ -95,5 +118,6 @@ def test_sync_from_itunes(self): def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == b'__main__': unittest.main(defaultTest='suite')