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')