diff --git a/README.md b/README.md index 31b717c..f4af28b 100644 --- a/README.md +++ b/README.md @@ -133,5 +133,5 @@ network = pylast.LastFMNetwork(...) To enable from pytest: ```sh -pytest -k test_album_search_images --log-cli-level debug +pytest --log-cli-level debug -k test_album_search_images ``` diff --git a/pylast/__init__.py b/pylast/__init__.py index 839f6a5..a53d632 100644 --- a/pylast/__init__.py +++ b/pylast/__init__.py @@ -903,7 +903,7 @@ class SessionKeyGenerator(object): a. network = get_*_network(API_KEY, API_SECRET) b. sg = SessionKeyGenerator(network) c. url = sg.get_web_auth_url() - d. Ask the user to open the url and authorize you, and wait for it. + d. Ask the user to open the URL and authorize you, and wait for it. e. session_key = sg.get_web_auth_session_key(url) 2) Username and Password Authentication: a. network = get_*_network(API_KEY, API_SECRET) @@ -961,7 +961,7 @@ class SessionKeyGenerator(object): def get_web_auth_session_key(self, url, token=""): """ - Retrieves the session key of a web authorization process by its url. + Retrieves the session key of a web authorization process by its URL. """ if url in self.web_auth_tokens.keys(): @@ -1375,7 +1375,7 @@ class _Opus(_BaseObject, _Taggable): __hash__ = _BaseObject.__hash__ def __init__(self, artist, title, network, ws_prefix, username=None, - images=None): + info=None): """ Create an opus instance. # Parameters: @@ -1384,6 +1384,9 @@ class _Opus(_BaseObject, _Taggable): * ws_prefix: 'album' or 'track' """ + if info is None: + info = {} + _BaseObject.__init__(self, network, ws_prefix) _Taggable.__init__(self, ws_prefix) @@ -1394,7 +1397,7 @@ class _Opus(_BaseObject, _Taggable): self.title = title self.username = username - self.images = images + self.info = info def __repr__(self): return "pylast.%s(%s, %s, %s)" % ( @@ -1428,6 +1431,21 @@ class _Opus(_BaseObject, _Taggable): return self.artist + def get_cover_image(self, size=SIZE_EXTRA_LARGE): + """ + Returns a URI to the cover image + size can be one of: + SIZE_EXTRA_LARGE + SIZE_LARGE + SIZE_MEDIUM + SIZE_SMALL + """ + if "image" not in self.info: + self.info["image"] = _extract_all( + self._request(self.ws_prefix + ".getInfo", cacheable=True), + "image") + return self.info["image"][size] + def get_title(self, properly_capitalized=False): """Returns the artist or track title.""" if properly_capitalized: @@ -1492,24 +1510,9 @@ class Album(_Opus): __hash__ = _Opus.__hash__ - def __init__(self, artist, title, network, username=None, images=None): + def __init__(self, artist, title, network, username=None, info=None): super(Album, self).__init__(artist, title, network, "album", username, - images) - - def get_cover_image(self, size=SIZE_EXTRA_LARGE): - """ - Returns a uri to the cover image - size can be one of: - SIZE_EXTRA_LARGE - SIZE_LARGE - SIZE_MEDIUM - SIZE_SMALL - """ - if not self.images: - self.images = _extract_all( - self._request(self.ws_prefix + ".getInfo", cacheable=True), - 'image') - return self.images[size] + info) def get_tracks(self): """Returns the list of Tracks on this album.""" @@ -1552,17 +1555,21 @@ class Artist(_BaseObject, _Taggable): __hash__ = _BaseObject.__hash__ - def __init__(self, name, network, username=None): + def __init__(self, name, network, username=None, info=None): """Create an artist object. # Parameters: * name str: The artist's name. """ + if info is None: + info = {} + _BaseObject.__init__(self, network, 'artist') _Taggable.__init__(self, 'artist') self.name = name self.username = username + self.info = info def __repr__(self): return "pylast.Artist(%s, %s)" % ( @@ -1606,7 +1613,7 @@ class Artist(_BaseObject, _Taggable): def get_cover_image(self, size=SIZE_EXTRA_LARGE): """ - Returns a uri to the cover image + Returns a URI to the cover image size can be one of: SIZE_MEGA SIZE_EXTRA_LARGE @@ -1615,8 +1622,11 @@ class Artist(_BaseObject, _Taggable): SIZE_SMALL """ - return _extract_all( - self._request(self.ws_prefix + ".getInfo", True), "image")[size] + if "image" not in self.info: + self.info["image"] = _extract_all( + self._request(self.ws_prefix + ".getInfo", cacheable=True), + "image") + return self.info["image"][size] def get_playcount(self): """Returns the number of plays on the network.""" @@ -1724,7 +1734,7 @@ class Artist(_BaseObject, _Taggable): "getTopTracks", "track", Track, params, cacheable) def get_url(self, domain_name=DOMAIN_ENGLISH): - """Returns the url of the artist page on the network. + """Returns the URL of the artist page on the network. # Parameters: * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH @@ -1800,7 +1810,7 @@ class Country(_BaseObject): "getTopTracks", "track", Track, params, cacheable) def get_url(self, domain_name=DOMAIN_ENGLISH): - """Returns the url of the country page on the network. + """Returns the URL of the country page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN @@ -1945,7 +1955,7 @@ class Tag(_BaseObject, _Chartable): return _extract_top_artists(doc, self.network) def get_url(self, domain_name=DOMAIN_ENGLISH): - """Returns the url of the tag page on the network. + """Returns the URL of the tag page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN @@ -1971,8 +1981,9 @@ class Track(_Opus): __hash__ = _Opus.__hash__ - def __init__(self, artist, title, network, username=None): - super(Track, self).__init__(artist, title, network, "track", username) + def __init__(self, artist, title, network, username=None, info=None): + super(Track, self).__init__(artist, title, network, "track", username, + info) def get_correction(self): """Returns the corrected track name.""" @@ -2454,7 +2465,7 @@ class User(_BaseObject, _Chartable): return _extract_all(doc, "image")[size] def get_url(self, domain_name=DOMAIN_ENGLISH): - """Returns the url of the user page on the network. + """Returns the URL of the user page on the network. * domain_name: The network's language domain. Possible values: o DOMAIN_ENGLISH o DOMAIN_GERMAN @@ -2550,11 +2561,14 @@ class AlbumSearch(_Search): seq = [] for node in master_node.getElementsByTagName("album"): - seq.append(Album( - _extract(node, "artist"), - _extract(node, "name"), - self.network, - images=_extract_all(node, 'image'))) + seq.append( + Album( + _extract(node, "artist"), + _extract(node, "name"), + self.network, + info={"image": _extract_all(node, "image")}, + ), + ) return seq @@ -2572,7 +2586,11 @@ class ArtistSearch(_Search): seq = [] for node in master_node.getElementsByTagName("artist"): - artist = Artist(_extract(node, "name"), self.network) + artist = Artist( + _extract(node, "name"), + self.network, + info={"image": _extract_all(node, "image")}, + ) artist.listener_count = _number(_extract(node, "listeners")) seq.append(artist) @@ -2603,7 +2621,9 @@ class TrackSearch(_Search): track = Track( _extract(node, "artist"), _extract(node, "name"), - self.network) + self.network, + info={"image": _extract_all(node, "image")}, + ) track.listener_count = _number(_extract(node, "listeners")) seq.append(track) @@ -2767,7 +2787,7 @@ def _extract_tracks(doc, network): def _url_safe(text): - """Does all kinds of tricks on a text to make it safe to use in a url.""" + """Does all kinds of tricks on a text to make it safe to use in a URL.""" return url_quote_plus(url_quote_plus(_string(text))).lower() diff --git a/tests/test_album.py b/tests/test_album.py index 2c7f369..dd76de8 100755 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -6,10 +6,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastAlbum(PyLastTestCase): +class TestPyLastAlbum(TestPyLastWithLastFm): def test_album_tags_are_topitems(self): # Arrange @@ -107,8 +107,8 @@ class TestPyLastAlbum(PyLastTestCase): image = album.get_cover_image() # Assert - self.assertTrue(image.startswith("https://")) - self.assertTrue(image.endswith(".png")) + self.assert_startswith(image, "https://") + self.assert_endswith(image, ".png") if __name__ == '__main__': diff --git a/tests/test_artist.py b/tests/test_artist.py index 58612ea..a4f2cfd 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -6,10 +6,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastArtist(PyLastTestCase): +class TestPyLastArtist(TestPyLastWithLastFm): def test_repr(self): # Arrange diff --git a/tests/test_country.py b/tests/test_country.py index 4f6b25f..a755db3 100755 --- a/tests/test_country.py +++ b/tests/test_country.py @@ -6,10 +6,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastCountry(PyLastTestCase): +class TestPyLastCountry(TestPyLastWithLastFm): def test_country_is_hashable(self): # Arrange diff --git a/tests/test_library.py b/tests/test_library.py index 1e437f1..a0aceb5 100755 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -6,10 +6,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastLibrary(PyLastTestCase): +class TestPyLastLibrary(TestPyLastWithLastFm): def test_repr(self): # Arrange @@ -19,7 +19,7 @@ class TestPyLastLibrary(PyLastTestCase): representation = repr(library) # Assert - self.assertTrue(representation.startswith("pylast.Library(")) + self.assert_startswith(representation, "pylast.Library(") def test_str(self): # Arrange @@ -29,7 +29,7 @@ class TestPyLastLibrary(PyLastTestCase): string = str(library) # Assert - self.assertTrue(string.endswith("'s Library")) + self.assert_endswith(string, "'s Library") def test_library_is_hashable(self): # Arrange diff --git a/tests/test_librefm.py b/tests/test_librefm.py index 7c958cb..1f50743 100755 --- a/tests/test_librefm.py +++ b/tests/test_librefm.py @@ -8,11 +8,11 @@ from flaky import flaky import pylast -from .test_pylast import load_secrets +from .test_pylast import PyLastTestCase, load_secrets -@flaky(max_runs=5, min_passes=1) -class TestPyLastWithLibreFm(unittest.TestCase): +@flaky(max_runs=3, min_passes=1) +class TestPyLastWithLibreFm(PyLastTestCase): """Own class for Libre.fm because we don't need the Last.fm setUp""" def test_libre_fm(self): @@ -42,7 +42,7 @@ class TestPyLastWithLibreFm(unittest.TestCase): representation = repr(network) # Assert - self.assertTrue(representation.startswith("pylast.LibreFMNetwork(")) + self.assert_startswith(representation, "pylast.LibreFMNetwork(") if __name__ == '__main__': diff --git a/tests/test_network.py b/tests/test_network.py index ab5dad2..4cd1a16 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -7,10 +7,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastNetwork(PyLastTestCase): +class TestPyLastNetwork(TestPyLastWithLastFm): def test_scrobble(self): # Arrange @@ -322,17 +322,17 @@ class TestPyLastNetwork(PyLastTestCase): # Act results = search.get_next_page() - images = results[0].images + images = results[0].info["image"] # Assert self.assertEqual(len(images), 4) - self.assertTrue(images[pylast.SIZE_SMALL].startswith("https://")) - self.assertTrue(images[pylast.SIZE_SMALL].endswith(".png")) + self.assert_startswith(images[pylast.SIZE_SMALL], "https://") + self.assert_endswith(images[pylast.SIZE_SMALL], ".png") self.assertIn("/34s/", images[pylast.SIZE_SMALL]) - self.assertTrue(images[pylast.SIZE_EXTRA_LARGE].startswith("https://")) - self.assertTrue(images[pylast.SIZE_EXTRA_LARGE].endswith(".png")) + self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") + self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") self.assertIn("/300x300/", images[pylast.SIZE_EXTRA_LARGE]) def test_artist_search(self): @@ -347,6 +347,26 @@ class TestPyLastNetwork(PyLastTestCase): self.assertIsInstance(results, list) self.assertIsInstance(results[0], pylast.Artist) + def test_artist_search_images(self): + # Arrange + artist = "Nirvana" + search = self.network.search_for_artist(artist) + + # Act + results = search.get_next_page() + images = results[0].info["image"] + + # Assert + self.assertEqual(len(images), 5) + + self.assert_startswith(images[pylast.SIZE_SMALL], "https://") + self.assert_endswith(images[pylast.SIZE_SMALL], ".png") + self.assertIn("/34s/", images[pylast.SIZE_SMALL]) + + self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") + self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") + self.assertIn("/300x300/", images[pylast.SIZE_EXTRA_LARGE]) + def test_track_search(self): # Arrange artist = "Nirvana" @@ -360,6 +380,27 @@ class TestPyLastNetwork(PyLastTestCase): self.assertIsInstance(results, list) self.assertIsInstance(results[0], pylast.Track) + def test_track_search_images(self): + # Arrange + artist = "Nirvana" + track = "Smells Like Teen Spirit" + search = self.network.search_for_track(artist, track) + + # Act + results = search.get_next_page() + images = results[0].info["image"] + + # Assert + self.assertEqual(len(images), 4) + + self.assert_startswith(images[pylast.SIZE_SMALL], "https://") + self.assert_endswith(images[pylast.SIZE_SMALL], ".png") + self.assertIn("/34s/", images[pylast.SIZE_SMALL]) + + self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") + self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") + self.assertIn("/300x300/", images[pylast.SIZE_EXTRA_LARGE]) + def test_search_get_total_result_count(self): # Arrange artist = "Nirvana" diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 9ebbcc1..aba7688 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -30,9 +30,18 @@ def load_secrets(): return doc -@flaky(max_runs=5, min_passes=1) class PyLastTestCase(unittest.TestCase): + def assert_startswith(self, str, prefix, start=None, end=None): + self.assertTrue(str.startswith(prefix, start, end)) + + def assert_endswith(self, str, suffix, start=None, end=None): + self.assertTrue(str.endswith(suffix, start, end)) + + +@flaky(max_runs=3, min_passes=1) +class TestPyLastWithLastFm(PyLastTestCase): + secrets = None def unix_timestamp(self): diff --git a/tests/test_tag.py b/tests/test_tag.py index e56aac5..d3980fb 100755 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -6,10 +6,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastTag(PyLastTestCase): +class TestPyLastTag(TestPyLastWithLastFm): def test_tag_is_hashable(self): # Arrange diff --git a/tests/test_track.py b/tests/test_track.py index bb34f56..b68c065 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -6,10 +6,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastTrack(PyLastTestCase): +class TestPyLastTrack(TestPyLastWithLastFm): def test_love(self): # Arrange diff --git a/tests/test_user.py b/tests/test_user.py index c742bd3..7b0e636 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -7,10 +7,10 @@ import unittest import pylast -from .test_pylast import PyLastTestCase +from .test_pylast import TestPyLastWithLastFm -class TestPyLastUser(PyLastTestCase): +class TestPyLastUser(TestPyLastWithLastFm): def test_repr(self): # Arrange @@ -20,7 +20,7 @@ class TestPyLastUser(PyLastTestCase): representation = repr(user) # Assert - self.assertTrue(representation.startswith("pylast.User('RJ',")) + self.assert_startswith(representation, "pylast.User('RJ',") def test_str(self): # Arrange @@ -342,7 +342,7 @@ class TestPyLastUser(PyLastTestCase): url = user.get_image() # Assert - self.assertTrue(url.startswith("https://")) + self.assert_startswith(url, "https://") def test_user_get_library(self): # Arrange @@ -392,8 +392,8 @@ class TestPyLastUser(PyLastTestCase): image = user.get_image() # Assert - self.assertTrue(image.startswith("https://")) - self.assertTrue(image.endswith(".png")) + self.assert_startswith(image, "https://") + self.assert_endswith(image, ".png") def test_get_url(self): # Arrange