diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index a3d5dcc7..7c83ab32 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 91eb8e17..aae87449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for audiobook endpoints: get_audiobook, get_audiobooks, and get_audiobook_chapters. +- Added integration tests for audiobook endpoints. +- Removed `python 2.7` from GitHub Actions CI workflow. Python v2.7 reached end of life support and is no longer supported by Ubuntu 20.04. +- Removed `python 3.6` from GitHub Actions CI workflow. Ubuntu 20.04 is not available in GitHub Actions for `python 3.6`. + ### Changed - Changes the YouTube video link for authentication tutorial (the old video was in low definition, the new one is in high definition) - Updated links to Spotify in documentation diff --git a/spotipy/client.py b/spotipy/client.py index d3b918f0..a026e412 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -111,14 +111,14 @@ class Spotify(object): # # [1] https://www.iana.org/assignments/uri-schemes/prov/spotify # [2] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - _regex_spotify_uri = r'^spotify:(?:(?Ptrack|artist|album|playlist|show|episode):(?P[0-9A-Za-z]+)|user:(?P[0-9A-Za-z]+):playlist:(?P[0-9A-Za-z]+))$' # noqa: E501 + _regex_spotify_uri = r'^spotify:(?:(?Ptrack|artist|album|playlist|show|episode|audiobook):(?P[0-9A-Za-z]+)|user:(?P[0-9A-Za-z]+):playlist:(?P[0-9A-Za-z]+))$' # noqa: E501 # Spotify URLs are defined at [1]. The assumption is made that they are all # pointing to open.spotify.com, so a regex is used to parse them as well, # instead of a more complex URL parsing function. # # [1] https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - _regex_spotify_url = r'^(http[s]?:\/\/)?open.spotify.com\/(?Ptrack|artist|album|playlist|show|episode|user)\/(?P[0-9A-Za-z]+)(\?.*)?$' # noqa: E501 + _regex_spotify_url = r'^(http[s]?:\/\/)?open.spotify.com\/(?Ptrack|artist|album|playlist|show|episode|user|audiobook)\/(?P[0-9A-Za-z]+)(\?.*)?$' # noqa: E501 _regex_base62 = r'^[0-9A-Za-z]+$' @@ -2033,3 +2033,51 @@ def _search_multiple_markets(self, q, limit, offset, type, markets, total): return results return results + + def get_audiobook(self, id, market=None): + """ Get Spotify catalog information for a single audiobook identified by its unique + Spotify ID. + + Parameters: + - id - the Spotify ID for the audiobook + - market - an ISO 3166-1 alpha-2 country code. + """ + audiobook_id = self._get_id("audiobook", id) + endpoint = f"audiobooks/{audiobook_id}" + + if market: + endpoint += f'?market={market}' + + return self._get(endpoint) + + def get_audiobooks(self, ids, market=None): + """ Get Spotify catalog information for multiple audiobooks based on their Spotify IDs. + + Parameters: + - ids - a list of Spotify IDs for the audiobooks + - market - an ISO 3166-1 alpha-2 country code. + """ + audiobook_ids = [self._get_id("audiobook", id) for id in ids] + endpoint = f"audiobooks?ids={','.join(audiobook_ids)}" + + if market: + endpoint += f'&market={market}' + + return self._get(endpoint) + + def get_audiobook_chapters(self, id, market=None, limit=20, offset=0): + """ Get Spotify catalog information about an audiobook’s chapters. + + Parameters: + - id - the Spotify ID for the audiobook + - market - an ISO 3166-1 alpha-2 country code. + - limit - the maximum number of items to return + - offset - the index of the first item to return + """ + audiobook_id = self._get_id("audiobook", id) + endpoint = f"audiobooks/{audiobook_id}/chapters?limit={limit}&offset={offset}" + + if market: + endpoint += f'&market={market}' + + return self._get(endpoint) diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index fe58160d..ca2faace 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -55,6 +55,16 @@ class AuthTestSpotipy(unittest.TestCase): heavyweight_ep1_url = 'https://open.spotify.com/episode/68kq3bNz6hEuq8NtdfwERG' reply_all_ep1_urn = 'spotify:episode:1KHjbpnmNpFmNTczQmTZlR' + american_gods_urn = 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN' + american_gods_id = '1IcM9Untg6d3ktuwObYGcN' + american_gods_url = 'https://open.spotify.com/audiobook/1IcM9Untg6d3ktuwObYGcN' + + four_books = [ + 'spotify:audiobook:1IcM9Untg6d3ktuwObYGcN', + 'spotify:audiobook:37sRC6carIX2Vf3Vv716T7', + 'spotify:audiobook:1Gep4UJ95xQawA55OgRI8n', + 'spotify:audiobook:4Sm381mcf5gBsi9yfhqgVB'] + @classmethod def setUpClass(self): self.spotify = Spotify( @@ -455,3 +465,31 @@ def test_available_markets(self): self.assertTrue(isinstance(markets, list)) self.assertIn("US", markets) self.assertIn("GB", markets) + + def test_get_audiobook(self): + audiobook = self.spotify.get_audiobook(self.american_gods_urn, market="US") + print(audiobook) + self.assertTrue(audiobook['name'] == + 'American Gods: The Tenth Anniversary Edition: A Novel') + + def test_get_audiobook_bad_urn(self): + with self.assertRaises(SpotifyException): + self.spotify.get_audiobook("bogus_urn", market="US") + + def test_get_audiobooks(self): + results = self.spotify.get_audiobooks(self.four_books, market="US") + self.assertTrue('audiobooks' in results) + self.assertTrue(len(results['audiobooks']) == 4) + self.assertTrue(results['audiobooks'][0]['name'] == + 'American Gods: The Tenth Anniversary Edition: A Novel') + self.assertTrue(results['audiobooks'][1]['name'] == 'The Da Vinci Code: A Novel') + self.assertTrue(results['audiobooks'][2]['name'] == 'Outlander') + self.assertTrue(results['audiobooks'][3]['name'] == 'Pachinko: A Novel') + + def test_get_audiobook_chapters(self): + results = self.spotify.get_audiobook_chapters( + self.american_gods_urn, market="US", limit=10, offset=5) + self.assertTrue('items' in results) + self.assertTrue(len(results['items']) == 10) + self.assertTrue(results['items'][0]['chapter_number'] == 5) + self.assertTrue(results['items'][9]['chapter_number'] == 14)