From 0a2d1337716194da8a5c8c10117069d18fad94c8 Mon Sep 17 00:00:00 2001 From: mpalazzolo Date: Sun, 16 Dec 2018 18:50:45 -0500 Subject: [PATCH] First Commit --- LICENSE.txt | 15 + README.md | 60 ++++ applemusicpy/__init__.py | 1 + applemusicpy/client.py | 734 +++++++++++++++++++++++++++++++++++++++ docs/Makefile | 20 ++ docs/make.bat | 36 ++ docs/source/.conf.py.swp | Bin 0 -> 16384 bytes docs/source/conf.py | 159 +++++++++ docs/source/index.rst | 78 +++++ setup.py | 16 + tests.py | 215 ++++++++++++ 11 files changed, 1334 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 applemusicpy/__init__.py create mode 100644 applemusicpy/client.py create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/.conf.py.swp create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 setup.py create mode 100644 tests.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d08b16f --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,15 @@ +The MIT License + +Copyright 2018 Matt Palazzolo + +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. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5313a7 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# apple-music-python + +A python wrapper for the Apple Music API. + +See the [Apple Music API documentation](https://developer.apple.com/documentation/applemusicapi/about_the_apple_music_api) for additional info: + +NOTE: This does not support library resources. + +## Getting Started + + +### Prerequisites + +You must have an Apple Developer Account and a MusicKit API Key. See instructions on how to obtain these here: [Getting Keys And Creating Tokens.](https://developer.apple.com/documentation/applemusicapi/getting_keys_and_creating_tokens) + +### Dependencies + +- [Requests](https://github.com/requests/requests) +- [PyJWT](https://github.com/jpadilla/pyjwt) + +### Installing + +``` +python setup.py install +``` + +or + +``` +pip install apple-music-python +``` + +### Example + +```python +import applemusicpy + +secret_key = 'x' +key_id = 'y' +team_id = 'z' + +am = applemusicpy.AppleMusic(secret_key=secret_key, key_id=key_id, team_id=team_id) +results = am.search('travis scott', types=['albums'], limit=5) +for item in results['results']['albums']['data']: + print(item['attributes']['name']) +``` + +## Versioning + +v1.0.0 - Initial Release - 12/15/2018 + +## Authors + +* **Matt Palazzolo** - [GitHub Profile](https://github.com/mpalazzolo) + +## License + +This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details + + diff --git a/applemusicpy/__init__.py b/applemusicpy/__init__.py new file mode 100644 index 0000000..0bb102d --- /dev/null +++ b/applemusicpy/__init__.py @@ -0,0 +1 @@ +from .client import AppleMusic \ No newline at end of file diff --git a/applemusicpy/client.py b/applemusicpy/client.py new file mode 100644 index 0000000..6fb6615 --- /dev/null +++ b/applemusicpy/client.py @@ -0,0 +1,734 @@ +from datetime import datetime, timedelta +import jwt +import requests +from requests.exceptions import HTTPError +import time +import re + + +class AppleMusic: + """ + This class is used to connect to the Apple Music API and make requests for catalog resources + """ + + def __init__(self, secret_key, key_id, team_id, proxies=None, + requests_session=True, max_retries=10, requests_timeout=None, session_length=12): + """ + :param proxies: A dictionary of proxies, if needed + :param secret_key: Secret Key provided by Apple + :param key_id: Key ID provided by Apple + :param team_id: Team ID provided by Apple + :param requests_session: Use request Sessions class. Speeds up API calls significantly when set to True + :param max_retries: Maximum amount of times to retry an API call before stopping + :param requests_timeout: Number of seconds requests should wait before timing out + :param session_length: Length Apple Music token is valid, in hours + """ + + self.proxies = proxies + self._secret_key = secret_key + self._key_id = key_id + self._team_id = team_id + self._alg = 'ES256' # encryption algo that Apple requires + self.token_str = "" # encrypted api token + self.generate_token(session_length) + self.root = 'https://api.music.apple.com/v1/' + self.max_retries = max_retries + self.requests_timeout = requests_timeout + if requests_session: + self._session = requests.Session() + else: + self._session = requests.api # individual calls, slower + + def generate_token(self, session_length): + """ + Generate encrypted token to be used by in API requests. + Set the class token parameter. + + :param session_length: Length Apple Music token is valid, in hours + """ + headers = { + 'alg': self._alg, + 'kid': self._key_id + } + payload = { + 'iss': self._team_id, # issuer + 'iat': int(datetime.now().timestamp()), # issued at + 'exp': int((datetime.now() + timedelta(hours=session_length)).timestamp()) # expiration time + } + token = jwt.encode(payload, self._secret_key, algorithm=self._alg, headers=headers) + self.token_str = token.decode() + + def _auth_headers(self): + """ + Get header for API request + + :return: header in dictionary format + """ + if self.token_str: + return {'Authorization': 'Bearer {}'.format(self.token_str)} + else: + return {} + + def _call(self, method, url, params): + """ + Make a call to the API + + :param method: 'GET', 'POST', 'DELETE', or 'PUT' + :param url: URL of API endpoint + :param params: API paramaters + + :return: JSON data from the API + """ + if not url.startswith('http'): + url = self.root + url + headers = self._auth_headers() + headers['Content-Type'] = 'application/json' + + r = self._session.request(method, url, + headers=headers, + proxies=self.proxies, + params=params, + timeout=self.requests_timeout) + r.raise_for_status() # Check for error + return r.json() + + def _get(self, url, **kwargs): + """ + GET request from the API + + :param url: URL for API endpoint + + :return: JSON data from the API + """ + retries = self.max_retries + delay = 1 + while retries > 0: + try: + return self._call('GET', url, kwargs) + except HTTPError as e: # Retry for some known issues + retries -= 1 + status = e.response.status_code + if status == 429 or (500 <= status < 600): + if retries < 0: + raise + else: + sleep_seconds = int(e.headers.get('Retry-After', delay)) + print('retrying ...' + str(sleep_seconds) + ' secs') + time.sleep(sleep_seconds + 1) + delay += 1 + else: + raise + except Exception as e: + print('exception', str(e)) + retries -= 1 + if retries >= 0: + print('retrying ...' + str(delay) + 'secs') + time.sleep(delay + 1) + delay += 1 + else: + raise + + def _post(self, url, **kwargs): + return self._call('POST', url, kwargs) + + def _delete(self, url, **kwargs): + return self._call('DELETE', url, kwargs) + + def _put(self, url, **kwargs): + return self._call('PUT', url, kwargs) + + def _get_resource(self, resource_id, resource_type, storefront='us', **kwargs): + """ + Get an Apple Music catalog resource (song, artist, album, etc.) + + :param resource_id: ID of resource, from API + :param resource_type: Resource type, (e.g. "songs") + :param storefront: Apple Music Storefront + + :return: JSON data from API + """ + url = self.root + 'catalog/{0}/{1}/{2}'.format(storefront, resource_type, str(resource_id)) + return self._get(url, **kwargs) + + def _get_resource_relationship(self, resource_id, resource_type, relationship, storefront='us', **kwargs): + """ + Get an Apple Music catalog resource relationship (e.g. a song's artist) + + :param resource_id: ID of resource + :param resource_type: Resource type (e.g. "songs") + :param relationship: Relationship type (e.g. "artists") + :param storefront: Apple Music Storefont + + :return: JSON data from API + """ + url = self.root + 'catalog/{0}/{1}/{2}/{3}'.format(storefront, resource_type, str(resource_id), + relationship) + return self._get(url, **kwargs) + + def _get_multiple_resources(self, resource_ids, resource_type, storefront='us', **kwargs): + """ + Get multiple Apple Music catalog resources + + :param resource_ids: List of resource IDs + :param resource_type: Resource type + :param storefront: Apple Music storefront + + :return: JSON data from API + """ + url = self.root + 'catalog/{0}/{1}'.format(storefront, resource_type) + id_string = ','.join(resource_ids) # API format is a string with IDs seperated by commas + return self._get(url, ids=id_string, **kwargs) + + def _get_resource_by_filter(self, filter_type, filter_list, resource_type, resource_ids=None, + storefront='us', **kwargs): + """ + Get mutiple catalog resources using filters + + :param filter_type: Type of filter (e.g. "isrc") + :param filter_list: List of values to filter on + :param resource_type: Resource type + :param resource_ids: List of resource IDs to use in conjunction for additional filtering + :param storefront: Apple Music storefront + + :return: JSON data from API + """ + url = self.root + 'catalog/{0}/{1}'.format(storefront, resource_type) + if resource_ids: + id_string = ','.join(resource_ids) + else: + id_string = None + filter_string = ','.join(filter_list) + filter_param = 'filter[{}]'.format(filter_type) + filter_arg = {filter_param: filter_string} + kwargs.update(filter_arg) + results = self._get(url, ids=id_string, **kwargs) + return results + + # Resources + def album(self, album_id, storefront='us', l=None, include=None): + """ + Get a catalog Album by ID + + :param album_id: Album ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Album data in JSON format + """ + return self._get_resource(album_id, 'albums', storefront=storefront, l=l, include=include) + + def album_relationship(self, album_id, relationship, storefront='us', l=None, limit=None, offset=None): + """ + Get an Album's relationship (e.g. list of tracks, or list of artists) + + :param album_id: Album ID + :param relationship: Relationship type (e.g. "artists") + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(album_id, 'albums', relationship, storefront=storefront, l=l, + limit=limit, offset=offset) + + def albums(self, album_ids, storefront='us', l=None, include=None): + """ + Get all catalog album data associated with the IDs provided + + :param album_ids: a list of album IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog album data in JSON format + """ + return self._get_multiple_resources(album_ids, 'albums', storefront=storefront, l=l, include=include) + + def music_video(self, music_video_id, storefront='us', l=None, include=None): + """ + Get a catalog Music Video by ID + + :param music_video_id: Music Video ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Music Video data in JSON format + """ + return self._get_resource(music_video_id, 'music-videos', storefront=storefront, l=l, include=include) + + def music_video_relationship(self, music_video_id, relationship, storefront='us', l=None, limit=None, offset=None): + """ + Get a Music Videos's relationship (e.g. list of artists) + + :param music_video_id: Music Video ID + :param relationship: Relationship type (e.g. "artists") + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(music_video_id, 'music-videos', relationship, + storefront=storefront, l=l, limit=limit, offset=offset) + + def music_videos(self, music_video_ids, storefront='us', l=None, include=None): + """ + Get all catalog music video data associated with the IDs provided + + :param music_video_ids: a list of music video IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog music video data in JSON format + """ + return self._get_multiple_resources(music_video_ids, 'music-videos', storefront=storefront, l=l, + include=include) + + def music_videos_by_isrc(self, isrcs, music_video_ids=None, storefront='us', l=None, include=None): + """ + Get all catalog music videos associated with the ISRCs provided + + :param isrcs: list of ISRCs + :param music_video_ids: IDs of music videos for additional filtering in conjunction with ISRC + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog music video data in JSON format + """ + return self._get_resource_by_filter('isrc', isrcs, 'music-videos', resource_ids=music_video_ids, + storefront=storefront, l=l, include=include) + + def playlist(self, playlist_id, storefront='us', l=None, include=None): + """ + Get a catalog Playlist by ID + + :param playlist_id: Playlist ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Playlist data in JSON format + """ + return self._get_resource(playlist_id, 'playlists', storefront=storefront, l=l, include=include) + + def playlist_relationship(self, playlist_id, relationship, storefront='us', l=None, limit=None, offset=None): + """ + Get a Playlists's relationship (e.g. list of tracks) + + :param playlist_id: Playlist ID + :param relationship: Relationship type (e.g. "tracks") + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(playlist_id, 'playlists', relationship, storefront=storefront, + l=l, limit=limit, offset=offset) + + def playlists(self, playlist_ids, storefront='us', l=None, include=None): + """ + Get all catalog album data associated with the IDs provided + + :param playlist_ids: a list of playlist IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog playlist data in JSON format + """ + return self._get_multiple_resources(playlist_ids, 'playlists', storefront=storefront, l=l, + include=include) + + def song(self, song_id, storefront='us', l=None, include=None): + """ + Get a catalog Song by ID + + :param song_id: Song ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Song data in JSON format + """ + return self._get_resource(song_id, 'songs', storefront=storefront, l=l, include=include) + + def song_relationship(self, song_id, relationship, storefront='us', l=None, limit=None, offset=None): + """ + Get a Song's relationship (e.g. artist) + + :param song_id: Song ID + :param relationship: Relationship type (e.g. "artists") + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(song_id, 'songs', relationship, storefront=storefront, l=l, + limit=limit, offset=offset) + + def songs(self, song_ids, storefront='us', l=None, include=None): + """ + Get all catalog song data associated with the IDs provided + + :param song_ids: a list of song IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog song data in JSON format + """ + return self._get_multiple_resources(song_ids, 'songs', storefront=storefront, l=l, include=include) + + def songs_by_isrc(self, isrcs, song_ids=None, storefront='us', l=None, include=None): + """ + Get all catalog songs associated with the ISRCs provided + + :param isrcs: list of ISRCs + :param song_ids: IDs of songs for additional filtering in conjunction with ISRC + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog song data in JSON format + """ + return self._get_resource_by_filter('isrc', isrcs, 'songs', resource_ids=song_ids, + storefront=storefront, l=l, include=include) + + def artist(self, artist_id, storefront='us', l=None, include=None): + """ + Get a catalog Artist by ID + + :param artist_id: Artist ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Artist data in JSON format + """ + return self._get_resource(artist_id, 'artists', storefront=storefront, l=l, include=include) + + def artist_relationship(self, artist_id, relationship, storefront='us', l=None, limit=None, offset=None): + """ + Get a Artist's relationship (e.g. song) + + :param artist_id: Artist ID + :param relationship: Relationship type (e.g. "songs") + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(artist_id, 'artists', relationship, storefront=storefront, + l=l, limit=limit, offset=offset) + + def artists(self, artist_ids, storefront='us', l=None, include=None): + """ + Get all catalog artist data associated with the IDs provided + + :param artist_ids: a list of artist IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog artist data in JSON format + """ + return self._get_multiple_resources(artist_ids, 'artists', storefront=storefront, l=l, include=include) + + def station(self, station_id, storefront='us', l=None, include=None): + """ + Get a catalog Station by ID + + :param station_id: Station ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Station data in JSON format + """ + return self._get_resource(station_id, 'stations', storefront=storefront, l=l, include=include) + + def stations(self, station_ids, storefront='us', l=None, include=None): + """ + Get all catalog station data associated with the IDs provided + + :param station_ids: a list of station IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog station data in JSON format + """ + return self._get_multiple_resources(station_ids, 'stations', storefront=storefront, + l=l, include=include) + + def curator(self, curator_id, storefront='us', l=None, include=None): + """ + Get a catalog Curator by ID + + :param curator_id: Curator ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Curator data in JSON format + """ + return self._get_resource(curator_id, 'curators', storefront=storefront, l=l, include=include) + + def curator_relationship(self, curator_id, relationship, storefront='us', l=None, limit=None, offset=None): + """ + Get a Curator's relationship (e.g. playlists) + + :param curator_id: Curator ID + :param relationship: Relationship type (e.g. "playlists") + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(curator_id, 'curators', relationship, storefront=storefront, + l=l, limit=limit, offset=offset) + + def curators(self, curator_ids, storefront='us', l=None, include=None): + """ + Get all curator album data associated with the IDs provided + + :param curator_ids: a list of curator IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog curator data in JSON format + """ + return self._get_multiple_resources(curator_ids, 'curators', storefront=storefront, l=l, + include=include) + + def activity(self, activity_id, storefront='us', l=None, include=None): + """ + Get a catalog Activity by ID + + :param activity_id: Activity ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Activity data in JSON format + """ + return self._get_resource(activity_id, 'activities', storefront=storefront, l=l, include=include) + + def activity_relationship(self, activity_id, relationship, storefront='us', limit=None, offset=None): + """ + Get an Activity's relationship (e.g. playlists) + + :param activity_id: Activity ID + :param relationship: Relationship type (e.g. "playlists") + :param storefront: Apple Music store front + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(activity_id, 'activities', relationship, storefront=storefront, + limit=limit, offset=offset) + + def activities(self, activity_ids, storefront='us', l=None, include=None): + """ + Get all catalog activity data associated with the IDs provided + + :param activity_ids: a list of activity IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog activity data in JSON format + """ + return self._get_multiple_resources(activity_ids, 'activities', storefront=storefront, l=l, + include=include) + + def apple_curator(self, apple_curator_id, storefront='us', l=None, include=None): + """ + Get a catalog Apple Curator by ID + + :param apple_curator_id: Apple Curator ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: Apple Curator data in JSON format + """ + return self._get_resource(apple_curator_id, 'apple-curators', storefront=storefront, l=l, + include=include) + + def apple_curator_relationship(self, apple_curator_id, relationship, storefront='us', l=None, limit=None, + offset=None): + """ + Get an Apple Curator's relationship (e.g. playlists) + + :param apple_curator_id: Apple Curator ID + :param relationship: Relationship type (e.g. "playlists") + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A List of relationship data in JSON format + """ + return self._get_resource_relationship(apple_curator_id, 'apple-curators', relationship, + storefront=storefront, l=l, limit=limit, offset=offset) + + def apple_curators(self, apple_curator_ids, storefront='us', l=None, include=None): + """ + Get all catalog apple curator data associated with the IDs provided + + :param apple_curator_ids: a list of apple curator IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param include: Additional relationships to include in the fetch. Check API documentation. + + :return: A list of catalog apple curator data in JSON format + """ + return self._get_multiple_resources(apple_curator_ids, 'apple-curators', storefront=storefront, l=l, + include=include) + + def genre(self, genre_id, storefront='us', l=None): + """ + Get a catalog Genre by ID + + :param genre_id: Genre ID + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + + :return: Genre data in JSON format + """ + return self._get_resource(genre_id, 'genres', storefront=storefront, l=l) + + # THIS IS LISTED IN APPLE API, BUT DOESN'T SEEM TO WORK + # def genre_relationship(self, genre_id, relationship, storefront='us', l=None, limit=None, offset=None): + # return self._get_resource_relationship(genre_id, 'genres', relationship, storefront=storefront, + # l=l, limit=limit, offset=offset) + + def genres(self, genre_ids, storefront='us', l=None): + """ + Get all catalog genre data associated with the IDs provided + + :param genre_ids: a list of genre IDs + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + + :return: A list of catalog genre data in JSON format + """ + return self._get_multiple_resources(genre_ids, 'genres', storefront=storefront, l=l) + + def genres_all(self, storefront='us', l=None, limit=None, offset=None): + """ + Get all genres + + :param storefront: Apple Music Storefront + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A list of genre data in JSON format + """ + url = self.root + 'catalog/{}/genres'.format(storefront) + return self._get(url, l=l, limit=limit, offset=offset) + + # Storefronts + def storefront(self, storefront_id, l=None): + """ + Get a Storefront by ID + + :param storefront_id: Storefont ID + :param l: The localization to use, specified by a language tag. Check API documentation. + + :return: Storefront data in JSON format + """ + url = self.root + 'storefronts/{}'.format(storefront_id) + return self._get(url, l=l) + + def storefronts(self, storefront_ids, l=None): + """ + Get all storefront data associated with the IDs provided + + :param storefront_ids: a list of storefront IDs + :param l: The localization to use, specified by a language tag. Check API documentation. + + :return: A list of storefront data in JSON format + """ + url = self.root + 'storefronts' + id_string = ','.join(storefront_ids) + return self._get(url, ids=id_string, l=l) + + def storefronts_all(self, l=None, limit=None, offset=None): + """ + Get all storefronts + + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A list of storefront data in JSON format + """ + url = self.root + 'storefronts' + return self._get(url, l=l, limit=limit, offset=offset) + + # Search + def search(self, term, storefront='us', l=None, limit=None, offset=None, types=None, hints=False): + """ + Query the Apple Music API based on a search term + + :param term: Search term + :param storefront: Apple Music store front + :param l: The localization to use, specified by a language tag. Check API documentation. + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + :param types: A list of resource types to return (e.g. songs, artists, etc.) + :param hints: Include search hints + + :return: The search results in JSON format + """ + url = self.root + 'catalog/{}/search'.format(storefront) + if hints: + url += '/hints' + term = re.sub(' +', '+', term) + if types: + type_str = ','.join(types) + else: + type_str = None + return self._get(url, term=term, l=l, limit=limit, offset=offset, types=type_str) + + # Charts + def charts(self, storefront='us', chart=None, types=None, l=None, genre=None, limit=None, offset=None): + """ + Get Apple Music Chart data + + :param storefront: Apple Music store front + :param chart: Chart ID + :param types: List of resource types (e.g. songs, albums, etc.) + :param l: The localization to use, specified by a language tag. Check API documentation. + :param genre: The genre of the chart + :param limit: The maximum amount of items to return + :param offset: The index of the first item returned + + :return: A list of chart data in JSON format + """ + url = self.root + 'catalog/{}/charts'.format(storefront) + if types: + type_str = ','.join(types) + else: + type_str = None + return self._get(url, types=type_str, chart=chart, l=l, genre=genre, limit=limit, offset=offset) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..581467a --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = apple-music-python +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..8f38a9c --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=apple-music-python + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/.conf.py.swp b/docs/source/.conf.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..14bde08a1af0728026a47e6ede1dec3d1502283c GIT binary patch literal 16384 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 literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..1e95710 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) + + +# -- Project information ----------------------------------------------------- + +project = 'apple-music-python' +copyright = '2018, Matt Palazzolo' +author = 'Matt Palazzolo' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'apple-music-pythondoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'apple-music-python.tex', 'apple-music-python Documentation', + 'Matt Palazzolo', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'apple-music-python', 'apple-music-python Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'apple-music-python', 'apple-music-python Documentation', + author, 'apple-music-python', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Extension configuration ------------------------------------------------- \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..bac870b --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,78 @@ +Welcome to apple-music-python's documentation! +============================================== + +A python wrapper for the Apple Music API. + +See the `Apple Music API documentation `_ for additional info: + +**NOTE:** This does not support library resources. + +Prerequisites +^^^^^^^^^^^^^ + +You must have an Apple Developer Account and a MusicKit API Key. +See instructions on how to obtain these here: `Getting Keys And Creating Tokens `_. + +Dependencies +^^^^^^^^^^^^ + +* `Requests `_ +* `PyJWT `_ + +Installation +^^^^^^^^^^^^ +:: + + python setup.py install + +or:: + + pip install apple-music-python + +Example +^^^^^^^ +:: + + import applemusicpy + + secret_key = 'x' + key_id = 'y' + team_id = 'z' + + am = applemusicpy.AppleMusic(secret_key, key_id, team_id) + results = am.search('travis scott', types=['albums'], limit=5) + + for item in results['results']['albums']['data']: + print(item['attributes']['name']) + +:mod:`client` Module +^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: applemusicpy.client + :members: + :special-members: __init__ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +Versioning +^^^^^^^^^^ + +v1.0.0 - Initial Release - 12/15/2018 + +Authors +^^^^^^^ + +* **Matt Palazzolo** - `GitHub Profile `_ + +License +^^^^^^^ +https://github.com/mpalazzolo/applemusicpython/LICENSE.txt + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1b947ff --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name='apple-music-python', + version='1.0.0', + packages=['applemusicpy'], + url='', + license='LICENSE.txt', + author='Matt Palazzolo', + author_email='mattpalazzolo@gmail.com', + description='A python wrapper for the Apple Music API', + install_requires=[ + 'requests>=2.18.4', + 'jwt>=0.5.4', + ], +) diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..d59410b --- /dev/null +++ b/tests.py @@ -0,0 +1,215 @@ +from applemusicpy import AppleMusic +import unittest + + +class TestApple(unittest.TestCase): + + def setUp(self): + # albums + self.born_to_run = '310730204' + self.ready_to_die = '204669326' + # music videos + self.rubber_soul = '401135199' + self.sgt_pepper = '401147268' + # ISRC + self.gods_plan_isrc = 'USCM51800004' + # playlists + self.janet_jackson = 'pl.acc464c750b94302b8806e5fcbe56e17' + self.eighties_pop = 'pl.97c6f95b0b884bedbcce117f9ea5d54b' + # songs + self.xo_tour_life = '1274153124' + self.new_patek = '1436530704' + # artists + self.lil_pump = '1129587661' + self.smokepurpp = '1122104172' + # stations + self.alt = 'ra.985484166' + self.pure_pop = 'ra.686227433' + # curators + self.large_up = '1107687517' + self.grand_ole_opry = '976439448' + # activity + self.party = '976439514' + self.chill = '976439503' + # apple curators + self.apple_alt = '976439526' + self.live_nation_tv = '1017168810' + # genres + self.pop = '14' + self.rock = '21' + # storefronts + self.us = 'us' + self.jp = 'jp' + # search + self.search_term = 'nice for what' + + def test_album(self): + results = am.album(self.born_to_run) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Born to Run') + + def test_album_relationship(self): + results = am.album_relationship(self.born_to_run, 'artists') + self.assertTrue(results['data'][0]['attributes']['name'] == 'Bruce Springsteen') + + def test_albums(self): + results = am.albums([self.born_to_run, self.ready_to_die]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'albums') + + def test_music_video(self): + results = am.music_video(self.rubber_soul) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Rubber Soul (Documentary)') + + def test_music_video_relationship(self): + results = am.music_video_relationship(self.rubber_soul, 'artists') + self.assertTrue(results['data'][0]['attributes']['name'] == 'The Beatles') + + def test_music_videos(self): + results = am.music_videos([self.rubber_soul, self.sgt_pepper]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'music-videos') + + # ISRCs don't seem to work for music videos + # def test_music_videos_by_isrc(self): + + def test_playlist(self): + results = am.playlist(self.janet_jackson) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Janet Jackson: No.1 Songs') + + def test_playlist_relationship(self): + results = am.playlist_relationship(self.eighties_pop, 'tracks') # playlist have 'tracks', artists have 'songs' + self.assertTrue(results['data'][0]['type'] == 'songs') + + def test_playlists(self): + results = am.playlists([self.janet_jackson, self.eighties_pop]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'playlists') + + def test_song(self): + results = am.song(self.xo_tour_life) + self.assertTrue(results['data'][0]['attributes']['name'] == 'XO TOUR Llif3') + + def test_song_relationship(self): + results = am.song_relationship(self.xo_tour_life, 'artists') + self.assertTrue(results['data'][0]['attributes']['name'] == 'Lil Uzi Vert') + + def test_songs(self): + results = am.songs([self.xo_tour_life, self.new_patek]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'songs') + + def test_songs_by_isrc(self): + results = am.songs_by_isrc([self.gods_plan_isrc]) + self.assertTrue(results['data'][0]['attributes']['name'] == 'God\'s Plan') + + def test_artist(self): + results = am.artist(self.lil_pump) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Lil Pump') + + def test_artist_relationship(self): + results = am.artist_relationship(self.lil_pump, 'songs') + self.assertTrue(results['data'][0]['type'] == 'songs') + + def test_artists(self): + results = am.artists([self.lil_pump, self.smokepurpp]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'artists') + + def test_station(self): + results = am.station(self.alt) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Alternative') + + def test_stations(self): + results = am.stations([self.alt, self.pure_pop]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'stations') + + def test_curator(self): + results = am.curator(self.large_up) + self.assertTrue(results['data'][0]['attributes']['name'] == 'LargeUp') + + def test_curator_relationship(self): + results = am.curator_relationship(self.grand_ole_opry, 'playlists') + self.assertTrue(results['data'][0]['type'] == 'playlists') + + def test_curators(self): + results = am.curators([self.large_up, self.grand_ole_opry]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'curators') + + def test_activity(self): + results = am.activity(self.party) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Party') + + def test_activity_relationship(self): + results = am.activity_relationship(self.party, 'playlists') + self.assertTrue(results['data'][0]['type'] == 'playlists') + + def test_activities(self): + results = am.activities([self.party, self.chill]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'activities') + + def test_apple_curator(self): + results = am.apple_curator(self.apple_alt) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Apple Music Alternative') + + def test_apple_curator_relationship(self): + results = am.apple_curator_relationship(self.apple_alt, 'playlists') + self.assertTrue(results['data'][0]['type'] == 'playlists') + + def test_apple_curators(self): + results = am.apple_curators([self.apple_alt, self.live_nation_tv]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'apple-curators') + + def test_genre(self): + results = am.genre(self.pop) + self.assertTrue(results['data'][0]['attributes']['name'] == 'Pop') + + def test_genres(self): + results = am.genres([self.pop, self.rock]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'genres') + + def test_genres_all(self): + results = am.genres_all() + self.assertTrue(results['data'][0]['id'] == '34') + + def test_storefront(self): + results = am.storefront(self.us) + self.assertTrue(results['data'][0]['attributes']['name'] == 'United States') + + def test_storefronts(self): + results = am.storefronts([self.us, self.jp]) + self.assertTrue(len(results['data']) == 2) + self.assertTrue(results['data'][0]['type'] == 'storefronts') + + def test_storefronts_all(self): + results = am.storefronts_all() + self.assertTrue(results['data'][0]['id'] == 'ai') + + def test_search(self): + results = am.search(self.search_term, types=['songs']) + self.assertTrue(results['results']['songs']['data'][0]['attributes']['name'] == 'Nice For What') + + def test_charts(self): + results = am.charts(types=['songs'], genre=self.pop) + self.assertTrue(results['results']['songs'][0]['name'] == 'Top Songs') + + +if __name__ == '__main__': + # These tests require API authorization, so need to read in keys + keys = {} + + with open('private_key.p8', 'r') as f: + keys['secret'] = f.read() + + with open('keys.txt') as f: + for line in f: + name, val = line.partition('=')[::2] + keys[name.strip()] = val.strip() + + am = AppleMusic(secret_key=keys['secret'], key_id=keys['keyID'], team_id=keys['teamID']) + + unittest.main()