├── LICENSE.txt ├── README.md ├── applemusicpy ├── __init__.py └── client.py ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── index.rst │ └── requirements.txt ├── setup.py └── tests.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2018 Matt Palazzolo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apple-music-python 2 | 3 | A python wrapper for the Apple Music API. 4 | 5 | See the [Apple Music API documentation](https://developer.apple.com/documentation/applemusicapi/about_the_apple_music_api) for additional info. 6 | 7 | NOTE: This does not support library resources. 8 | 9 | ## Getting Started 10 | 11 | ### Documentation 12 | Find full documentation of the project here: 13 | https://apple-music-python.readthedocs.io 14 | 15 | ### Prerequisites 16 | 17 | 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) 18 | 19 | ### Dependencies 20 | 21 | - [Requests](https://github.com/requests/requests) 22 | - [PyJWT](https://github.com/jpadilla/pyjwt) 23 | - [Cryptography](https://github.com/pyca/cryptography) 24 | 25 | ### Installing 26 | 27 | ``` 28 | python setup.py install 29 | ``` 30 | 31 | or 32 | 33 | ``` 34 | pip install apple-music-python 35 | ``` 36 | 37 | ### Example 38 | 39 | ```python 40 | import applemusicpy 41 | 42 | secret_key = 'x' 43 | key_id = 'y' 44 | team_id = 'z' 45 | 46 | am = applemusicpy.AppleMusic(secret_key=secret_key, key_id=key_id, team_id=team_id) 47 | results = am.search('travis scott', types=['albums'], limit=5) 48 | for item in results['results']['albums']['data']: 49 | print(item['attributes']['name']) 50 | ``` 51 | 52 | ## Versioning 53 | 54 | - v1.0.0 - Initial Release - 12/15/2018 55 | - v1.0.1 - Updated package info on PyPI - 12/16/2018 56 | - v1.0.2 - Added Windows search support - 01/21/2019 57 | - v1.0.3 - Fixed error handling of HTTPError - 11/03/2019 58 | - v1.0.4 - Fixed error with reading token - 01/24/2021 59 | - v1.0.5 - Refresh token before request if token is expired - 05/09/2021 60 | 61 | ## Authors 62 | 63 | * **Matt Palazzolo** - [GitHub Profile](https://github.com/mpalazzolo) 64 | 65 | ## License 66 | 67 | This project is licensed under the MIT License - see the [LICENSE.txt](LICENSE.txt) file for details 68 | 69 | 70 | -------------------------------------------------------------------------------- /applemusicpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import AppleMusic -------------------------------------------------------------------------------- /applemusicpy/client.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import jwt 3 | import requests 4 | from requests.exceptions import HTTPError 5 | import time 6 | import re 7 | 8 | 9 | class AppleMusic: 10 | """ 11 | This class is used to connect to the Apple Music API and make requests for catalog resources 12 | """ 13 | 14 | def __init__(self, secret_key, key_id, team_id, proxies=None, 15 | requests_session=True, max_retries=10, requests_timeout=None, session_length=12): 16 | """ 17 | :param proxies: A dictionary of proxies, if needed 18 | :param secret_key: Secret Key provided by Apple 19 | :param key_id: Key ID provided by Apple 20 | :param team_id: Team ID provided by Apple 21 | :param requests_session: Use request Sessions class. Speeds up API calls significantly when set to True 22 | :param max_retries: Maximum amount of times to retry an API call before stopping 23 | :param requests_timeout: Number of seconds requests should wait before timing out 24 | :param session_length: Length Apple Music token is valid, in hours 25 | """ 26 | 27 | self.proxies = proxies 28 | self._secret_key = secret_key 29 | self._key_id = key_id 30 | self._team_id = team_id 31 | self._alg = 'ES256' # encryption algo that Apple requires 32 | self.token_str = "" # encrypted api token 33 | self.session_length = session_length 34 | self.token_valid_until = None 35 | self.generate_token(session_length) 36 | self.root = 'https://api.music.apple.com/v1/' 37 | self.max_retries = max_retries 38 | self.requests_timeout = requests_timeout 39 | if requests_session: 40 | self._session = requests.Session() 41 | else: 42 | self._session = requests.api # individual calls, slower 43 | 44 | def token_is_valid(self): 45 | return datetime.now() <= self.token_valid_until if self.token_valid_until is not None else False 46 | 47 | def generate_token(self, session_length): 48 | """ 49 | Generate encrypted token to be used by in API requests. 50 | Set the class token parameter. 51 | 52 | :param session_length: Length Apple Music token is valid, in hours 53 | """ 54 | token_exp_time = datetime.now() + timedelta(hours=session_length) 55 | headers = { 56 | 'alg': self._alg, 57 | 'kid': self._key_id 58 | } 59 | payload = { 60 | 'iss': self._team_id, # issuer 61 | 'iat': int(datetime.now().timestamp()), # issued at 62 | 'exp': int(token_exp_time.timestamp()) # expiration time 63 | } 64 | self.token_valid_until = token_exp_time 65 | token = jwt.encode(payload, self._secret_key, algorithm=self._alg, headers=headers) 66 | self.token_str = token if type(token) is not bytes else token.decode() 67 | 68 | 69 | def _auth_headers(self): 70 | """ 71 | Get header for API request 72 | 73 | :return: header in dictionary format 74 | """ 75 | if self.token_str: 76 | return {'Authorization': 'Bearer {}'.format(self.token_str)} 77 | else: 78 | return {} 79 | 80 | def _call(self, method, url, params): 81 | """ 82 | Make a call to the API 83 | 84 | :param method: 'GET', 'POST', 'DELETE', or 'PUT' 85 | :param url: URL of API endpoint 86 | :param params: API paramaters 87 | 88 | :return: JSON data from the API 89 | """ 90 | if not url.startswith('http'): 91 | url = self.root + url 92 | 93 | if not self.token_is_valid(): 94 | self.generate_token(self.session_length) 95 | 96 | headers = self._auth_headers() 97 | headers['Content-Type'] = 'application/json' 98 | 99 | r = self._session.request(method, url, 100 | headers=headers, 101 | proxies=self.proxies, 102 | params=params, 103 | timeout=self.requests_timeout) 104 | r.raise_for_status() # Check for error 105 | return r.json() 106 | 107 | def _get(self, url, **kwargs): 108 | """ 109 | GET request from the API 110 | 111 | :param url: URL for API endpoint 112 | 113 | :return: JSON data from the API 114 | """ 115 | retries = self.max_retries 116 | delay = 1 117 | while retries > 0: 118 | try: 119 | return self._call('GET', url, kwargs) 120 | except HTTPError as e: # Retry for some known issues 121 | retries -= 1 122 | status = e.response.status_code 123 | if status == 429 or (500 <= status < 600): 124 | if retries < 0: 125 | raise 126 | else: 127 | print('retrying ...' + str(delay) + ' secs') 128 | time.sleep(delay + 1) 129 | delay += 1 130 | else: 131 | raise 132 | except Exception as e: 133 | print('exception', str(e)) 134 | retries -= 1 135 | if retries >= 0: 136 | print('retrying ...' + str(delay) + 'secs') 137 | time.sleep(delay + 1) 138 | delay += 1 139 | else: 140 | raise 141 | 142 | def _post(self, url, **kwargs): 143 | return self._call('POST', url, kwargs) 144 | 145 | def _delete(self, url, **kwargs): 146 | return self._call('DELETE', url, kwargs) 147 | 148 | def _put(self, url, **kwargs): 149 | return self._call('PUT', url, kwargs) 150 | 151 | def _get_resource(self, resource_id, resource_type, storefront='us', **kwargs): 152 | """ 153 | Get an Apple Music catalog resource (song, artist, album, etc.) 154 | 155 | :param resource_id: ID of resource, from API 156 | :param resource_type: Resource type, (e.g. "songs") 157 | :param storefront: Apple Music Storefront 158 | 159 | :return: JSON data from API 160 | """ 161 | url = self.root + 'catalog/{0}/{1}/{2}'.format(storefront, resource_type, str(resource_id)) 162 | return self._get(url, **kwargs) 163 | 164 | def _get_resource_relationship(self, resource_id, resource_type, relationship, storefront='us', **kwargs): 165 | """ 166 | Get an Apple Music catalog resource relationship (e.g. a song's artist) 167 | 168 | :param resource_id: ID of resource 169 | :param resource_type: Resource type (e.g. "songs") 170 | :param relationship: Relationship type (e.g. "artists") 171 | :param storefront: Apple Music Storefont 172 | 173 | :return: JSON data from API 174 | """ 175 | url = self.root + 'catalog/{0}/{1}/{2}/{3}'.format(storefront, resource_type, str(resource_id), 176 | relationship) 177 | return self._get(url, **kwargs) 178 | 179 | def _get_resource_relationship_view(self, resource_id, resource_type, relationship_view, storefront='us', **kwargs): 180 | """ 181 | Get an Apple Music catalog resource relationship view (e.g. a song's artist) 182 | 183 | :param resource_id: ID of resource 184 | :param resource_type: Resource type (e.g. "songs") 185 | :param relationship_view: Relationship view type (e.g. "related-albums") 186 | :param storefront: Apple Music Storefont 187 | 188 | :return: JSON data from API 189 | """ 190 | url = self.root + 'catalog/{0}/{1}/{2}/view/{3}'.format(storefront, resource_type, str(resource_id), 191 | relationship_view) 192 | return self._get(url, **kwargs) 193 | 194 | def _get_multiple_resources(self, resource_ids, resource_type, storefront='us', **kwargs): 195 | """ 196 | Get multiple Apple Music catalog resources 197 | 198 | :param resource_ids: List of resource IDs 199 | :param resource_type: Resource type 200 | :param storefront: Apple Music storefront 201 | 202 | :return: JSON data from API 203 | """ 204 | url = self.root + 'catalog/{0}/{1}'.format(storefront, resource_type) 205 | id_string = ','.join(resource_ids) # API format is a string with IDs seperated by commas 206 | return self._get(url, ids=id_string, **kwargs) 207 | 208 | def _get_resource_by_filter(self, filter_type, filter_list, resource_type, resource_ids=None, 209 | storefront='us', **kwargs): 210 | """ 211 | Get mutiple catalog resources using filters 212 | 213 | :param filter_type: Type of filter (e.g. "isrc") 214 | :param filter_list: List of values to filter on 215 | :param resource_type: Resource type 216 | :param resource_ids: List of resource IDs to use in conjunction for additional filtering 217 | :param storefront: Apple Music storefront 218 | 219 | :return: JSON data from API 220 | """ 221 | url = self.root + 'catalog/{0}/{1}'.format(storefront, resource_type) 222 | if resource_ids: 223 | id_string = ','.join(resource_ids) 224 | else: 225 | id_string = None 226 | filter_string = ','.join(filter_list) 227 | filter_param = 'filter[{}]'.format(filter_type) 228 | filter_arg = {filter_param: filter_string} 229 | kwargs.update(filter_arg) 230 | results = self._get(url, ids=id_string, **kwargs) 231 | return results 232 | 233 | # Resources 234 | def album(self, album_id, storefront='us', l=None, include=None): 235 | """ 236 | Get a catalog Album by ID 237 | 238 | :param album_id: Album ID 239 | :param storefront: Apple Music Storefront 240 | :param l: The localization to use, specified by a language tag. Check API documentation. 241 | :param include: Additional relationships to include in the fetch. Check API documentation. 242 | 243 | :return: Album data in JSON format 244 | """ 245 | return self._get_resource(album_id, 'albums', storefront=storefront, l=l, include=include) 246 | 247 | def album_relationship(self, album_id, relationship, storefront='us', l=None, limit=None, offset=None): 248 | """ 249 | Get an Album's relationship (e.g. list of tracks, or list of artists) 250 | 251 | :param album_id: Album ID 252 | :param relationship: Relationship type (e.g. "artists") 253 | :param storefront: Apple Music store front 254 | :param l: The localization to use, specified by a language tag. Check API documentation. 255 | :param limit: The maximum amount of items to return 256 | :param offset: The index of the first item returned 257 | 258 | :return: A List of relationship data in JSON format 259 | """ 260 | return self._get_resource_relationship(album_id, 'albums', relationship, storefront=storefront, l=l, 261 | limit=limit, offset=offset) 262 | 263 | def album_relationship_view(self, album_id, relationship_view, storefront='us', l=None, limit=None, offset=None): 264 | """ 265 | Get an Album's relationship (e.g. list of tracks, or list of artists) 266 | 267 | :param album_id: Album ID 268 | :param relationship_view: Relationship view type (e.g. "related-albums") 269 | :param storefront: Apple Music store front 270 | :param l: The localization to use, specified by a language tag. Check API documentation. 271 | :param limit: The maximum amount of items to return 272 | :param offset: The index of the first item returned 273 | 274 | :return: A List of relationship view data in JSON format 275 | """ 276 | return self._get_resource_relationship_view(album_id, 'albums', relationship_view, storefront=storefront, l=l, 277 | limit=limit, offset=offset) 278 | 279 | def albums(self, album_ids, storefront='us', l=None, include=None): 280 | """ 281 | Get all catalog album data associated with the IDs provided 282 | 283 | :param album_ids: a list of album IDs 284 | :param storefront: Apple Music store front 285 | :param l: The localization to use, specified by a language tag. Check API documentation. 286 | :param include: Additional relationships to include in the fetch. Check API documentation. 287 | 288 | :return: A list of catalog album data in JSON format 289 | """ 290 | return self._get_multiple_resources(album_ids, 'albums', storefront=storefront, l=l, include=include) 291 | 292 | def music_video(self, music_video_id, storefront='us', l=None, include=None): 293 | """ 294 | Get a catalog Music Video by ID 295 | 296 | :param music_video_id: Music Video ID 297 | :param storefront: Apple Music Storefront 298 | :param l: The localization to use, specified by a language tag. Check API documentation. 299 | :param include: Additional relationships to include in the fetch. Check API documentation. 300 | 301 | :return: Music Video data in JSON format 302 | """ 303 | return self._get_resource(music_video_id, 'music-videos', storefront=storefront, l=l, include=include) 304 | 305 | def music_video_relationship(self, music_video_id, relationship, storefront='us', l=None, limit=None, offset=None): 306 | """ 307 | Get a Music Videos's relationship (e.g. list of artists) 308 | 309 | :param music_video_id: Music Video ID 310 | :param relationship: Relationship type (e.g. "artists") 311 | :param storefront: Apple Music store front 312 | :param l: The localization to use, specified by a language tag. Check API documentation. 313 | :param limit: The maximum amount of items to return 314 | :param offset: The index of the first item returned 315 | 316 | :return: A List of relationship data in JSON format 317 | """ 318 | return self._get_resource_relationship(music_video_id, 'music-videos', relationship, 319 | storefront=storefront, l=l, limit=limit, offset=offset) 320 | 321 | def music_video_relationship_view(self, music_video_id, relationship_view, 322 | storefront='us', l=None, limit=None, offset=None): 323 | """ 324 | Get a Music Videos's relationship view(e.g. list of artists) 325 | 326 | :param music_video_id: Music Video ID 327 | :param relationship_view: Relationship view type (e.g. "more-by-artist") 328 | :param storefront: Apple Music store front 329 | :param l: The localization to use, specified by a language tag. Check API documentation. 330 | :param limit: The maximum amount of items to return 331 | :param offset: The index of the first item returned 332 | 333 | :return: A List of relationship view data in JSON format 334 | """ 335 | return self._get_resource_relationship_view(music_video_id, 'music-videos', relationship_view, 336 | storefront=storefront, l=l, limit=limit, offset=offset) 337 | 338 | def music_videos(self, music_video_ids, storefront='us', l=None, include=None): 339 | """ 340 | Get all catalog music video data associated with the IDs provided 341 | 342 | :param music_video_ids: a list of music video IDs 343 | :param storefront: Apple Music store front 344 | :param l: The localization to use, specified by a language tag. Check API documentation. 345 | :param include: Additional relationships to include in the fetch. Check API documentation. 346 | 347 | :return: A list of catalog music video data in JSON format 348 | """ 349 | return self._get_multiple_resources(music_video_ids, 'music-videos', storefront=storefront, l=l, 350 | include=include) 351 | 352 | def music_videos_by_isrc(self, isrcs, music_video_ids=None, storefront='us', l=None, include=None): 353 | """ 354 | Get all catalog music videos associated with the ISRCs provided 355 | 356 | :param isrcs: list of ISRCs 357 | :param music_video_ids: IDs of music videos for additional filtering in conjunction with ISRC 358 | :param storefront: Apple Music store front 359 | :param l: The localization to use, specified by a language tag. Check API documentation. 360 | :param include: Additional relationships to include in the fetch. Check API documentation. 361 | 362 | :return: A list of catalog music video data in JSON format 363 | """ 364 | return self._get_resource_by_filter('isrc', isrcs, 'music-videos', resource_ids=music_video_ids, 365 | storefront=storefront, l=l, include=include) 366 | 367 | def playlist(self, playlist_id, storefront='us', l=None, include=None): 368 | """ 369 | Get a catalog Playlist by ID 370 | 371 | :param playlist_id: Playlist ID 372 | :param storefront: Apple Music Storefront 373 | :param l: The localization to use, specified by a language tag. Check API documentation. 374 | :param include: Additional relationships to include in the fetch. Check API documentation. 375 | 376 | :return: Playlist data in JSON format 377 | """ 378 | return self._get_resource(playlist_id, 'playlists', storefront=storefront, l=l, include=include) 379 | 380 | def playlist_relationship(self, playlist_id, relationship, storefront='us', l=None, limit=None, offset=None): 381 | """ 382 | Get a Playlists's relationship (e.g. list of tracks) 383 | 384 | :param playlist_id: Playlist ID 385 | :param relationship: Relationship type (e.g. "tracks") 386 | :param storefront: Apple Music store front 387 | :param l: The localization to use, specified by a language tag. Check API documentation. 388 | :param limit: The maximum amount of items to return 389 | :param offset: The index of the first item returned 390 | 391 | :return: A List of relationship data in JSON format 392 | """ 393 | return self._get_resource_relationship(playlist_id, 'playlists', relationship, storefront=storefront, 394 | l=l, limit=limit, offset=offset) 395 | 396 | def playlist_relationship_view(self, playlist_id, relationship_view, storefront='us', l=None, limit=None, offset=None): 397 | """ 398 | Get a Playlists's relationship view(e.g. list of tracks) 399 | 400 | :param playlist_id: Playlist ID 401 | :param relationship: Relationship view type (e.g. "featured-artists") 402 | :param storefront: Apple Music store front 403 | :param l: The localization to use, specified by a language tag. Check API documentation. 404 | :param limit: The maximum amount of items to return 405 | :param offset: The index of the first item returned 406 | 407 | :return: A List of relationship view data in JSON format 408 | """ 409 | return self._get_resource_relationship_view(playlist_id, 'playlists', relationship_view, storefront=storefront, 410 | l=l, limit=limit, offset=offset) 411 | 412 | def playlists(self, playlist_ids, storefront='us', l=None, include=None): 413 | """ 414 | Get all catalog album data associated with the IDs provided 415 | 416 | :param playlist_ids: a list of playlist IDs 417 | :param storefront: Apple Music store front 418 | :param l: The localization to use, specified by a language tag. Check API documentation. 419 | :param include: Additional relationships to include in the fetch. Check API documentation. 420 | 421 | :return: A list of catalog playlist data in JSON format 422 | """ 423 | return self._get_multiple_resources(playlist_ids, 'playlists', storefront=storefront, l=l, 424 | include=include) 425 | 426 | def song(self, song_id, storefront='us', l=None, include=None): 427 | """ 428 | Get a catalog Song by ID 429 | 430 | :param song_id: Song ID 431 | :param storefront: Apple Music Storefront 432 | :param l: The localization to use, specified by a language tag. Check API documentation. 433 | :param include: Additional relationships to include in the fetch. Check API documentation. 434 | 435 | :return: Song data in JSON format 436 | """ 437 | return self._get_resource(song_id, 'songs', storefront=storefront, l=l, include=include) 438 | 439 | def song_relationship(self, song_id, relationship, storefront='us', l=None, limit=None, offset=None): 440 | """ 441 | Get a Song's relationship (e.g. artist) 442 | 443 | :param song_id: Song ID 444 | :param relationship: Relationship type (e.g. "artists") 445 | :param storefront: Apple Music store front 446 | :param l: The localization to use, specified by a language tag. Check API documentation. 447 | :param limit: The maximum amount of items to return 448 | :param offset: The index of the first item returned 449 | 450 | :return: A List of relationship data in JSON format 451 | """ 452 | return self._get_resource_relationship(song_id, 'songs', relationship, storefront=storefront, l=l, 453 | limit=limit, offset=offset) 454 | 455 | def songs(self, song_ids, storefront='us', l=None, include=None): 456 | """ 457 | Get all catalog song data associated with the IDs provided 458 | 459 | :param song_ids: a list of song IDs 460 | :param storefront: Apple Music store front 461 | :param l: The localization to use, specified by a language tag. Check API documentation. 462 | :param include: Additional relationships to include in the fetch. Check API documentation. 463 | 464 | :return: A list of catalog song data in JSON format 465 | """ 466 | return self._get_multiple_resources(song_ids, 'songs', storefront=storefront, l=l, include=include) 467 | 468 | def songs_by_isrc(self, isrcs, song_ids=None, storefront='us', l=None, include=None): 469 | """ 470 | Get all catalog songs associated with the ISRCs provided 471 | 472 | :param isrcs: list of ISRCs 473 | :param song_ids: IDs of songs for additional filtering in conjunction with ISRC 474 | :param storefront: Apple Music store front 475 | :param l: The localization to use, specified by a language tag. Check API documentation. 476 | :param include: Additional relationships to include in the fetch. Check API documentation. 477 | 478 | :return: A list of catalog song data in JSON format 479 | """ 480 | return self._get_resource_by_filter('isrc', isrcs, 'songs', resource_ids=song_ids, 481 | storefront=storefront, l=l, include=include) 482 | 483 | def artist(self, artist_id, storefront='us', l=None, include=None): 484 | """ 485 | Get a catalog Artist by ID 486 | 487 | :param artist_id: Artist ID 488 | :param storefront: Apple Music Storefront 489 | :param l: The localization to use, specified by a language tag. Check API documentation. 490 | :param include: Additional relationships to include in the fetch. Check API documentation. 491 | 492 | :return: Artist data in JSON format 493 | """ 494 | return self._get_resource(artist_id, 'artists', storefront=storefront, l=l, include=include) 495 | 496 | def artist_relationship(self, artist_id, relationship, storefront='us', l=None, limit=None, offset=None): 497 | """ 498 | Get a Artist's relationship (e.g. song) 499 | 500 | :param artist_id: Artist ID 501 | :param relationship: Relationship type (e.g. "songs") 502 | :param storefront: Apple Music store front 503 | :param l: The localization to use, specified by a language tag. Check API documentation. 504 | :param limit: The maximum amount of items to return 505 | :param offset: The index of the first item returned 506 | 507 | :return: A List of relationship data in JSON format 508 | """ 509 | return self._get_resource_relationship(artist_id, 'artists', relationship, storefront=storefront, 510 | l=l, limit=limit, offset=offset) 511 | 512 | def artist_relationship_view(self, artist_id, relationship_view, storefront='us', l=None, limit=None, offset=None): 513 | """ 514 | Get a Artist's relationship (e.g. song) 515 | 516 | :param artist_id: Artist ID 517 | :param relationship_view: Relationship view type (e.g. "top-songs") 518 | :param storefront: Apple Music store front 519 | :param l: The localization to use, specified by a language tag. Check API documentation. 520 | :param limit: The maximum amount of items to return 521 | :param offset: The index of the first item returned 522 | 523 | :return: A List of relationship data in JSON format 524 | """ 525 | return self._get_resource_relationship_view(artist_id, 'artists', relationship_view, storefront=storefront, 526 | l=l, limit=limit, offset=offset) 527 | 528 | def artists(self, artist_ids, storefront='us', l=None, include=None): 529 | """ 530 | Get all catalog artist data associated with the IDs provided 531 | 532 | :param artist_ids: a list of artist IDs 533 | :param storefront: Apple Music store front 534 | :param l: The localization to use, specified by a language tag. Check API documentation. 535 | :param include: Additional relationships to include in the fetch. Check API documentation. 536 | 537 | :return: A list of catalog artist data in JSON format 538 | """ 539 | return self._get_multiple_resources(artist_ids, 'artists', storefront=storefront, l=l, include=include) 540 | 541 | def station(self, station_id, storefront='us', l=None, include=None): 542 | """ 543 | Get a catalog Station by ID 544 | 545 | :param station_id: Station ID 546 | :param storefront: Apple Music Storefront 547 | :param l: The localization to use, specified by a language tag. Check API documentation. 548 | :param include: Additional relationships to include in the fetch. Check API documentation. 549 | 550 | :return: Station data in JSON format 551 | """ 552 | return self._get_resource(station_id, 'stations', storefront=storefront, l=l, include=include) 553 | 554 | def stations(self, station_ids, storefront='us', l=None, include=None): 555 | """ 556 | Get all catalog station data associated with the IDs provided 557 | 558 | :param station_ids: a list of station IDs 559 | :param storefront: Apple Music store front 560 | :param l: The localization to use, specified by a language tag. Check API documentation. 561 | :param include: Additional relationships to include in the fetch. Check API documentation. 562 | 563 | :return: A list of catalog station data in JSON format 564 | """ 565 | return self._get_multiple_resources(station_ids, 'stations', storefront=storefront, 566 | l=l, include=include) 567 | 568 | def curator(self, curator_id, storefront='us', l=None, include=None): 569 | """ 570 | Get a catalog Curator by ID 571 | 572 | :param curator_id: Curator ID 573 | :param storefront: Apple Music Storefront 574 | :param l: The localization to use, specified by a language tag. Check API documentation. 575 | :param include: Additional relationships to include in the fetch. Check API documentation. 576 | 577 | :return: Curator data in JSON format 578 | """ 579 | return self._get_resource(curator_id, 'curators', storefront=storefront, l=l, include=include) 580 | 581 | def curator_relationship(self, curator_id, relationship, storefront='us', l=None, limit=None, offset=None): 582 | """ 583 | Get a Curator's relationship (e.g. playlists) 584 | 585 | :param curator_id: Curator ID 586 | :param relationship: Relationship type (e.g. "playlists") 587 | :param storefront: Apple Music store front 588 | :param l: The localization to use, specified by a language tag. Check API documentation. 589 | :param limit: The maximum amount of items to return 590 | :param offset: The index of the first item returned 591 | 592 | :return: A List of relationship data in JSON format 593 | """ 594 | return self._get_resource_relationship(curator_id, 'curators', relationship, storefront=storefront, 595 | l=l, limit=limit, offset=offset) 596 | 597 | def curators(self, curator_ids, storefront='us', l=None, include=None): 598 | """ 599 | Get all curator album data associated with the IDs provided 600 | 601 | :param curator_ids: a list of curator IDs 602 | :param storefront: Apple Music store front 603 | :param l: The localization to use, specified by a language tag. Check API documentation. 604 | :param include: Additional relationships to include in the fetch. Check API documentation. 605 | 606 | :return: A list of catalog curator data in JSON format 607 | """ 608 | return self._get_multiple_resources(curator_ids, 'curators', storefront=storefront, l=l, 609 | include=include) 610 | 611 | def activity(self, activity_id, storefront='us', l=None, include=None): 612 | """ 613 | Get a catalog Activity by ID 614 | 615 | :param activity_id: Activity ID 616 | :param storefront: Apple Music Storefront 617 | :param l: The localization to use, specified by a language tag. Check API documentation. 618 | :param include: Additional relationships to include in the fetch. Check API documentation. 619 | 620 | :return: Activity data in JSON format 621 | """ 622 | return self._get_resource(activity_id, 'activities', storefront=storefront, l=l, include=include) 623 | 624 | def activity_relationship(self, activity_id, relationship, storefront='us', limit=None, offset=None): 625 | """ 626 | Get an Activity's relationship (e.g. playlists) 627 | 628 | :param activity_id: Activity ID 629 | :param relationship: Relationship type (e.g. "playlists") 630 | :param storefront: Apple Music store front 631 | :param limit: The maximum amount of items to return 632 | :param offset: The index of the first item returned 633 | 634 | :return: A List of relationship data in JSON format 635 | """ 636 | return self._get_resource_relationship(activity_id, 'activities', relationship, storefront=storefront, 637 | limit=limit, offset=offset) 638 | 639 | def activities(self, activity_ids, storefront='us', l=None, include=None): 640 | """ 641 | Get all catalog activity data associated with the IDs provided 642 | 643 | :param activity_ids: a list of activity IDs 644 | :param storefront: Apple Music store front 645 | :param l: The localization to use, specified by a language tag. Check API documentation. 646 | :param include: Additional relationships to include in the fetch. Check API documentation. 647 | 648 | :return: A list of catalog activity data in JSON format 649 | """ 650 | return self._get_multiple_resources(activity_ids, 'activities', storefront=storefront, l=l, 651 | include=include) 652 | 653 | def apple_curator(self, apple_curator_id, storefront='us', l=None, include=None): 654 | """ 655 | Get a catalog Apple Curator by ID 656 | 657 | :param apple_curator_id: Apple Curator ID 658 | :param storefront: Apple Music Storefront 659 | :param l: The localization to use, specified by a language tag. Check API documentation. 660 | :param include: Additional relationships to include in the fetch. Check API documentation. 661 | 662 | :return: Apple Curator data in JSON format 663 | """ 664 | return self._get_resource(apple_curator_id, 'apple-curators', storefront=storefront, l=l, 665 | include=include) 666 | 667 | def apple_curator_relationship(self, apple_curator_id, relationship, storefront='us', l=None, limit=None, 668 | offset=None): 669 | """ 670 | Get an Apple Curator's relationship (e.g. playlists) 671 | 672 | :param apple_curator_id: Apple Curator ID 673 | :param relationship: Relationship type (e.g. "playlists") 674 | :param storefront: Apple Music store front 675 | :param l: The localization to use, specified by a language tag. Check API documentation. 676 | :param limit: The maximum amount of items to return 677 | :param offset: The index of the first item returned 678 | 679 | :return: A List of relationship data in JSON format 680 | """ 681 | return self._get_resource_relationship(apple_curator_id, 'apple-curators', relationship, 682 | storefront=storefront, l=l, limit=limit, offset=offset) 683 | 684 | def apple_curators(self, apple_curator_ids, storefront='us', l=None, include=None): 685 | """ 686 | Get all catalog apple curator data associated with the IDs provided 687 | 688 | :param apple_curator_ids: a list of apple curator IDs 689 | :param storefront: Apple Music store front 690 | :param l: The localization to use, specified by a language tag. Check API documentation. 691 | :param include: Additional relationships to include in the fetch. Check API documentation. 692 | 693 | :return: A list of catalog apple curator data in JSON format 694 | """ 695 | return self._get_multiple_resources(apple_curator_ids, 'apple-curators', storefront=storefront, l=l, 696 | include=include) 697 | 698 | def genre(self, genre_id, storefront='us', l=None): 699 | """ 700 | Get a catalog Genre by ID 701 | 702 | :param genre_id: Genre ID 703 | :param storefront: Apple Music Storefront 704 | :param l: The localization to use, specified by a language tag. Check API documentation. 705 | 706 | :return: Genre data in JSON format 707 | """ 708 | return self._get_resource(genre_id, 'genres', storefront=storefront, l=l) 709 | 710 | # THIS IS LISTED IN APPLE API, BUT DOESN'T SEEM TO WORK 711 | # def genre_relationship(self, genre_id, relationship, storefront='us', l=None, limit=None, offset=None): 712 | # return self._get_resource_relationship(genre_id, 'genres', relationship, storefront=storefront, 713 | # l=l, limit=limit, offset=offset) 714 | 715 | def genres(self, genre_ids, storefront='us', l=None): 716 | """ 717 | Get all catalog genre data associated with the IDs provided 718 | 719 | :param genre_ids: a list of genre IDs 720 | :param storefront: Apple Music store front 721 | :param l: The localization to use, specified by a language tag. Check API documentation. 722 | 723 | :return: A list of catalog genre data in JSON format 724 | """ 725 | return self._get_multiple_resources(genre_ids, 'genres', storefront=storefront, l=l) 726 | 727 | def genres_all(self, storefront='us', l=None, limit=None, offset=None): 728 | """ 729 | Get all genres 730 | 731 | :param storefront: Apple Music Storefront 732 | :param l: The localization to use, specified by a language tag. Check API documentation. 733 | :param limit: The maximum amount of items to return 734 | :param offset: The index of the first item returned 735 | 736 | :return: A list of genre data in JSON format 737 | """ 738 | url = self.root + 'catalog/{}/genres'.format(storefront) 739 | return self._get(url, l=l, limit=limit, offset=offset) 740 | 741 | # Storefronts 742 | def storefront(self, storefront_id, l=None): 743 | """ 744 | Get a Storefront by ID 745 | 746 | :param storefront_id: Storefont ID 747 | :param l: The localization to use, specified by a language tag. Check API documentation. 748 | 749 | :return: Storefront data in JSON format 750 | """ 751 | url = self.root + 'storefronts/{}'.format(storefront_id) 752 | return self._get(url, l=l) 753 | 754 | def storefronts(self, storefront_ids, l=None): 755 | """ 756 | Get all storefront data associated with the IDs provided 757 | 758 | :param storefront_ids: a list of storefront IDs 759 | :param l: The localization to use, specified by a language tag. Check API documentation. 760 | 761 | :return: A list of storefront data in JSON format 762 | """ 763 | url = self.root + 'storefronts' 764 | id_string = ','.join(storefront_ids) 765 | return self._get(url, ids=id_string, l=l) 766 | 767 | def storefronts_all(self, l=None, limit=None, offset=None): 768 | """ 769 | Get all storefronts 770 | 771 | :param l: The localization to use, specified by a language tag. Check API documentation. 772 | :param limit: The maximum amount of items to return 773 | :param offset: The index of the first item returned 774 | 775 | :return: A list of storefront data in JSON format 776 | """ 777 | url = self.root + 'storefronts' 778 | return self._get(url, l=l, limit=limit, offset=offset) 779 | 780 | # Search 781 | def search(self, term, storefront='us', l=None, limit=None, offset=None, types=None, hints=False, os='linux'): 782 | """ 783 | Query the Apple Music API based on a search term 784 | 785 | :param term: Search term 786 | :param storefront: Apple Music store front 787 | :param l: The localization to use, specified by a language tag. Check API documentation. 788 | :param limit: The maximum amount of items to return 789 | :param offset: The index of the first item returned 790 | :param types: A list of resource types to return (e.g. songs, artists, etc.) 791 | :param hints: Include search hints 792 | :param os: Operating System being used. If search isn't working on Windows, try os='windows'. 793 | 794 | :return: The search results in JSON format 795 | """ 796 | url = self.root + 'catalog/{}/search'.format(storefront) 797 | if hints: 798 | url += '/hints' 799 | term = re.sub(' +', '+', term) 800 | if types: 801 | type_str = ','.join(types) 802 | else: 803 | type_str = None 804 | 805 | if os == 'linux': 806 | return self._get(url, term=term, l=l, limit=limit, offset=offset, types=type_str) 807 | elif os == 'windows': 808 | params = { 809 | 'term': term, 810 | 'limit': limit, 811 | 'offset': offset, 812 | 'types': type_str 813 | } 814 | 815 | # The params parameter in requests converts '+' to '%2b' 816 | # On some Windows computers, this breaks the API request, so generate full URL instead 817 | param_string = '?' 818 | for param, value in params.items(): 819 | if value is None: 820 | continue 821 | param_string = param_string + str(param) + '=' + str(value) + '&' 822 | param_string = param_string[:len(param_string) - 1] # This removes the last trailing '&' 823 | 824 | return self._get(url + param_string) 825 | else: 826 | return None 827 | 828 | 829 | 830 | # Charts 831 | def charts(self, storefront='us', chart=None, types=None, l=None, genre=None, limit=None, offset=None): 832 | """ 833 | Get Apple Music Chart data 834 | 835 | :param storefront: Apple Music store front 836 | :param chart: Chart ID 837 | :param types: List of resource types (e.g. songs, albums, etc.) 838 | :param l: The localization to use, specified by a language tag. Check API documentation. 839 | :param genre: The genre of the chart 840 | :param limit: The maximum amount of items to return 841 | :param offset: The index of the first item returned 842 | 843 | :return: A list of chart data in JSON format 844 | """ 845 | url = self.root + 'catalog/{}/charts'.format(storefront) 846 | if types: 847 | type_str = ','.join(types) 848 | else: 849 | type_str = None 850 | return self._get(url, types=type_str, chart=chart, l=l, genre=genre, limit=limit, offset=offset) 851 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = apple-music-python 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=apple-music-python 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../..')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'apple-music-python' 23 | copyright = '2018, Matt Palazzolo' 24 | author = 'Matt Palazzolo' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | # 60 | # This is also used if you do content translation via gettext catalogs. 61 | # Usually you set "language" from the command line for these cases. 62 | language = None 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | # This pattern also affects html_static_path and html_extra_path . 67 | exclude_patterns = [] 68 | 69 | # The name of the Pygments (syntax highlighting) style to use. 70 | pygments_style = 'sphinx' 71 | 72 | 73 | # -- Options for HTML output ------------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | # 78 | html_theme = "sphinx_rtd_theme" 79 | 80 | # Theme options are theme-specific and customize the look and feel of a theme 81 | # further. For a list of options available for each theme, see the 82 | # documentation. 83 | # 84 | # html_theme_options = {} 85 | 86 | # Add any paths that contain custom static files (such as style sheets) here, 87 | # relative to this directory. They are copied after the builtin static files, 88 | # so a file named "default.css" will overwrite the builtin "default.css". 89 | html_static_path = ['_static'] 90 | 91 | # Custom sidebar templates, must be a dictionary that maps document names 92 | # to template names. 93 | # 94 | # The default sidebars (for documents that don't match any pattern) are 95 | # defined by theme itself. Builtin themes are using these templates by 96 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 97 | # 'searchbox.html']``. 98 | # 99 | # html_sidebars = {} 100 | 101 | 102 | # -- Options for HTMLHelp output --------------------------------------------- 103 | 104 | # Output file base name for HTML help builder. 105 | htmlhelp_basename = 'apple-music-pythondoc' 106 | 107 | 108 | # -- Options for LaTeX output ------------------------------------------------ 109 | 110 | latex_elements = { 111 | # The paper size ('letterpaper' or 'a4paper'). 112 | # 113 | # 'papersize': 'letterpaper', 114 | 115 | # The font size ('10pt', '11pt' or '12pt'). 116 | # 117 | # 'pointsize': '10pt', 118 | 119 | # Additional stuff for the LaTeX preamble. 120 | # 121 | # 'preamble': '', 122 | 123 | # Latex figure (float) alignment 124 | # 125 | # 'figure_align': 'htbp', 126 | } 127 | 128 | # Grouping the document tree into LaTeX files. List of tuples 129 | # (source start file, target name, title, 130 | # author, documentclass [howto, manual, or own class]). 131 | latex_documents = [ 132 | (master_doc, 'apple-music-python.tex', 'apple-music-python Documentation', 133 | 'Matt Palazzolo', 'manual'), 134 | ] 135 | 136 | 137 | # -- Options for manual page output ------------------------------------------ 138 | 139 | # One entry per manual page. List of tuples 140 | # (source start file, name, description, authors, manual section). 141 | man_pages = [ 142 | (master_doc, 'apple-music-python', 'apple-music-python Documentation', 143 | [author], 1) 144 | ] 145 | 146 | 147 | # -- Options for Texinfo output ---------------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'apple-music-python', 'apple-music-python Documentation', 154 | author, 'apple-music-python', 'One line description of project.', 155 | 'Miscellaneous'), 156 | ] 157 | 158 | 159 | # -- Extension configuration ------------------------------------------------- -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to apple-music-python's documentation! 2 | ============================================== 3 | 4 | A python wrapper for the Apple Music API. 5 | 6 | See the `Apple Music API documentation `_ for additional info: 7 | 8 | **NOTE:** This does not support library resources. 9 | 10 | Prerequisites 11 | ^^^^^^^^^^^^^ 12 | 13 | You must have an Apple Developer Account and a MusicKit API Key. 14 | See instructions on how to obtain these here: `Getting Keys And Creating Tokens `_. 15 | 16 | Dependencies 17 | ^^^^^^^^^^^^ 18 | 19 | * `Requests `_ 20 | * `PyJWT `_ 21 | * `Cryptography `_ 22 | 23 | Installation 24 | ^^^^^^^^^^^^ 25 | :: 26 | 27 | python setup.py install 28 | 29 | or:: 30 | 31 | pip install apple-music-python 32 | 33 | Example 34 | ^^^^^^^ 35 | :: 36 | 37 | import applemusicpy 38 | 39 | secret_key = 'x' 40 | key_id = 'y' 41 | team_id = 'z' 42 | 43 | am = applemusicpy.AppleMusic(secret_key, key_id, team_id) 44 | results = am.search('travis scott', types=['albums'], limit=5) 45 | 46 | for item in results['results']['albums']['data']: 47 | print(item['attributes']['name']) 48 | 49 | :mod:`client` Module 50 | ^^^^^^^^^^^^^^^^^^^^ 51 | 52 | .. automodule:: applemusicpy.client 53 | :members: 54 | :special-members: __init__ 55 | 56 | .. toctree:: 57 | :maxdepth: 2 58 | :caption: Contents: 59 | 60 | Versioning 61 | ^^^^^^^^^^ 62 | 63 | v1.0.0 - Initial Release - 12/15/2018 64 | v1.0.1 - Updated package info on PyPI - 12/16/2018 65 | v1.0.2 - Added Windows search support - 01/21/2019 66 | v1.0.3 - Fixed error handling of HTTPError - 11/03/2019 67 | v1.0.4 - Fixed error with reading token - 01/24/2021 68 | v1.0.5 - Refresh token before request if token is expired - 05/09/2021 69 | 70 | Authors 71 | ^^^^^^^ 72 | 73 | * **Matt Palazzolo** - `GitHub Profile `_ 74 | 75 | License 76 | ^^^^^^^ 77 | https://github.com/mpalazzolo/apple-music-python/LICENSE.txt 78 | 79 | Indices and tables 80 | ================== 81 | 82 | * :ref:`genindex` 83 | * :ref:`modindex` 84 | * :ref:`search` 85 | -------------------------------------------------------------------------------- /docs/source/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | asn1crypto==0.24.0 3 | Babel==2.6.0 4 | certifi==2018.11.29 5 | cffi==1.11.5 6 | chardet==3.0.4 7 | cryptography==3.3.2 8 | docutils==0.14 9 | idna==2.8 10 | imagesize==1.1.0 11 | Jinja2==2.11.3 12 | MarkupSafe==1.1.0 13 | packaging==19.0 14 | pycparser==2.19 15 | Pygments==2.7.4 16 | PyJWT==1.7.1 17 | pyparsing==2.3.1 18 | pytz==2018.9 19 | requests==2.21.0 20 | six==1.12.0 21 | snowballstemmer==1.2.1 22 | Sphinx==1.8.3 23 | sphinx-rtd-theme==0.4.2 24 | sphinxcontrib-websupport==1.1.0 25 | urllib3==1.24.2 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.md', 'r') as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='apple-music-python', 8 | url='https://github.com/mpalazzolo/apple-music-python', 9 | version='1.0.5', 10 | packages=['applemusicpy'], 11 | license='LICENSE.txt', 12 | author='Matt Palazzolo', 13 | author_email='mattpalazzolo@gmail.com', 14 | description='A python wrapper for the Apple Music API', 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | install_requires=[ 18 | 'requests>=2.21', 19 | 'pyjwt>=1.7.1', 20 | 'cryptography>=3.2' 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from applemusicpy import AppleMusic 2 | import unittest 3 | 4 | 5 | class TestApple(unittest.TestCase): 6 | 7 | def setUp(self): 8 | # albums 9 | self.born_to_run = '310730204' 10 | self.ready_to_die = '204669326' 11 | # music videos 12 | self.rubber_soul = '401135199' 13 | self.sgt_pepper = '401147268' 14 | # ISRC 15 | self.gods_plan_isrc = 'USCM51800004' 16 | # playlists 17 | self.janet_jackson = 'pl.acc464c750b94302b8806e5fcbe56e17' 18 | self.eighties_pop = 'pl.97c6f95b0b884bedbcce117f9ea5d54b' 19 | # songs 20 | self.xo_tour_life = '1274153124' 21 | self.new_patek = '1436530704' 22 | # artists 23 | self.lil_pump = '1129587661' 24 | self.smokepurpp = '1122104172' 25 | # stations 26 | self.alt = 'ra.985484166' 27 | self.pure_pop = 'ra.686227433' 28 | # curators 29 | self.large_up = '1107687517' 30 | self.grand_ole_opry = '976439448' 31 | # activity 32 | self.party = '976439514' 33 | self.chill = '976439503' 34 | # apple curators 35 | self.apple_alt = '976439526' 36 | self.live_nation_tv = '1017168810' 37 | # genres 38 | self.pop = '14' 39 | self.rock = '21' 40 | # storefronts 41 | self.us = 'us' 42 | self.jp = 'jp' 43 | # search 44 | self.search_term = 'nice for what' 45 | 46 | def test_album(self): 47 | results = am.album(self.born_to_run) 48 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Born To Run') 49 | 50 | def test_album_relationship(self): 51 | results = am.album_relationship(self.born_to_run, 'artists') 52 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Bruce Springsteen') 53 | 54 | def test_albums(self): 55 | results = am.albums([self.born_to_run, self.ready_to_die]) 56 | self.assertTrue(len(results['data']) == 2) 57 | self.assertTrue(results['data'][0]['type'] == 'albums') 58 | 59 | def test_music_video(self): 60 | results = am.music_video(self.rubber_soul) 61 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Rubber Soul (Documentary)') 62 | 63 | def test_music_video_relationship(self): 64 | results = am.music_video_relationship(self.rubber_soul, 'artists') 65 | self.assertTrue(results['data'][0]['attributes']['name'] == 'The Beatles') 66 | 67 | def test_music_videos(self): 68 | results = am.music_videos([self.rubber_soul, self.sgt_pepper]) 69 | self.assertTrue(len(results['data']) == 2) 70 | self.assertTrue(results['data'][0]['type'] == 'music-videos') 71 | 72 | # ISRCs don't seem to work for music videos 73 | # def test_music_videos_by_isrc(self): 74 | 75 | def test_playlist(self): 76 | results = am.playlist(self.janet_jackson) 77 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Janet Jackson: No.1 Songs') 78 | 79 | def test_playlist_relationship(self): 80 | results = am.playlist_relationship(self.eighties_pop, 'tracks') # playlist have 'tracks', artists have 'songs' 81 | self.assertTrue(results['data'][0]['type'] == 'songs') 82 | 83 | def test_playlists(self): 84 | results = am.playlists([self.janet_jackson, self.eighties_pop]) 85 | self.assertTrue(len(results['data']) == 2) 86 | self.assertTrue(results['data'][0]['type'] == 'playlists') 87 | 88 | def test_song(self): 89 | results = am.song(self.xo_tour_life) 90 | self.assertTrue(results['data'][0]['attributes']['name'] == 'XO TOUR Llif3') 91 | 92 | def test_song_relationship(self): 93 | results = am.song_relationship(self.xo_tour_life, 'artists') 94 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Lil Uzi Vert') 95 | 96 | def test_songs(self): 97 | results = am.songs([self.xo_tour_life, self.new_patek]) 98 | self.assertTrue(len(results['data']) == 2) 99 | self.assertTrue(results['data'][0]['type'] == 'songs') 100 | 101 | def test_songs_by_isrc(self): 102 | results = am.songs_by_isrc([self.gods_plan_isrc]) 103 | self.assertTrue(results['data'][0]['attributes']['name'] == 'God\'s Plan') 104 | 105 | def test_artist(self): 106 | results = am.artist(self.lil_pump) 107 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Lil Pump') 108 | 109 | def test_artist_relationship(self): 110 | results = am.artist_relationship(self.lil_pump, 'songs') 111 | self.assertTrue(results['data'][0]['type'] == 'songs') 112 | 113 | def test_artists(self): 114 | results = am.artists([self.lil_pump, self.smokepurpp]) 115 | self.assertTrue(len(results['data']) == 2) 116 | self.assertTrue(results['data'][0]['type'] == 'artists') 117 | 118 | def test_station(self): 119 | results = am.station(self.alt) 120 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Alternative') 121 | 122 | def test_stations(self): 123 | results = am.stations([self.alt, self.pure_pop]) 124 | self.assertTrue(len(results['data']) == 2) 125 | self.assertTrue(results['data'][0]['type'] == 'stations') 126 | 127 | def test_curator(self): 128 | results = am.curator(self.large_up) 129 | self.assertTrue(results['data'][0]['attributes']['name'] == 'LargeUp') 130 | 131 | def test_curator_relationship(self): 132 | results = am.curator_relationship(self.grand_ole_opry, 'playlists') 133 | self.assertTrue(results['data'][0]['type'] == 'playlists') 134 | 135 | def test_curators(self): 136 | results = am.curators([self.large_up, self.grand_ole_opry]) 137 | self.assertTrue(len(results['data']) == 2) 138 | self.assertTrue(results['data'][0]['type'] == 'curators') 139 | 140 | def test_activity(self): 141 | results = am.activity(self.party) 142 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Party') 143 | 144 | def test_activity_relationship(self): 145 | results = am.activity_relationship(self.party, 'playlists') 146 | self.assertTrue(results['data'][0]['type'] == 'playlists') 147 | 148 | def test_activities(self): 149 | results = am.activities([self.party, self.chill]) 150 | self.assertTrue(len(results['data']) == 2) 151 | self.assertTrue(results['data'][0]['type'] == 'activities') 152 | 153 | def test_apple_curator(self): 154 | results = am.apple_curator(self.apple_alt) 155 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Apple Music Alternative') 156 | 157 | def test_apple_curator_relationship(self): 158 | results = am.apple_curator_relationship(self.apple_alt, 'playlists') 159 | self.assertTrue(results['data'][0]['type'] == 'playlists') 160 | 161 | def test_apple_curators(self): 162 | results = am.apple_curators([self.apple_alt, self.live_nation_tv]) 163 | self.assertTrue(len(results['data']) == 2) 164 | self.assertTrue(results['data'][0]['type'] == 'apple-curators') 165 | 166 | def test_genre(self): 167 | results = am.genre(self.pop) 168 | self.assertTrue(results['data'][0]['attributes']['name'] == 'Pop') 169 | 170 | def test_genres(self): 171 | results = am.genres([self.pop, self.rock]) 172 | self.assertTrue(len(results['data']) == 2) 173 | self.assertTrue(results['data'][0]['type'] == 'genres') 174 | 175 | def test_genres_all(self): 176 | results = am.genres_all() 177 | self.assertTrue(results['data'][0]['id'] == '34') 178 | 179 | def test_storefront(self): 180 | results = am.storefront(self.us) 181 | self.assertTrue(results['data'][0]['attributes']['name'] == 'United States') 182 | 183 | def test_storefronts(self): 184 | results = am.storefronts([self.us, self.jp]) 185 | self.assertTrue(len(results['data']) == 2) 186 | self.assertTrue(results['data'][0]['type'] == 'storefronts') 187 | 188 | def test_storefronts_all(self): 189 | results = am.storefronts_all() 190 | self.assertTrue(results['data'][0]['id'] == 'dz') 191 | 192 | def test_search(self): 193 | results = am.search(self.search_term, types=['songs']) 194 | self.assertTrue(results['results']['songs']['data'][0]['attributes']['name'] == 'Nice For What') 195 | 196 | def test_search_windows(self): 197 | results = am.search(self.search_term, types=['songs'], os='windows') 198 | self.assertTrue(results['results']['songs']['data'][0]['attributes']['name'] == 'Nice For What') 199 | 200 | def test_charts(self): 201 | results = am.charts(types=['songs'], genre=self.pop) 202 | self.assertTrue(results['results']['songs'][0]['name'] == 'Top Songs') 203 | 204 | 205 | if __name__ == '__main__': 206 | # These tests require API authorization, so need to read in keys 207 | keys = {} 208 | 209 | with open('private_key.p8', 'r') as f: 210 | keys['secret'] = f.read() 211 | 212 | with open('keys.txt') as f: 213 | for line in f: 214 | name, val = line.partition('=')[::2] 215 | keys[name.strip()] = val.strip() 216 | 217 | am = AppleMusic(secret_key=keys['secret'], key_id=keys['keyID'], team_id=keys['teamID']) 218 | 219 | unittest.main() 220 | --------------------------------------------------------------------------------