diff --git a/build/lib/libpytunes/Library.py b/build/lib/libpytunes/Library.py new file mode 100644 index 0000000..a405dba --- /dev/null +++ b/build/lib/libpytunes/Library.py @@ -0,0 +1,145 @@ +import logging +import plistlib +from six import PY2 +from six.moves.urllib import parse as urlparse +import time + +from libpytunes.Song import Song +from libpytunes.Playlist import Playlist + + +logger = logging.getLogger(__name__) + +try: + import xspf + xspfAvailable = True +except ImportError: + xspfAvailable = False + pass + + +class Library: + def __init__(self, itunesxml, musicPathXML=None, musicPathSystem=None, filesOnly=False): + # musicPathXML and musicPathSystem will do path conversion + # when xml is being processed on different OS then iTunes + self.musicPathXML = musicPathXML + self.musicPathSystem = musicPathSystem + self.filesOnly = filesOnly + with open(itunesxml, 'rb') as f: + self.il = plistlib.load(f) + self.songs = {} + self.getSongs() + + def getSongs(self): + format = "%Y-%m-%d %H:%M:%S" + for trackid, attributes in self.il['Tracks'].items(): + s = Song() + + s.name = attributes.get('Name') + + # Support classical music naming (Work+Movement Number+Movement Name) since iTunes 12.5 + s.work = attributes.get('Work') + s.movement_number = attributes.get('Movement Number') + s.movement_count = attributes.get('Movement Count') + s.movement_name = attributes.get('Movement Name') + + s.track_id = int(attributes.get('Track ID')) if attributes.get('Track ID') else None + s.artist = attributes.get('Artist') + s.album_artist = attributes.get('Album Artist') + s.composer = attributes.get('Composer') + s.album = attributes.get('Album') + s.genre = attributes.get('Genre') + s.kind = attributes.get('Kind') + s.size = int(attributes.get('Size')) if attributes.get('Size') else None + s.total_time = attributes.get('Total Time') + s.track_number = attributes.get('Track Number') + s.track_count = int(attributes.get('Track Count')) if attributes.get('Track Count') else None + s.disc_number = int(attributes.get('Disc Number')) if attributes.get('Disc Number') else None + s.disc_count = int(attributes.get('Disc Count')) if attributes.get('Disc Count') else None + s.year = int(attributes.get('Year')) if attributes.get('Year') else None + s.date_modified = time.strptime(str(attributes.get('Date Modified')), format) if attributes.get('Date Modified') else None + s.date_added = time.strptime(str(attributes.get('Date Added')), format) if attributes.get('Date Added') else None + s.bit_rate = int(attributes.get('Bit Rate')) if attributes.get('Bit Rate') else None + s.sample_rate = int(attributes.get('Sample Rate')) if attributes.get('Sample Rate') else None + s.comments = attributes.get("Comments") + s.rating = int(attributes.get('Rating')) if attributes.get('Rating') else None + s.rating_computed = 'Rating Computed' in attributes + s.play_count = int(attributes.get('Play Count')) if attributes.get('Play Count') else None + s.album_rating = attributes.get('Album Rating') + s.album_rating_computed = 'Album Rating Computed' in attributes + s.persistent_id = attributes.get('Persistent ID') + + if attributes.get('Location'): + s.location_escaped = attributes.get('Location') + s.location = s.location_escaped + s.location = urlparse.unquote(urlparse.urlparse(attributes.get('Location')).path[1:]) + s.location = s.location.decode('utf-8') if PY2 else s.location # fixes bug #19 + if (self.musicPathXML is not None and self.musicPathSystem is not None): + s.location = s.location.replace(self.musicPathXML, self.musicPathSystem) + + s.compilation = 'Compilation' in attributes + s.lastplayed = time.strptime(str(attributes.get('Play Date UTC')), format) if attributes.get('Play Date UTC') else None + s.skip_count = int(attributes.get('Skip Count')) if attributes.get('Skip Count') else None + s.skip_date = time.strptime(str(attributes.get('Skip Date')), format) if attributes.get('Skip Date') else None + s.length = int(attributes.get('Total Time')) if attributes.get('Total Time') else None + s.track_type = attributes.get('Track Type') + s.grouping = attributes.get('Grouping') + s.podcast = 'Podcast' in attributes + s.movie = 'Movie' in attributes + s.has_video = 'Has Video' in attributes + s.loved = 'Loved' in attributes + s.album_loved = 'Album Loved' in attributes + s.playlist_only = 'Playlist Only' in attributes + s.apple_music = 'Apple Music' in attributes + s.protected = 'Protected' in attributes + + self.songs[int(trackid)] = s + + def getPlaylistNames(self, ignoreList=[ + "Library", "Music", "Movies", "TV Shows", "Purchased", "iTunes DJ", "Podcasts" + ]): + + playlists = [] + for playlist in self.il['Playlists']: + if playlist['Name'] not in ignoreList: + playlists.append(playlist['Name']) + return playlists + + def getPlaylist(self, playlistName): + for playlist in self.il['Playlists']: + if playlist['Name'] == playlistName: + # id playlist_id track_num url title album artist length uniqueid + p = Playlist(playlistName) + p.playlist_id = playlist['Playlist ID'] + p.is_folder = playlist.get('Folder', False) + p.playlist_persistent_id = playlist.get('Playlist Persistent ID') + p.parent_persistent_id = playlist.get('Parent Persistent ID') + p.distinguished_kind = playlist.get('Distinguished Kind') + p.is_genius_playlist = True if playlist.get('Genius Track ID') else False + p.is_smart_playlist = True if playlist.get('Smart Info') and not playlist.get('Folder', False) else False + tracknum = 1 + # Make sure playlist was not empty + if 'Playlist Items' in playlist: + for track in playlist['Playlist Items']: + id = int(track['Track ID']) + t = self.songs[id] + t.playlist_order = tracknum + tracknum += 1 + p.tracks.append(t) + return p + + def getPlaylistxspf(self, playlistName): + global xspfAvailable + if (xspfAvailable): + x = xspf.Xspf() + for playlist in self.il['Playlists']: + if playlist['Name'] == playlistName: + x.title = playlistName + x.info = "" + for track in playlist['Playlist Items']: + id = int(track['Track ID']) + x.add_track(title=self.songs[id].name, creator="", location=self.songs[id].location) + return x.toXml() + else: + logger.warning("xspf library missing, go to https://github.com/alastair/xspf to install.") + return None diff --git a/build/lib/libpytunes/Playlist.py b/build/lib/libpytunes/Playlist.py new file mode 100644 index 0000000..1598173 --- /dev/null +++ b/build/lib/libpytunes/Playlist.py @@ -0,0 +1,19 @@ +from six import iteritems + +class Playlist: + is_folder = False + playlist_persistent_id = None + parent_persistent_id = None + distinguished_kind = None + playlist_id = None + + def __init__(self, playListName=None): + self.name = playListName + self.tracks = [] + + def __iter__(self): + for attr, value in iteritems(self.__dict__): + yield attr, value + + def ToDict(self): + return {key: value for (key, value) in self} diff --git a/build/lib/libpytunes/Song.py b/build/lib/libpytunes/Song.py new file mode 100644 index 0000000..285e77e --- /dev/null +++ b/build/lib/libpytunes/Song.py @@ -0,0 +1,96 @@ +from six import iteritems + + +class Song: + """ + Song Attributes: + name (String) + track_id (Integer) + artist (String) + album_artist (String) + composer = None (String) + album = None (String) + genre = None (String) + kind = None (String) + size = None (Integer) + total_time = None (Integer) + track_number = None (Integer) + track_count = None (Integer) + disc_number = None (Integer) + disc_count = None (Integer) + year = None (Integer) + date_modified = None (Time) + date_added = None (Time) + bit_rate = None (Integer) + sample_rate = None (Integer) + comments = None (String) + rating = None (Integer) + rating_computed = False (Boolean) + album_rating = None (Integer) + play_count = None (Integer) + location = None (String) + location_escaped = None (String) + compilation = False (Boolean) + grouping = None (String) + lastplayed = None (Time) + skip_count = None (Integer) + skip_date = None (Time) + length = None (Integer) + persistent_id = None (String) + album_rating_computed = False (Boolean) + work = None (String) + movement_name = None (String) + movement_number = None (Integer) + movement_count = None (Integer) + playlist_only = None (Bool) + apple_music = None (Bool) + protected = None (Bool) + """ + name = None + track_id = None + artist = None + album_artist = None + composer = None + album = None + genre = None + kind = None + size = None + total_time = None + track_number = None + track_count = None + disc_number = None + disc_count = None + year = None + date_modified = None + date_added = None + bit_rate = None + sample_rate = None + comments = None + rating = None + rating_computed = None + album_rating = None + play_count = None + skip_count = None + skip_date = None + location = None + location_escaped = None + compilation = None + grouping = None + lastplayed = None + length = None + persistent_id = None + album_rating_computed = None + work = None + movement_name = None + movement_number = None + movement_count = None + playlist_only = None + apple_music = None + protected = None + + def __iter__(self): + for attr, value in iteritems(self.__dict__): + yield attr, value + + def ToDict(self): + return {key: value for (key, value) in self} diff --git a/build/lib/libpytunes/__init__.py b/build/lib/libpytunes/__init__.py new file mode 100644 index 0000000..12af6e8 --- /dev/null +++ b/build/lib/libpytunes/__init__.py @@ -0,0 +1,3 @@ +from libpytunes.Library import Library +from libpytunes.Song import Song +from libpytunes.Playlist import Playlist diff --git a/build/lib/libpytunes/tests/__init__.py b/build/lib/libpytunes/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/libpytunes/tests/test_library.py b/build/lib/libpytunes/tests/test_library.py new file mode 100644 index 0000000..c324430 --- /dev/null +++ b/build/lib/libpytunes/tests/test_library.py @@ -0,0 +1,25 @@ +import unittest +from libpytunes import Library +import os + +class TestLibrary(unittest.TestCase): + + def setUp(self): + self.it_library = Library(os.path.join(os.path.dirname(__file__), "Test Library.xml")) + + def test_songs(self): + + for id, song in self.it_library.songs.items(): + assert(hasattr(song, 'name') == True) + + def test_playlists(self): + + playlists = self.it_library.getPlaylistNames() + + for song in self.it_library.getPlaylist(playlists[0]).tracks: + assert(hasattr(song, 'track_number')) + assert(hasattr(song, 'artist')) + assert(hasattr(song, 'name')) + +if __name__ == '__main__': + unittest.main() diff --git a/dist/libpytunes-1.5.2-py3.10.egg b/dist/libpytunes-1.5.2-py3.10.egg new file mode 100644 index 0000000..08827fc Binary files /dev/null and b/dist/libpytunes-1.5.2-py3.10.egg differ diff --git a/libpytunes.egg-info/PKG-INFO b/libpytunes.egg-info/PKG-INFO index 6672885..add3d72 100644 --- a/libpytunes.egg-info/PKG-INFO +++ b/libpytunes.egg-info/PKG-INFO @@ -1,149 +1,159 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: libpytunes Version: 1.5.2 Summary: Python Itunes Library parser Home-page: https://github.com/liamks/libpytunes Author: Liam Kaufman -Author-email: UNKNOWN +Author-email: License: MIT -Description: # libpytunes - - Created by Liam Kaufman (liamkaufman.com) - - Contributions by Liam Kaufman (liamkaufman.com), Steven Miller (copart), dpchu, selftext, z4r, pschorf, Mathew Bramson (mbramson), Roger Filmyer (rfilmyer), cktse, Scot Hacker (shacker) - - **Before using libpytunes it is recommended that you backup your Itunes Library XML file. Use libpytunes at your own risk - there is no guarantee that it works or will not blow-up your computer!** - - ## Usage: - - ``` - from libpytunes import Library - - l = Library("/path/to/iTunes Library.xml") - - for id, song in l.songs.items(): - if song and song.rating: - if song.rating > 80: - print(song.name, song.rating) - - playlists=l.getPlaylistNames() - - for song in l.getPlaylist(playlists[0]).tracks: - print("[{t}] {a} - {n}".format(t=song.track_number, a=song.artist, n=song.name)) - ``` - - See below for available song attributes. - - If your library is very large, reading the XML into memory could be quite slow. If you need to access the library repeatedly, Python's "pickle" can save a binary representation of the XML object to disk for much faster access (up to 10x faster). To use a pickled version, do something like this: - - ``` - import os.path - import pickle - import time - from libpytunes import Library - - lib_path = "/Users/[username]/Music/iTunes/iTunes Library.xml" - pickle_file = "itl.p" - expiry = 60 * 60 # Refresh pickled file if older than - epoch_time = int(time.time()) # Now - - # Generate pickled version of database if stale or doesn't exist - if not os.path.isfile(pickle_file) or os.path.getmtime(pickle_file) + expiry < epoch_time: - itl_source = Library(lib_path) - pickle.dump(itl_source, open(pickle_file, "wb")) - - itl = pickle.load(open(pickle_file, "rb")) - - for id, song in itl.songs.items(): - if song and song.rating: - if song.rating > 80: - print("{n}, {r}".format(n=song.name, r=song.rating)) - ``` - - ## Notes - - Track counts may not match those shown in iTunes. e.g. This may report a higher number than the song count shown in iTunes itself. : - - ``` - l = Library("iTunes Library.xml") - len(l.songs) - ``` - - This is because iTunes does not count things like Podcasts and Voice Memos as "Music," whereas libpytunes counts **all** tracks. - - The songs dictionary is keyed on TrackID (as coded in iTunes xml). Playlists are lists of Song objects, with their order noted as a `playlist_order` attribute. - - ### Attributes of the Song class: - - ``` - persistent_id (String) - name (String) - artist (String) - album_artist (String) - composer = None (String) - album = None (String) - genre = None (String) - kind = None (String) - size = None (Integer) - total_time = None (Integer) - track_number = None (Integer) - track_count = None (Integer) - disc_number = None (Integer) - disc_count = None (Integer) - year = None (Integer) - date_modified = None (Time) - date_added = None (Time) - bit_rate = None (Integer) - sample_rate = None (Integer) - comments = None (String) - rating = None (Integer) - album_rating = None (Integer) - play_count = None (Integer) - location = None (String) - location_escaped = None (String) - compilation = False (Boolean) - grouping = None (String) - lastplayed = None (Time) - skip_count = None (Integer) - skip_date = None(Time) - length = None (Integer) - work = None (String) - movement_name = None (String) - movement_number = None (Integer) - movement_count = None (Integer) - loved = False (Boolean) - album_loved = False (Boolean) - - ``` - - Songs retrieved as part of a playlist have an additional attribute: - ``` - playlist_order = None (Integer) - ``` - - - Song object attributes can be iterated through like this: - ``` - for key, value in SongItem: - . - ``` - - You can also convert songs directly to Dictionaries with the ToDict() Method. - ``` - SongDictionary = SongItem.ToDict() - ``` - - ### Attributes of the Playlist class: - ``` - name (String) - tracks (List[Song]) - is_folder = False (Boolean) - playlist_persistent_id = None (String) - parent_persistent_id = None (String) - ``` - - ### Legacy Mode - Support for `legacymode` has been removed with version 1.5 - Platform: UNKNOWN Classifier: License :: OSI Approved :: MIT License +License-File: LICENSE + +![Travis CI Master branch](https://travis-ci.org/liamks/libpytunes.svg?branch=master) + +# libpytunes + +Created by Liam Kaufman (liamkaufman.com) + +Contributions by Liam Kaufman (liamkaufman.com), Steven Miller (copart), dpchu, selftext, z4r, pschorf, Mathew Bramson (mbramson), Roger Filmyer (rfilmyer), cktse, Scot Hacker (shacker) + +**Before using libpytunes it is recommended that you backup your Itunes Library XML file. Use libpytunes at your own risk - there is no guarantee that it works or will not blow-up your computer!** + +If you don't see an .xml library file in `~/Music/iTunes`, you probably started using iTunes after version 12.2, and have never enabled sharing between iTunes and other apps. To generate one, go to iTunes Preferences | Advanced and select "Share iTunes Library XML with other applications." ([Apple docs](https://support.apple.com/en-us/HT201610)) + +## Usage: + +``` +from libpytunes import Library + +l = Library("/path/to/iTunes Library.xml") + +for id, song in l.songs.items(): + if song and song.rating: + if song.rating > 80: + print(song.name, song.rating) + +playlists=l.getPlaylistNames() + +for song in l.getPlaylist(playlists[0]).tracks: + print("[{t}] {a} - {n}".format(t=song.track_number, a=song.artist, n=song.name)) +``` + +See below for available song attributes. + +If your library is very large, reading the XML into memory could be quite slow. If you need to access the library repeatedly, Python's "pickle" can save a binary representation of the XML object to disk for much faster access (up to 10x faster). To use a pickled version, do something like this: + +``` +import os.path +import pickle +import time +from libpytunes import Library + +lib_path = "/Users/[username]/Music/iTunes/iTunes Library.xml" +pickle_file = "itl.p" +expiry = 60 * 60 # Refresh pickled file if older than +epoch_time = int(time.time()) # Now + +# Generate pickled version of database if stale or doesn't exist +if not os.path.isfile(pickle_file) or os.path.getmtime(pickle_file) + expiry < epoch_time: + itl_source = Library(lib_path) + pickle.dump(itl_source, open(pickle_file, "wb")) + +itl = pickle.load(open(pickle_file, "rb")) + +for id, song in itl.songs.items(): + if song and song.rating: + if song.rating > 80: + print("{n}, {r}".format(n=song.name, r=song.rating)) +``` + +## Notes + +Track counts may not match those shown in iTunes. e.g. This may report a higher number than the song count shown in iTunes itself. : + +``` +l = Library("iTunes Library.xml") +len(l.songs) +``` + +This is because iTunes does not count things like Podcasts and Voice Memos as "Music," whereas libpytunes counts **all** tracks. + +The songs dictionary is keyed on TrackID (as coded in iTunes xml). Playlists are lists of Song objects, with their order noted as a `playlist_order` attribute. + +### Attributes of the Song class: + +``` +persistent_id (String) +name (String) +artist (String) +album_artist (String) +composer = None (String) +album = None (String) +genre = None (String) +kind = None (String) +size = None (Integer) +total_time = None (Integer) +track_number = None (Integer) +track_count = None (Integer) +disc_number = None (Integer) +disc_count = None (Integer) +year = None (Integer) +date_modified = None (Time) +date_added = None (Time) +bit_rate = None (Integer) +sample_rate = None (Integer) +comments = None (String) +rating = None (Integer) +album_rating = None (Integer) +play_count = None (Integer) +location = None (String) +location_escaped = None (String) +compilation = False (Boolean) +grouping = None (String) +lastplayed = None (Time) +skip_count = None (Integer) +skip_date = None(Time) +length = None (Integer) +work = None (String) +movement_name = None (String) +movement_number = None (Integer) +movement_count = None (Integer) +loved = False (Boolean) +album_loved = False (Boolean) +playlist_only = None (Bool) +apple_music = None (Bool) +protected = None (Bool) + +``` + +Songs retrieved as part of a playlist have an additional attribute: +``` +playlist_order = None (Integer) +``` + + +Song object attributes can be iterated through like this: +``` +for key, value in SongItem: + . +``` + +You can also convert songs directly to Dictionaries with the ToDict() Method. +``` +SongDictionary = SongItem.ToDict() +``` + +### Attributes of the Playlist class: +``` +name (String) +tracks (List[Song]) +is_folder = False (Boolean) +playlist_persistent_id = None (String) +parent_persistent_id = None (String) +``` + +### Legacy Mode +Support for `legacymode` has been removed with version 1.5 + + diff --git a/libpytunes.egg-info/SOURCES.txt b/libpytunes.egg-info/SOURCES.txt index 20d9043..a2236bb 100644 --- a/libpytunes.egg-info/SOURCES.txt +++ b/libpytunes.egg-info/SOURCES.txt @@ -1,3 +1,6 @@ +LICENSE +README.md +setup.py libpytunes/Library.py libpytunes/Playlist.py libpytunes/Song.py @@ -6,6 +9,7 @@ libpytunes.egg-info/PKG-INFO libpytunes.egg-info/SOURCES.txt libpytunes.egg-info/dependency_links.txt libpytunes.egg-info/namespace_packages.txt +libpytunes.egg-info/requires.txt libpytunes.egg-info/top_level.txt libpytunes/tests/__init__.py libpytunes/tests/test_library.py \ No newline at end of file diff --git a/libpytunes.egg-info/requires.txt b/libpytunes.egg-info/requires.txt new file mode 100644 index 0000000..0b0a8ce --- /dev/null +++ b/libpytunes.egg-info/requires.txt @@ -0,0 +1 @@ +six>=1.11.0 diff --git a/libpytunes/Library.py b/libpytunes/Library.py index 67748d6..a405dba 100755 --- a/libpytunes/Library.py +++ b/libpytunes/Library.py @@ -25,7 +25,8 @@ def __init__(self, itunesxml, musicPathXML=None, musicPathSystem=None, filesOnly self.musicPathXML = musicPathXML self.musicPathSystem = musicPathSystem self.filesOnly = filesOnly - self.il = plistlib.readPlist(itunesxml) # Much better support of xml special characters + with open(itunesxml, 'rb') as f: + self.il = plistlib.load(f) self.songs = {} self.getSongs() diff --git a/setup.py b/setup.py index c761ea0..04139aa 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ ], packages=find_packages(exclude=['ez_setup']), install_requires=[ - 'six==1.11.0' + 'six>=1.11.0' ], namespace_packages=[] )