From 26b3fdf69fa6c6570acf39aec9b07d953e448f0a Mon Sep 17 00:00:00 2001 From: SCervino Date: Mon, 8 Aug 2022 18:01:05 +0200 Subject: [PATCH 1/2] I replaced the deprecated method readPlist --- libpytunes/Library.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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() From 62aec0627b9697f013fa9397453d0f18b97b6a89 Mon Sep 17 00:00:00 2001 From: SCervino Date: Mon, 22 Aug 2022 18:47:52 +0200 Subject: [PATCH 2/2] fixed six version for SpotiTunes compatibility --- build/lib/libpytunes/Library.py | 145 ++++++++++ build/lib/libpytunes/Playlist.py | 19 ++ build/lib/libpytunes/Song.py | 96 +++++++ build/lib/libpytunes/__init__.py | 3 + build/lib/libpytunes/tests/__init__.py | 0 build/lib/libpytunes/tests/test_library.py | 25 ++ dist/libpytunes-1.5.2-py3.10.egg | Bin 0 -> 13452 bytes libpytunes.egg-info/PKG-INFO | 292 +++++++++++---------- libpytunes.egg-info/SOURCES.txt | 4 + libpytunes.egg-info/requires.txt | 1 + setup.py | 2 +- 11 files changed, 445 insertions(+), 142 deletions(-) create mode 100644 build/lib/libpytunes/Library.py create mode 100644 build/lib/libpytunes/Playlist.py create mode 100644 build/lib/libpytunes/Song.py create mode 100644 build/lib/libpytunes/__init__.py create mode 100644 build/lib/libpytunes/tests/__init__.py create mode 100644 build/lib/libpytunes/tests/test_library.py create mode 100644 dist/libpytunes-1.5.2-py3.10.egg create mode 100644 libpytunes.egg-info/requires.txt 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 0000000000000000000000000000000000000000..08827fcdacc7029970b9f0fd1a2d6747d6476545 GIT binary patch literal 13452 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 WFc|nxD3q6H7ubuO;^&kZ@P7bp&}O{= literal 0 HcmV?d00001 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/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=[] )