diff --git a/beetsplug/metasync/__init__.py b/beetsplug/metasync/__init__.py
index 743bc8277..380c72707 100644
--- a/beetsplug/metasync/__init__.py
+++ b/beetsplug/metasync/__init__.py
@@ -14,28 +14,64 @@
"""Synchronize information from music player libraries
"""
+from abc import abstractmethod, ABCMeta
+from importlib import import_module
-from beets import ui, logging
+from beets.util.confit import ConfigValueError
+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')
+
+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
+
+ def __init__(self, config, log):
+ self.item_types = {}
+ self.config = config
+ self._log = log
+
+ @abstractmethod
+ def sync_from_source(self, item):
+ pass
+
+
+def load_meta_sources():
+ """ Returns a dictionary of all the MetaSources
+ E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true
+ """
+ meta_sources = {}
+
+ 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
+
+
+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()
- }
+ item_types = load_item_types()
def __init__(self):
super(MetaSyncPlugin, self).__init__()
@@ -45,9 +81,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]
@@ -56,31 +92,43 @@ 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_source_instances = {}
+ items = lib.items(query)
- if module not in modules.keys():
- log.error(u'Unknown metadata source \'' + player + '\'')
- continue
+ # Avoid needlessly instantiating meta sources (can be expensive)
+ if not items:
+ self._log.info(u'No items found matching query')
+ return
- classes = inspect.getmembers(modules[module], inspect.isclass)
+ # Instantiate the meta sources
+ for player in sources:
+ try:
+ meta_source_instances[player] = \
+ META_SOURCES[player](self.config, self._log)
+ except KeyError:
+ self._log.error(u'Unknown metadata source \'{0}\''.format(
+ player))
+ except (ImportError, ConfigValueError) as e:
+ self._log.error(u'Failed to instantiate metadata source '
+ u'\'{0}\': {1}'.format(player, e))
- for entry in classes:
- if entry[0].lower() == player:
- sources[player] = entry[1]()
- else:
- continue
+ # Avoid needlessly iterating over items
+ if not meta_source_instances:
+ self._log.error(u'No valid metadata sources found')
+ return
- for item in lib.items(query):
- for player in sources.values():
- player.get_data(item)
+ # Sync the items with all of the meta sources
+ for item in items:
+ for meta_source in meta_source_instances.values():
+ 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 3ffeaeac9..b544c22ec 100644
--- a/beetsplug/metasync/amarok.py
+++ b/beetsplug/metasync/amarok.py
@@ -18,12 +18,33 @@
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
+
+from beets.util import displayable_path
+from beets.dbcore import types
+from beets.library import DateType
+from beetsplug.metasync import MetaSource
-class Amarok(object):
+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' \
\
@@ -31,11 +52,16 @@ class Amarok(object):
\
'
- def __init__(self):
+ 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')
- def get_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
new file mode 100644
index 000000000..b2795f9a4
--- /dev/null
+++ b/beetsplug/metasync/itunes.py
@@ -0,0 +1,116 @@
+# 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
+import plistlib
+import urllib
+from urlparse import urlparse
+from time import mktime
+
+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
+
+
+@contextmanager
+def create_temporary_copy(path):
+ temp_dir = tempfile.mkdtemp()
+ temp_path = os.path.join(temp_dir, 'temp_itunes_lib')
+ shutil.copyfile(path, temp_path)
+ try:
+ yield temp_path
+ finally:
+ 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 = {
+ '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)
+
+ 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 = config['itunes']['library'].as_filename()
+
+ 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(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'
+ else:
+ hint = ''
+ raise ConfigValueError(u'invalid iTunes library' + hint)
+
+ # 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):
+ result = self.collection.get(util.bytestring_path(item.path).lower())
+
+ if not result:
+ self._log.warning(u'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())
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 4488e821b..9432a932c 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. :bug:`1450`
+
+
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 cd157eacd..6703d3c19 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 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.
+
+itunes
+''''''
+
+The path to your iTunes library **xml** file has to be configured, e.g.::
+
+ metasync:
+ source: itunes
+ itunes:
+ library: ~/Music/iTunes Library.xml
+
+Please note the indentation.
Usage
-----
@@ -44,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.
diff --git a/test/rsrc/itunes_library_unix.xml b/test/rsrc/itunes_library_unix.xml
new file mode 100644
index 000000000..c95bb52b4
--- /dev/null
+++ b/test/rsrc/itunes_library_unix.xml
@@ -0,0 +1,167 @@
+
+
+
+
+ Major Version1
+ Minor Version1
+ Date2015-05-08T14:36:28Z
+ Application Version12.1.2.27
+ Features5
+ Show Content Ratings
+ Music Folderfile:////Music/
+ 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:///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://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
+
+
+ Playlists
+
+
+ NameLibrary
+ Master
+ Playlist ID11480
+ Playlist Persistent IDCD6FF684E7A6A166
+ Visible
+ All Items
+ Playlist Items
+
+
+ Track ID634
+
+
+ Track ID636
+
+
+ Track ID638
+
+
+
+
+ NameMusic
+ Playlist ID16906
+ Playlist Persistent ID4FB2E64E0971DD45
+ Distinguished Kind4
+ Music
+ All Items
+ Playlist Items
+
+
+ Track ID634
+
+
+ Track ID636
+
+
+ Track ID638
+
+
+
+
+
+
diff --git a/test/rsrc/itunes_library_windows.xml b/test/rsrc/itunes_library_windows.xml
new file mode 100644
index 000000000..19184c3f2
--- /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
new file mode 100644
index 000000000..8e2669c41
--- /dev/null
+++ b/test/test_metasync.py
@@ -0,0 +1,123 @@
+# 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
+import platform
+import time
+from datetime import datetime
+from beets.library import Item
+
+from test import _common
+from test._common import unittest
+from test.helper import TestHelper
+
+
+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_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'
+
+ 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'
+ 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'
+
+ 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)
+
+ 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,
+ _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,
+ _parsetime('2015-05-04 12:20:51'))
+ self.assertFalse(hasattr(self.lib.items()[1], 'itunes_lastskipped'))
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
+
+
+if __name__ == b'__main__':
+ unittest.main(defaultTest='suite')