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