diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..933df46 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,24 @@ +name: Test +on: + - push + - pull_request +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v6 + - name: Restore pip cache + uses: actions/cache@v5 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Run tests + run: | + python3 -m venv env + . env/bin/activate + pip install --editable ".[dev]" + pytest diff --git a/README.rst b/README.rst index 1ac9ca0..e3c2cc4 100644 --- a/README.rst +++ b/README.rst @@ -1,41 +1,94 @@ Python iTunes ============= -A simple python wrapper to access iTunes Store API http://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html +A simple Python wrapper to access `iTunes Search API`_. + +.. _iTunes Search API: https://performance-partners.apple.com/search-api Installation ------------ -Pypi package available at http://pypi.python.org/pypi/python-itunes/1.0 +The library is distributed `on PyPI`_, +and can be installed into a `virtual environment`_ for your project +with ``pip``: + +.. code-block:: sh + + $ pip install python-itunes + +To install the latest development version, +``pip`` can fetch the source from the Git repository: + +.. code-block:: sh + + $ pip install git+https://github.com/ocelma/python-itunes + +Usually, you would list this dependency in your ``pyproject.toml``: + +.. code-block:: toml + + [project] + # ... + dependencies = [ + "python-itunes", + # or + "python-itunes @ git+https://github.com/ocelma/python-itunes", + ] -:: +.. _on PyPI: http://pypi.python.org/pypi/python-itunes +.. _virtual environment: https://docs.python.org/3/library/venv.html - $ easy_install python-itunes +Development +----------- -Or download the code from https://github.com/ocelma/python-itunes/archives/master and then +To hack on the library itself, +create a venv, +and make an *editable* install of the library, +along with development tools: -:: +.. code-block:: sh - $ python setup.py install + $ git clone https://github.com/ocelma/python-itunes + $ cd python-itunes + $ python3 -m venv env + $ . env/bin/activate + $ pip install --editable ".[dev]" -.. note:: +If you get an error like this:: - If you're using python version <= 2.5 you'll need to install simplejson. E.g: + ERROR: File "setup.py" or "setup.cfg" not found. Directory cannot be installed in editable mode: /path/to/python-itunes + (A "pyproject.toml" file was found, but editable mode currently requires a setuptools-based build.) -:: +...your ``pip`` is too old. +Upgrading the version installed in your venv +will resolve the problem: - $ easy_install simplejson +.. code-block:: sh + $ pip install --upgrade pip + +Whenever you open a new terminal, +don't forget to re-activate the venv: + +.. code-block:: sh + + $ cd python-itunes + $ . env/bin/activate + +Then, when you ``import itunes`` in a Python REPL, +changes made to the library source +are available immediately without reinstalling the package. Examples -------- Search ~~~~~~ -:: + +.. code-block:: python import itunes - + # Search band U2 artist = itunes.search_artist('u2')[0] for album in artist.get_albums(): @@ -52,7 +105,7 @@ Search # Global Search 'Beatles' items = itunes.search(query='beatles') - for item in items: + for item in items: print '[' + item.type + ']', item.get_artist(), item.get_name(), item.get_url(), item.get_release_date() # Search 'Angry Birds' game @@ -69,20 +122,20 @@ Search Lookup ~~~~~~ -:: +.. code-block:: python import itunes # Lookup Achtung Baby album by U2 U2_ACHTUNGBABY_ID = 475390461 album = itunes.lookup(U2_ACHTUNGBABY_ID) - + print album.get_url() print album.get_artwork() - + artist = album.get_artist() tracks = album.get_tracks() - + # Lookup song One from Achtung Baby album by U2 U2_ONE_ID = 475391315 track = itunes.lookup(U2_ONE_ID) @@ -93,7 +146,7 @@ Lookup Caching JSON results ~~~~~~~~~~~~~~~~~~~~ -:: +.. code-block:: python import itunes @@ -106,6 +159,6 @@ Caching JSON results Tests ----- -:: +.. code-block:: sh - $ nosetests tests + $ pytest diff --git a/itunes/__init__.py b/itunes/__init__.py index 048c6f6..9878bd5 100644 --- a/itunes/__init__.py +++ b/itunes/__init__.py @@ -1,27 +1,12 @@ #!/usr/bin/python """A python interface to search iTunes Store""" -import os -import urllib2, urllib -import urlparse -import re import datetime -try: - import simplejson as json -except ImportError: - import json -try: - from hashlib import md5 -except ImportError: - from md5 import md5 - -__name__ = 'pyitunes' -__doc__ = 'A python interface to search iTunes Store' -__author__ = 'Oscar Celma' -__version__ = '0.2' -__license__ = 'GPL' -__maintainer__ = 'Oscar Celma' -__email__ = 'ocelma@bmat.com' -__status__ = 'Beta' +from hashlib import md5 +import json +import numbers +import os +import urllib.parse +import urllib.request API_VERSION = '2' # iTunes API version COUNTRY = 'US' # ISO Country Store @@ -65,24 +50,24 @@ def _download_response(self): data = [] for name in self.params.keys(): value = self.params[name] - if isinstance(value, int) or isinstance(value, float) or isinstance(value, long): + if isinstance(value, numbers.Number): value = str(value) try: - data.append('='.join((name, urllib.quote_plus(value.replace('&', '&').encode('utf8'))))) + data.append('='.join((name, urllib.parse.quote_plus(value.replace('&', '&').encode('utf8'))))) except UnicodeDecodeError: - data.append('='.join((name, urllib.quote_plus(value.replace('&', '&'))))) + data.append('='.join((name, urllib.parse.quote_plus(value.replace('&', '&'))))) data = '&'.join(data) url = HOST_NAME - parsed_url = urlparse.urlparse(url) + parsed_url = urllib.parse.urlsplit(url) if not parsed_url.scheme: url = "http://" + url url += self.method + '?' url += data - request = urllib2.Request(url) - response = urllib2.urlopen(request) - return response.read() + request = urllib.request.Request(url) + response = urllib.request.urlopen(request) + return response.read().decode() def execute(self, cacheable=False): try: @@ -92,8 +77,8 @@ def execute(self, cacheable=False): response = self._download_response() response = clean_json(response) return json.loads(response) - except urllib2.HTTPError, e: - raise self._get_error(e.fp.read()) + except urllib.error.HTTPError as e: + raise self._get_error(e.msg) def _get_cache_key(self): """Cache key""" @@ -147,15 +132,15 @@ def _get_params(self): def get(self): self._json_results = self._request(cacheable=is_caching_enabled()) - if self._json_results.has_key('errorMessage'): + if 'errorMessage' in self._json_results: raise ServiceException(type='Error', message=self._json_results['errorMessage']) self._num_results = self._json_results['resultCount'] l = [] for json in self._json_results['results']: type = None - if json.has_key('wrapperType'): + if 'wrapperType' in json: type = json['wrapperType'] - elif json.has_key('kind'): + elif 'kind' in json: type = json['kind'] if type == 'artist': @@ -174,9 +159,9 @@ def get(self): id = json['trackId'] item = Software(id) else: - if json.has_key('collectionId'): + if 'collectionId' in json: id = json['collectionId'] - elif json.has_key('artistId'): + elif 'artistId' in json: id = json['artistId'] item = Item(id) item._set(json) @@ -253,7 +238,7 @@ def __init__(self, id): def _set(self, json): self.json = json #print json - if json.has_key('kind'): + if 'kind' in json: self.type = json['kind'] else: self.type = json['wrapperType'] @@ -269,7 +254,7 @@ def _set_genre(self, json): def _set_release(self, json): self.release_date = None - if json.has_key('releaseDate') and json['releaseDate']: + if 'releaseDate' in json and json['releaseDate']: self.release_date = json['releaseDate'].split('T')[0] def _set_country(self, json): @@ -277,34 +262,34 @@ def _set_country(self, json): def _set_artwork(self, json): self.artwork = dict() - if json.has_key('artworkUrl30'): + if 'artworkUrl30' in json: self.artwork['30'] = json['artworkUrl30'] - if json.has_key('artworkUrl60'): + if 'artworkUrl60' in json: self.artwork['60'] = json['artworkUrl60'] - if json.has_key('artworkUrl100'): + if 'artworkUrl100' in json: self.artwork['100'] = json['artworkUrl100'] - if json.has_key('artworkUrl512'): + if 'artworkUrl512' in json: self.artwork['512'] = json['artworkUrl512'] - if json.has_key('artworkUrl1100'): + if 'artworkUrl1100' in json: self.artwork['1100'] = json['artworkUrl1100'] def _set_url(self, json): self.url = None - if json.has_key('trackViewUrl'): + if 'trackViewUrl' in json: self.url = json['trackViewUrl'] - elif json.has_key('collectionViewUrl'): + elif 'collectionViewUrl' in json: self.url = json['collectionViewUrl'] - elif json.has_key('artistViewUrl'): + elif 'artistViewUrl' in json: self.url = json['artistViewUrl'] # REPR, EQ, NEQ def __repr__(self): if not self.name: - if self.json.has_key('collectionName'): + if 'collectionName' in self.json: self._set_name(self.json['collectionName']) - elif self.json.has_key('artistName'): + elif 'artistName' in self.json: self._set_name(self.json['artistName']) - return self.name.encode('utf8') + return self.name def __eq__(self, other): if other == None: @@ -322,9 +307,9 @@ def _set_name(self, name): # GETTERs def get_id(self): if not self.id: - if self.json.has_key('collectionId'): + if 'collectionId' in self.json: self.id = self.json['collectionId'] - elif self.json.has_key('artistId'): + elif 'artistId' in self.json: self.id = self.json['artistId'] return self.id @@ -466,11 +451,11 @@ def _set(self, json): self.url = json.get('trackViewUrl', None) self.preview_url = json.get('previewUrl', None) self.price = None - if json.has_key('trackPrice') and json['trackPrice'] is not None: + if 'trackPrice' in json and json['trackPrice'] is not None: self.price = round(json['trackPrice'], 4) self.number = json.get('trackNumber', None) self.duration = None - if json.has_key('trackTimeMillis') and json['trackTimeMillis'] is not None: + if 'trackTimeMillis' in json and json['trackTimeMillis'] is not None: self.duration = round(json.get('trackTimeMillis', 0.0)/1000.0, 2) try: self._set_artist(json) @@ -489,7 +474,7 @@ def _set_artist(self, json): self.artist._set(json) def _set_album(self, json): - if json.has_key('collectionId'): + if 'collectionId' in json: id = json['collectionId'] self.album = Album(id) self.album._set(json) @@ -546,7 +531,7 @@ def _set_file_size_bytes(self, json): def _set_current_version_release_date(self, json): self.current_version_release_date = None - if json.has_key('currentVersionReleaseDate') and json['currentVersionReleaseDate']: + if 'currentVersionReleaseDate' in json and json['currentVersionReleaseDate']: self.current_version_release_date = datetime.datetime.strptime( json['currentVersionReleaseDate'], r'%Y-%m-%dT%H:%M:%SZ' ) def _set_bundle_id(self, json): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c406d1c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-itunes" +version = "1.0" +description = "A simple python wrapper to access iTunes Store API" +authors = [{ name = "Oscar Celma", email = "ocelma@bmat.com" }] +maintainers = [{ name = "Oscar Celma", email = "ocelma@bmat.com" }] +license = { text = "GPL-3.0-or-later" } + +[project.urls] +source = "https://github.com/ocelma/python-itunes" + +[project.optional-dependencies] +dev = ["pytest"] + +[tool.pytest] +testpaths = ["tests"] +python_files = ["*.py"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 0b542e1..0000000 --- a/setup.py +++ /dev/null @@ -1,21 +0,0 @@ -import os.path -try: - from setuptools import setup, Extension -except ImportError: - from distutils.core import setup, Extension - -VERSION = "1.0" - -setup( - name = "python-itunes", - version = VERSION, - description="A simple python wrapper to access iTunes Store API", - author='Oscar Celma', - author_email='ocelma@bmat.com', - maintainer='Oscar Celma', - maintainer_email='ocelma@bmat.com', - license = "http://www.gnu.org/copyleft/gpl.html", - platforms = ["any"], - url="https://github.com/ocelma/python-itunes", - packages=['itunes'], -) diff --git a/tests/tests.py b/tests/tests.py index d7d58f3..19780ad 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,95 +1,91 @@ # -*- coding: utf-8 -*- -from nose.tools import assert_equal, assert_not_equal, assert_raises, assert_true import itunes +import pytest U2 = 'U2' U2_ONE = 'One' -U2_ACHTUNGBABY = 'Achtung Baby (Deluxe Edition) [Remastered]' # 'Achtung Baby' +U2_ACHTUNGBABY = 'Achtung Baby (20th Anniversary Deluxe Edition)' -MUSIC_VIDEO_KIND = 'music-video' +FEATURE_MOVIE_KIND = 'feature-movie' SONG_KIND = "song" COLLECTION_KIND = "collection" -U2_ONE_ID = 475391315 # Before it was 368617 -U2_ACHTUNGBABY_ID = 475390461 # Before it was 368713 +U2_ONE_ID = 1440809274 +U2_ACHTUNGBABY_ID = 1440808807 U2_ID = 78500 -U2_URL = 'https://itunes.apple.com/us/artist/u2/id%s?uo=4' % U2_ID -U2_ACHTUNGBABY_URL = 'https://itunes.apple.com/us/album/achtung-baby-deluxe-edition/id%s?uo=4' % U2_ACHTUNGBABY_ID -U2_ONE_URL = 'https://itunes.apple.com/us/album/one/id%s?i=%s&uo=4' % (U2_ACHTUNGBABY_ID, U2_ONE_ID) +U2_URL = 'https://music.apple.com/us/artist/u2/%s?uo=4' % U2_ID +U2_ACHTUNGBABY_URL = 'https://music.apple.com/us/album/achtung-baby-20th-anniversary-deluxe-edition/%s?uo=4' % U2_ACHTUNGBABY_ID +U2_ONE_URL = 'https://music.apple.com/us/album/one/%s?i=%s&uo=4' % (U2_ACHTUNGBABY_ID, U2_ONE_ID) #SEARCHES def test_search_track_kind(): - assert_equal(itunes.search_track('u2 achtung baby one')[0].get_type(), SONG_KIND) + assert itunes.search_track('u2 achtung baby one')[0].get_type() == SONG_KIND def test_search_album(): - assert_equal(itunes.search_album('u2 achtung baby')[0].get_type(), COLLECTION_KIND) + assert itunes.search_album('u2 achtung baby')[0].get_type() == COLLECTION_KIND def test_search_artist(): - assert_equal(itunes.search_artist('u2')[0].get_id(), U2_ID) + assert itunes.search_artist('u2')[0].get_id() == U2_ID def test_search_artist_store(): - U2_URL_ES = 'https://itunes.apple.com/es/artist/u2/id78500?l=en&uo=4' - assert_equal(itunes.search_artist('u2', store='ES')[0].get_id(), U2_ID) - assert_equal(itunes.search_artist('u2', store='ES')[0].get_url(), U2_URL_ES) + U2_URL_ES = 'https://music.apple.com/es/artist/u2/78500?l=en&uo=4' + assert itunes.search_artist('u2', store='ES')[0].get_id() == U2_ID + assert itunes.search_artist('u2', store='ES')[0].get_url() == U2_URL_ES #LOOKUPS def test_lookup_track(): item = itunes.lookup(U2_ONE_ID) - assert_true(isinstance(item, itunes.Track)) - assert_equal(item.get_id(), U2_ONE_ID) - assert_equal(item.get_name(), U2_ONE) + assert isinstance(item, itunes.Track) + assert item.get_id() == U2_ONE_ID + assert item.get_name() == U2_ONE - assert_equal(item.get_album().get_id(), U2_ACHTUNGBABY_ID) - assert_equal(item.get_artist().get_id(), U2_ID) + assert item.get_album().get_id() == U2_ACHTUNGBABY_ID + assert item.get_artist().get_id() == U2_ID def test_lookup_album(): item = itunes.lookup(U2_ACHTUNGBABY_ID) - assert_true(isinstance(item, itunes.Album)) - assert_equal(item.get_id(), U2_ACHTUNGBABY_ID) - assert_equal(item.get_name(), U2_ACHTUNGBABY) + assert isinstance(item, itunes.Album) + assert item.get_id() == U2_ACHTUNGBABY_ID + assert item.get_name() == U2_ACHTUNGBABY - assert_equal(item.get_artist().get_id(), U2_ID) + assert item.get_artist().get_id() == U2_ID def test_lookup_artist(): item = itunes.lookup(U2_ID) - assert_true(isinstance(item, itunes.Artist)) - assert_equal(item.get_id(), U2_ID) - assert_equal(item.get_name(), U2) + assert isinstance(item, itunes.Artist) + assert item.get_id() == U2_ID + assert item.get_name() == U2 def test_lookup_notfound(): UNKNOWN_ID = 0 - assert_raises(itunes.ServiceException, itunes.lookup, UNKNOWN_ID) + with pytest.raises(itunes.ServiceException): + itunes.lookup(UNKNOWN_ID) #METHODS def test_artist_url(): item = itunes.lookup(U2_ID) - assert_equal(item.get_url(), U2_URL) + assert item.get_url() == U2_URL def test_album_url(): item = itunes.lookup(U2_ACHTUNGBABY_ID) - assert_equal(item.get_url(), U2_ACHTUNGBABY_URL) + assert item.get_url() == U2_ACHTUNGBABY_URL def test_track_url(): item = itunes.lookup(U2_ONE_ID) - assert_equal(item.get_url(), U2_ONE_URL) + assert item.get_url() == U2_ONE_URL def test_album_length(): item = itunes.lookup(U2_ACHTUNGBABY_ID) - assert_true(len(item.get_tracks()) == 26) # 12) + assert len(item.get_tracks()) == 26 # 12) def test_music_video_kind(): item = itunes.lookup(U2_ID) - assert_equal(item.get_music_videos()[0].get_type(), MUSIC_VIDEO_KIND) + assert item.get_music_videos()[0].get_type() == FEATURE_MOVIE_KIND #TEXT: Unicode def test_unicode(): - assert_equal(itunes.search_artist('Björk')[0].get_id(), itunes.search_artist(u'Bj\xf6rk')[0].get_id()) + assert itunes.search_artist('Björk')[0].get_id() == itunes.search_artist(u'Bj\xf6rk')[0].get_id() def test_unicode2(): - assert_equal(itunes.search_artist('Björk')[:5], itunes.search_artist(u'Bj\xf6rk')[:5]) - -def test_movie_as_track(): - item = itunes.search(query='the godfather', media='movie')[0] - assert_equal(item.get_artist(), None) - + assert itunes.search_artist('Björk')[:5] == itunes.search_artist(u'Bj\xf6rk')[:5]