From 92004058ba66be45301ffe4adc2033bea64c4bbf Mon Sep 17 00:00:00 2001 From: kvanzuijlen Date: Sun, 12 Jul 2020 11:54:46 +0200 Subject: [PATCH 01/11] Added option to stream from resources to reduce memory usage --- README.md | 2 +- src/pylast/__init__.py | 293 +++++++++++++++++++---------------------- tests/test_album.py | 2 +- tests/test_artist.py | 12 +- tests/test_network.py | 8 +- tests/test_pylast.py | 15 ++- tests/test_track.py | 8 +- tests/test_user.py | 26 ++-- 8 files changed, 174 insertions(+), 192 deletions(-) diff --git a/README.md b/README.md index 806c718..1ec2e8f 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE To run all unit and integration tests: ```sh -pip install pytest flaky +pip install .[tests] pytest ``` diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index fb2de22..f3a61f6 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1149,21 +1149,22 @@ class _BaseObject: return first_child.wholeText.strip() - def _get_things(self, method, thing, thing_type, params=None, cacheable=True): + def _get_things(self, method, thing, thing_type, params=None, cacheable=True, stream=False): """Returns a list of the most played thing_types by this thing.""" - limit = params.get("limit", 1) - seq = [] - for node in _collect_nodes( - limit, self, self.ws_prefix + "." + method, cacheable, params - ): - title = _extract(node, "name") - artist = _extract(node, "name", 1) - playcount = _number(_extract(node, "playcount")) + def _stream_get_things(): + limit = params.get("limit", 1) + nodes = _collect_nodes( + limit, self, self.ws_prefix + "." + method, cacheable, params, stream=stream, + ) + for node in nodes: + title = _extract(node, "name") + artist = _extract(node, "name", 1) + playcount = _number(_extract(node, "playcount")) - seq.append(TopItem(thing_type(artist, title, self.network), playcount)) + yield TopItem(thing_type(artist, title, self.network), playcount) - return seq + return _stream_get_things() if stream else list(_stream_get_things()) def get_wiki_published_date(self): """ @@ -1835,21 +1836,21 @@ class Artist(_BaseObject, _Taggable): return artists - def get_top_albums(self, limit=None, cacheable=True): + def get_top_albums(self, limit=None, cacheable=True, stream=True): """Returns a list of the top albums.""" params = self._get_params() if limit: params["limit"] = limit - return self._get_things("getTopAlbums", "album", Album, params, cacheable) + return self._get_things("getTopAlbums", "album", Album, params, cacheable, stream=stream) - def get_top_tracks(self, limit=None, cacheable=True): + def get_top_tracks(self, limit=None, cacheable=True, stream=True): """Returns a list of the most played Tracks by this artist.""" params = self._get_params() if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable) + return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the URL of the artist page on the network. @@ -1917,13 +1918,13 @@ class Country(_BaseObject): return _extract_top_artists(doc, self) - def get_top_tracks(self, limit=None, cacheable=True): + def get_top_tracks(self, limit=None, cacheable=True, stream=True): """Returns a sequence of the most played tracks""" params = self._get_params() if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable) + return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the URL of the country page on the network. @@ -1978,24 +1979,24 @@ class Library(_BaseObject): """Returns the user who owns this library.""" return self.user - def get_artists(self, limit=50, cacheable=True): + def get_artists(self, limit=50, cacheable=True, stream=True): """ Returns a sequence of Album objects if limit==None it will return all (may take a while) """ - seq = [] - for node in _collect_nodes( - limit, self, self.ws_prefix + ".getArtists", cacheable - ): - name = _extract(node, "name") + def _get_artists(): + for node in _collect_nodes( + limit, self, self.ws_prefix + ".getArtists", cacheable, stream=stream + ): + name = _extract(node, "name") - playcount = _number(_extract(node, "playcount")) - tagcount = _number(_extract(node, "tagcount")) + playcount = _number(_extract(node, "playcount")) + tagcount = _number(_extract(node, "tagcount")) - seq.append(LibraryItem(Artist(name, self.network), playcount, tagcount)) + yield LibraryItem(Artist(name, self.network), playcount, tagcount) - return seq + return _get_artists() if stream else list(_get_artists()) class Tag(_BaseObject, _Chartable): @@ -2047,13 +2048,13 @@ class Tag(_BaseObject, _Chartable): return _extract_top_albums(doc, self.network) - def get_top_tracks(self, limit=None, cacheable=True): + def get_top_tracks(self, limit=None, cacheable=True, stream=True): """Returns a list of the most played Tracks for this tag.""" params = self._get_params() if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable) + return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) def get_top_artists(self, limit=None, cacheable=True): """Returns a sequence of the most played artists.""" @@ -2241,6 +2242,14 @@ class User(_BaseObject, _Chartable): def _get_params(self): return {self.ws_prefix: self.get_name()} + def _extract_played_track(self, track_node): + title = _extract(track_node, "name") + track_artist = _extract(track_node, "artist") + date = _extract(track_node, "date") + album = _extract(track_node, "album") + timestamp = track_node.getElementsByTagName("date")[0].getAttribute("uts") + return PlayedTrack(Track(track_artist, title, self.network), album, date, timestamp) + def get_name(self, properly_capitalized=False): """Returns the user name.""" @@ -2251,7 +2260,7 @@ class User(_BaseObject, _Chartable): return self.name - def get_artist_tracks(self, artist, cacheable=False): + def get_artist_tracks(self, artist, cacheable=False, stream=True): """ Deprecated by Last.fm. Get a list of tracks by a given artist scrobbled by this user, @@ -2269,63 +2278,56 @@ class User(_BaseObject, _Chartable): params = self._get_params() params["artist"] = artist - seq = [] - for track in _collect_nodes( - None, self, self.ws_prefix + ".getArtistTracks", cacheable, params - ): - title = _extract(track, "name") - artist = _extract(track, "artist") - date = _extract(track, "date") - album = _extract(track, "album") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + def _get_artist_tracks(): + for track_node in _collect_nodes( + None, self, self.ws_prefix + ".getArtistTracks", cacheable, params, stream=stream, + ): + yield self._extract_played_track(track_node=track_node) - seq.append( - PlayedTrack(Track(artist, title, self.network), album, date, timestamp) - ) + return _get_artist_tracks() if stream else list(_get_artist_tracks()) - return seq - - def get_friends(self, limit=50, cacheable=False): + def get_friends(self, limit=50, cacheable=False, stream=True): """Returns a list of the user's friends. """ - seq = [] - for node in _collect_nodes( - limit, self, self.ws_prefix + ".getFriends", cacheable - ): - seq.append(User(_extract(node, "name"), self.network)) + def _get_friends(): + for node in _collect_nodes( + limit, self, self.ws_prefix + ".getFriends", cacheable, stream=stream + ): + yield User(_extract(node, "name"), self.network) - return seq + return _get_friends() if stream else list(_get_friends()) - def get_loved_tracks(self, limit=50, cacheable=True): + def get_loved_tracks(self, limit=50, cacheable=True, stream=True): """ Returns this user's loved track as a sequence of LovedTrack objects in reverse order of their timestamp, all the way back to the first track. If limit==None, it will try to pull all the available data. + If stream=True, it will yield tracks as soon as a page has been retrieved. This method uses caching. Enable caching only if you're pulling a large amount of data. """ - params = self._get_params() - if limit: - params["limit"] = limit + def _get_loved_tracks(): + params = self._get_params() + if limit: + params["limit"] = limit - seq = [] - for track in _collect_nodes( - limit, self, self.ws_prefix + ".getLovedTracks", cacheable, params - ): - try: - artist = _extract(track, "name", 1) - except IndexError: # pragma: no cover - continue - title = _extract(track, "name") - date = _extract(track, "date") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + for track in _collect_nodes( + limit, self, self.ws_prefix + ".getLovedTracks", cacheable, params, stream=stream + ): + try: + artist = _extract(track, "name", 1) + except IndexError: # pragma: no cover + continue + title = _extract(track, "name") + date = _extract(track, "date") + timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") - seq.append(LovedTrack(Track(artist, title, self.network), date, timestamp)) + yield LovedTrack(Track(artist, title, self.network), date, timestamp) - return seq + return _get_loved_tracks() if stream else list(_get_loved_tracks()) def get_now_playing(self): """ @@ -2353,7 +2355,7 @@ class User(_BaseObject, _Chartable): return Track(artist, title, self.network, self.name, info=info) - def get_recent_tracks(self, limit=10, cacheable=True, time_from=None, time_to=None): + def get_recent_tracks(self, limit=10, cacheable=True, time_from=None, time_to=None, stream=True): """ Returns this user's played track as a sequence of PlayedTrack objects in reverse order of playtime, all the way back to the first track. @@ -2368,45 +2370,35 @@ class User(_BaseObject, _Chartable): before this time, in UNIX timestamp format (integer number of seconds since 00:00:00, January 1st 1970 UTC). This must be in the UTC time zone. + stream: If True, it will yield tracks as soon as a page has been retrieved. This method uses caching. Enable caching only if you're pulling a large amount of data. """ - params = self._get_params() - if limit: - params["limit"] = limit + 1 # in case we remove the now playing track - if time_from: - params["from"] = time_from - if time_to: - params["to"] = time_to + def _get_recent_tracks(): + params = self._get_params() + if limit: + params["limit"] = limit + 1 # in case we remove the now playing track + if time_from: + params["from"] = time_from + if time_to: + params["to"] = time_to - seq = [] - for track in _collect_nodes( - limit + 1 if limit else None, - self, - self.ws_prefix + ".getRecentTracks", - cacheable, - params, - ): + for track_node in _collect_nodes( + limit + 1 if limit else None, + self, + self.ws_prefix + ".getRecentTracks", + cacheable, + params, + stream=stream + ): + if track_node.hasAttribute("nowplaying"): + continue # to prevent the now playing track from sneaking in - if track.hasAttribute("nowplaying"): - continue # to prevent the now playing track from sneaking in + yield self._extract_played_track(track_node=track_node) - title = _extract(track, "name") - artist = _extract(track, "artist") - date = _extract(track, "date") - album = _extract(track, "album") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") - - seq.append( - PlayedTrack(Track(artist, title, self.network), album, date, timestamp) - ) - - if limit: - # Slice, in case we didn't remove a now playing track - seq = seq[:limit] - return seq + return _get_recent_tracks() if stream else list(_get_recent_tracks()) def get_country(self): """Returns the name of the country of the user.""" @@ -2545,7 +2537,7 @@ class User(_BaseObject, _Chartable): return seq - def get_top_tracks(self, period=PERIOD_OVERALL, limit=None, cacheable=True): + def get_top_tracks(self, period=PERIOD_OVERALL, limit=None, cacheable=True, stream=True): """Returns the top tracks played by a user. * period: The period of time. Possible values: o PERIOD_OVERALL @@ -2561,33 +2553,24 @@ class User(_BaseObject, _Chartable): if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable) + return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) - def get_track_scrobbles(self, artist, track, cacheable=False): + def get_track_scrobbles(self, artist, track, cacheable=False, stream=True): """ Get a list of this user's scrobbles of this artist's track, including scrobble time. """ - params = self._get_params() params["artist"] = artist params["track"] = track - seq = [] - for track in _collect_nodes( - None, self, self.ws_prefix + ".getTrackScrobbles", cacheable, params - ): - title = _extract(track, "name") - artist = _extract(track, "artist") - date = _extract(track, "date") - album = _extract(track, "album") - timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") + def _get_track_scrobbles(): + for track_node in _collect_nodes( + None, self, self.ws_prefix + ".getTrackScrobbles", cacheable, params, stream=stream + ): + yield self._extract_played_track(track_node) - seq.append( - PlayedTrack(Track(artist, title, self.network), album, date, timestamp) - ) - - return seq + return _get_track_scrobbles() if stream else list(_get_track_scrobbles()) def get_image(self, size=SIZE_EXTRA_LARGE): """ @@ -2797,59 +2780,57 @@ def cleanup_nodes(doc): return doc -def _collect_nodes(limit, sender, method_name, cacheable, params=None): +def _collect_nodes(limit, sender, method_name, cacheable, params=None, stream=False): """ Returns a sequence of dom.Node objects about as close to limit as possible """ - if not params: params = sender._get_params() - nodes = [] - page = 1 - end_of_pages = False + def _stream_collect_nodes(): + node_count = 0 + page = 1 + end_of_pages = False - while not end_of_pages and (not limit or (limit and len(nodes) < limit)): - params["page"] = str(page) + while not end_of_pages and (not limit or (limit and node_count < limit)): + params["page"] = str(page) - tries = 1 - while True: - try: - doc = sender._request(method_name, cacheable, params) - break # success - except Exception as e: - if tries >= 3: - raise e - # Wait and try again - time.sleep(1) - tries += 1 + tries = 1 + while True: + try: + doc = sender._request(method_name, cacheable, params) + break # success + except Exception as e: + if tries >= 3: + raise e + # Wait and try again + time.sleep(1) + tries += 1 - doc = cleanup_nodes(doc) + doc = cleanup_nodes(doc) - # break if there are no child nodes - if not doc.documentElement.childNodes: - break - main = doc.documentElement.childNodes[0] + # break if there are no child nodes + if not doc.documentElement.childNodes: + break + main = doc.documentElement.childNodes[0] - if main.hasAttribute("totalPages"): - total_pages = _number(main.getAttribute("totalPages")) - elif main.hasAttribute("totalpages"): - total_pages = _number(main.getAttribute("totalpages")) - else: - raise Exception("No total pages attribute") + if main.hasAttribute("totalPages") or main.hasAttribute("totalpages"): + total_pages = _number(main.getAttribute("totalPages") or main.getAttribute("totalpages")) + else: + raise Exception("No total pages attribute") - for node in main.childNodes: - if not node.nodeType == xml.dom.Node.TEXT_NODE and ( - not limit or (len(nodes) < limit) - ): - nodes.append(node) + for node in main.childNodes: + if not node.nodeType == xml.dom.Node.TEXT_NODE and ( + not limit or (node_count < limit) + ): + node_count += 1 + yield node - if page >= total_pages: - end_of_pages = True + end_of_pages = page >= total_pages - page += 1 + page += 1 - return nodes + return _stream_collect_nodes() if stream else list(_stream_collect_nodes()) def _extract(node, name, index=0): diff --git a/tests/test_album.py b/tests/test_album.py index 8a213b5..d6bf3e1 100755 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -32,7 +32,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): # Act # limit=2 to ignore now-playing: - track = lastfm_user.get_recent_tracks(limit=2)[0] + track = list(lastfm_user.get_recent_tracks(limit=2))[0] # Assert assert hasattr(track, "album") diff --git a/tests/test_artist.py b/tests/test_artist.py index 8250f5b..8f9a97e 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -2,9 +2,9 @@ """ Integration (not unit) tests for pylast.py """ -import pylast import pytest +import pylast from .test_pylast import WRITE_TEST, TestPyLastWithLastFm @@ -78,7 +78,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_tracks(limit=2) + things = artist.get_top_tracks(limit=2, stream=False) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) @@ -89,7 +89,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_albums(limit=2) + things = list(artist.get_top_albums(limit=2)) # Assert self.helper_two_different_things_in_top_list(things, pylast.Album) @@ -101,7 +101,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_albums(limit=limit) + things = artist.get_top_albums(limit=limit, stream=False) # Assert assert len(things) == 1 @@ -113,7 +113,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_albums(limit=limit) + things = artist.get_top_albums(limit=limit, stream=False) # Assert assert len(things) == 50 @@ -125,7 +125,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_albums(limit=limit) + things = list(artist.get_top_albums(limit=limit)) # Assert assert len(things) == 100 diff --git a/tests/test_network.py b/tests/test_network.py index bad8c54..7c2e68d 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -5,9 +5,9 @@ Integration (not unit) tests for pylast.py import re import time -import pylast import pytest +import pylast from .test_pylast import WRITE_TEST, TestPyLastWithLastFm @@ -26,7 +26,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert # limit=2 to ignore now-playing: - last_scrobble = lastfm_user.get_recent_tracks(limit=2)[0] + last_scrobble = list(lastfm_user.get_recent_tracks(limit=2))[0] assert str(last_scrobble.track.artist).lower() == artist assert str(last_scrobble.track.title).lower() == title @@ -153,7 +153,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): country = self.network.get_country("Croatia") # Act - things = country.get_top_tracks(limit=2) + things = country.get_top_tracks(limit=2, stream=False) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) @@ -171,7 +171,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): tag = self.network.get_tag("blues") # Act - things = tag.get_top_tracks(limit=2) + things = tag.get_top_tracks(limit=2, stream=False) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 83a64ad..20d1604 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -6,10 +6,11 @@ import os import sys import time -import pylast import pytest from flaky import flaky +import pylast + WRITE_TEST = sys.version_info[:2] == (3, 8) @@ -82,9 +83,9 @@ class TestPyLastWithLastFm(PyLastTestCase): assert a is not None assert b is not None assert c is not None - assert len(a) >= 0 - assert len(b) >= 0 - assert len(c) >= 0 + assert isinstance(len(a), int) + assert isinstance(len(b), int) + assert isinstance(len(c), int) assert a == b assert b == c @@ -94,9 +95,9 @@ class TestPyLastWithLastFm(PyLastTestCase): func = getattr(thing, function_name, None) # Act - result1 = func(limit=1, cacheable=False) - result2 = func(limit=1, cacheable=True) - result3 = func(limit=1) + result1 = func(limit=1, cacheable=False, stream=False) + result2 = func(limit=1, cacheable=True, stream=False) + result3 = list(func(limit=1)) # Assert self.helper_validate_results(result1, result2, result3) diff --git a/tests/test_track.py b/tests/test_track.py index 3bfe995..8ab6faa 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -4,9 +4,9 @@ Integration (not unit) tests for pylast.py """ import time -import pylast import pytest +import pylast from .test_pylast import WRITE_TEST, TestPyLastWithLastFm @@ -23,7 +23,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): track.love() # Assert - loved = lastfm_user.get_loved_tracks(limit=1) + loved = list(lastfm_user.get_loved_tracks(limit=1)) assert str(loved[0].track.artist).lower() == "test artist" assert str(loved[0].track.title).lower() == "test title" @@ -41,7 +41,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): time.sleep(1) # Delay, for Last.fm latency. TODO Can this be removed later? # Assert - loved = lastfm_user.get_loved_tracks(limit=1) + loved = list(lastfm_user.get_loved_tracks(limit=1)) if len(loved): # OK to be empty but if not: assert str(loved[0].track.artist) != "Test Artist" assert str(loved[0].track.title) != "test title" @@ -79,7 +79,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): def test_track_is_hashable(self): # Arrange artist = self.network.get_artist("Test Artist") - track = artist.get_top_tracks()[0].item + track = artist.get_top_tracks(stream=False)[0].item assert isinstance(track, pylast.Track) # Act/Assert diff --git a/tests/test_user.py b/tests/test_user.py index b0ae898..5415cc8 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -8,9 +8,9 @@ import os import re import warnings -import pylast import pytest +import pylast from .test_pylast import TestPyLastWithLastFm @@ -142,10 +142,10 @@ class TestPyLastUser(TestPyLastWithLastFm): user = self.network.get_user("test-user") # Act/Assert - assert len(user.get_loved_tracks(limit=20)) == 20 - assert len(user.get_loved_tracks(limit=100)) <= 100 - assert len(user.get_loved_tracks(limit=None)) >= 23 - assert len(user.get_loved_tracks(limit=0)) >= 23 + assert len(user.get_loved_tracks(limit=20, stream=False)) == 20 + assert len(user.get_loved_tracks(limit=100, stream=False)) <= 100 + assert len(user.get_loved_tracks(limit=None, stream=False)) >= 23 + assert len(user.get_loved_tracks(limit=0, stream=False)) >= 23 def test_user_is_hashable(self): # Arrange @@ -210,7 +210,7 @@ class TestPyLastUser(TestPyLastWithLastFm): lastfm_user = self.network.get_user("RJ") # Act - things = lastfm_user.get_top_tracks(limit=2) + things = lastfm_user.get_top_tracks(limit=2, stream=False) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) @@ -361,7 +361,7 @@ class TestPyLastUser(TestPyLastWithLastFm): utc_end = calendar.timegm(end.utctimetuple()) # Act - tracks = lastfm_user.get_recent_tracks(time_from=utc_start, time_to=utc_end) + tracks = lastfm_user.get_recent_tracks(time_from=utc_start, time_to=utc_end, stream=False) # Assert assert len(tracks) == 1 @@ -379,7 +379,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Act tracks = lastfm_user.get_recent_tracks( - time_from=utc_start, time_to=utc_end, limit=None + time_from=utc_start, time_to=utc_end, limit=None, stream=False ) # Assert @@ -449,7 +449,7 @@ class TestPyLastUser(TestPyLastWithLastFm): user = self.network.get_user("bbc6music") # Act - scrobbles = user.get_track_scrobbles(artist, title) + scrobbles = user.get_track_scrobbles(artist, title, stream=False) # Assert assert len(scrobbles) > 0 @@ -463,9 +463,9 @@ class TestPyLastUser(TestPyLastWithLastFm): user = self.network.get_user("bbc6music") # Act - result1 = user.get_track_scrobbles(artist, title, cacheable=False) - result2 = user.get_track_scrobbles(artist, title, cacheable=True) - result3 = user.get_track_scrobbles(artist, title) + result1 = user.get_track_scrobbles(artist, title, cacheable=False, stream=False) + result2 = list(user.get_track_scrobbles(artist, title, cacheable=True)) + result3 = list(user.get_track_scrobbles(artist, title)) # Assert self.helper_validate_results(result1, result2, result3) @@ -480,4 +480,4 @@ class TestPyLastUser(TestPyLastWithLastFm): match="Deprecated - This type of request is no longer supported", ): warnings.filterwarnings("ignore", category=DeprecationWarning) - lastfm_user.get_artist_tracks(artist="Test Artist") + lastfm_user.get_artist_tracks(artist="Test Artist", stream=False) From 11d955dd89d1597110bf6752016682451b267541 Mon Sep 17 00:00:00 2001 From: kvanzuijlen Date: Sun, 12 Jul 2020 12:21:40 +0200 Subject: [PATCH 02/11] Now playing shouldn't count as a recently played track --- src/pylast/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index f3a61f6..938cc7b 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -2385,6 +2385,7 @@ class User(_BaseObject, _Chartable): if time_to: params["to"] = time_to + track_count = 0 for track_node in _collect_nodes( limit + 1 if limit else None, self, @@ -2396,7 +2397,10 @@ class User(_BaseObject, _Chartable): if track_node.hasAttribute("nowplaying"): continue # to prevent the now playing track from sneaking in + if limit and track_count >= limit: + break yield self._extract_played_track(track_node=track_node) + track_count += 1 return _get_recent_tracks() if stream else list(_get_recent_tracks()) From 99fb7cd7a5d2960420093346637085a54e72e35d Mon Sep 17 00:00:00 2001 From: kvanzuijlen Date: Sun, 12 Jul 2020 12:23:32 +0200 Subject: [PATCH 03/11] Added a parameter to choose whether to include now playing or not --- src/pylast/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 938cc7b..b0a49c5 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -2355,7 +2355,7 @@ class User(_BaseObject, _Chartable): return Track(artist, title, self.network, self.name, info=info) - def get_recent_tracks(self, limit=10, cacheable=True, time_from=None, time_to=None, stream=True): + def get_recent_tracks(self, limit=10, cacheable=True, time_from=None, time_to=None, stream=True, now_playing=False): """ Returns this user's played track as a sequence of PlayedTrack objects in reverse order of playtime, all the way back to the first track. @@ -2394,7 +2394,7 @@ class User(_BaseObject, _Chartable): params, stream=stream ): - if track_node.hasAttribute("nowplaying"): + if track_node.hasAttribute("nowplaying") and not now_playing: continue # to prevent the now playing track from sneaking in if limit and track_count >= limit: From 15672922a7a4be69829d746f48215347543c98ca Mon Sep 17 00:00:00 2001 From: kvanzuijlen Date: Sun, 12 Jul 2020 12:24:06 +0200 Subject: [PATCH 04/11] General code improvements --- src/pylast/__init__.py | 74 ++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index b0a49c5..48baf68 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -28,12 +28,12 @@ import ssl import tempfile import time import warnings -import xml.dom from http.client import HTTPSConnection from urllib.parse import quote_plus -from xml.dom import Node, minidom import pkg_resources +import xml.dom +from xml.dom import Node, minidom __author__ = "Amr Hassan, hugovk, Mice Pápai" __copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2020 hugovk, 2017 Mice Pápai" @@ -424,7 +424,9 @@ class _Network: """ if not file_path: - file_path = tempfile.mktemp(prefix="pylast_tmp_") + file = tempfile.TemporaryFile(prefix="pylast_tmp_") + file.close() + file_path = file.name self.cache_backend = _ShelfCacheBackend(file_path) @@ -668,8 +670,7 @@ class LastFMNetwork(_Network): password_hash="", token="", ): - _Network.__init__( - self, + super().__init__( name="Last.fm", homepage="https://www.last.fm", ws_server=("ws.audioscrobbler.com", "/2.0/"), @@ -736,8 +737,7 @@ class LibreFMNetwork(_Network): self, api_key="", api_secret="", session_key="", username="", password_hash="" ): - _Network.__init__( - self, + super().__init__( name="Libre.fm", homepage="https://libre.fm", ws_server=("libre.fm", "/2.0/"), @@ -1205,11 +1205,11 @@ class _BaseObject: return _extract(node, section) -class _Chartable: +class _Chartable(_BaseObject): """Common functions for classes with charts.""" - def __init__(self, ws_prefix): - self.ws_prefix = ws_prefix # TODO move to _BaseObject? + def __init__(self, network, ws_prefix): + super().__init__(network=network, ws_prefix=ws_prefix) def get_weekly_chart_dates(self): """Returns a list of From and To tuples for the available charts.""" @@ -1276,11 +1276,11 @@ class _Chartable: return seq -class _Taggable: +class _Taggable(_BaseObject): """Common functions for classes with tags.""" - def __init__(self, ws_prefix): - self.ws_prefix = ws_prefix # TODO move to _BaseObject + def __init__(self, network, ws_prefix): + super().__init__(network=network, ws_prefix=ws_prefix) def add_tags(self, tags): """Adds one or several tags. @@ -1385,9 +1385,9 @@ class _Taggable: for element in elements: tag_name = _extract(element, "name") - tagcount = _extract(element, "count") + tag_count = _extract(element, "count") - seq.append(TopItem(Tag(tag_name, self.network), tagcount)) + seq.append(TopItem(Tag(tag_name, self.network), tag_count)) if limit: seq = seq[:limit] @@ -1463,7 +1463,7 @@ class NetworkError(Exception): return "NetworkError: %s" % str(self.underlying_error) -class _Opus(_BaseObject, _Taggable): +class _Opus(_Taggable): """An album or track.""" artist = None @@ -1484,8 +1484,7 @@ class _Opus(_BaseObject, _Taggable): if info is None: info = {} - _BaseObject.__init__(self, network, ws_prefix) - _Taggable.__init__(self, ws_prefix) + super().__init__(network=network, ws_prefix=ws_prefix) if isinstance(artist, Artist): self.artist = artist @@ -1653,7 +1652,7 @@ class Album(_Opus): } -class Artist(_BaseObject, _Taggable): +class Artist(_Taggable): """An artist.""" name = None @@ -1670,8 +1669,7 @@ class Artist(_BaseObject, _Taggable): if info is None: info = {} - _BaseObject.__init__(self, network, "artist") - _Taggable.__init__(self, "artist") + super().__init__(network=network, ws_prefix="artist") self.name = name self.username = username @@ -1883,7 +1881,7 @@ class Country(_BaseObject): __hash__ = _BaseObject.__hash__ def __init__(self, name, network): - _BaseObject.__init__(self, network, "geo") + super().__init__(network=network, ws_prefix="geo") self.name = name @@ -1958,7 +1956,7 @@ class Library(_BaseObject): __hash__ = _BaseObject.__hash__ def __init__(self, user, network): - _BaseObject.__init__(self, network, "library") + super().__init__(network=network, ws_prefix="library") if isinstance(user, User): self.user = user @@ -1999,7 +1997,7 @@ class Library(_BaseObject): return _get_artists() if stream else list(_get_artists()) -class Tag(_BaseObject, _Chartable): +class Tag(_Chartable): """A Last.fm object tag.""" name = None @@ -2007,8 +2005,7 @@ class Tag(_BaseObject, _Chartable): __hash__ = _BaseObject.__hash__ def __init__(self, name, network): - _BaseObject.__init__(self, network, "tag") - _Chartable.__init__(self, "tag") + super().__init__(network=network, ws_prefix="tag") self.name = name @@ -2210,7 +2207,7 @@ class Track(_Opus): } -class User(_BaseObject, _Chartable): +class User(_Chartable): """A Last.fm user.""" name = None @@ -2218,8 +2215,7 @@ class User(_BaseObject, _Chartable): __hash__ = _BaseObject.__hash__ def __init__(self, user_name, network): - _BaseObject.__init__(self, network, "user") - _Chartable.__init__(self, "user") + super().__init__(network=network, ws_prefix="user") self.name = user_name @@ -2619,21 +2615,21 @@ class User(_BaseObject, _Chartable): class AuthenticatedUser(User): def __init__(self, network): - User.__init__(self, network.username, network) + super().__init__(user_name=network.username, network=network) def _get_params(self): return {"user": self.get_name()} - def get_name(self): + def get_name(self, properly_capitalized=False): """Returns the name of the authenticated user.""" - return self.name + return super().get_name(properly_capitalized=properly_capitalized) class _Search(_BaseObject): """An abstract class. Use one of its derivatives.""" def __init__(self, ws_prefix, search_terms, network): - _BaseObject.__init__(self, network, ws_prefix) + super().__init__(network, ws_prefix) self._ws_prefix = ws_prefix self.search_terms = search_terms @@ -2673,8 +2669,7 @@ class AlbumSearch(_Search): """Search for an album by name.""" def __init__(self, album_name, network): - - _Search.__init__(self, "album", {"album": album_name}, network) + super().__init__(ws_prefix="album", search_terms={"album": album_name}, network=network) def get_next_page(self): """Returns the next page of results as a sequence of Album objects.""" @@ -2699,7 +2694,7 @@ class ArtistSearch(_Search): """Search for an artist by artist name.""" def __init__(self, artist_name, network): - _Search.__init__(self, "artist", {"artist": artist_name}, network) + super().__init__(ws_prefix="artist", search_terms={"artist": artist_name}, network=network) def get_next_page(self): """Returns the next page of results as a sequence of Artist objects.""" @@ -2726,10 +2721,7 @@ class TrackSearch(_Search): """ def __init__(self, artist_name, track_title, network): - - _Search.__init__( - self, "track", {"track": track_title, "artist": artist_name}, network - ) + super().__init__(ws_prefix="track", search_terms={"track": track_title, "artist": artist_name}, network=network) def get_next_page(self): """Returns the next page of results as a sequence of Track objects.""" @@ -2928,8 +2920,6 @@ def _number(string): if not string: return 0 - elif string == "": - return 0 else: try: return int(string) From 52abbba2bdc4c193e2c4b7618d98ea3c0346b7a0 Mon Sep 17 00:00:00 2001 From: kvanzuijlen Date: Sun, 12 Jul 2020 13:13:22 +0200 Subject: [PATCH 05/11] tox lint changes --- src/pylast/__init__.py | 94 +++++++++++++++++++++++++++++++++--------- tests/test_artist.py | 2 +- tests/test_network.py | 2 +- tests/test_pylast.py | 3 +- tests/test_track.py | 2 +- tests/test_user.py | 24 ++++++++++- 6 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 48baf68..8b4cdd5 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -28,12 +28,12 @@ import ssl import tempfile import time import warnings +import xml.dom from http.client import HTTPSConnection from urllib.parse import quote_plus +from xml.dom import Node, minidom import pkg_resources -import xml.dom -from xml.dom import Node, minidom __author__ = "Amr Hassan, hugovk, Mice Pápai" __copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2020 hugovk, 2017 Mice Pápai" @@ -1149,13 +1149,20 @@ class _BaseObject: return first_child.wholeText.strip() - def _get_things(self, method, thing, thing_type, params=None, cacheable=True, stream=False): + def _get_things( + self, method, thing, thing_type, params=None, cacheable=True, stream=False + ): """Returns a list of the most played thing_types by this thing.""" def _stream_get_things(): limit = params.get("limit", 1) nodes = _collect_nodes( - limit, self, self.ws_prefix + "." + method, cacheable, params, stream=stream, + limit, + self, + self.ws_prefix + "." + method, + cacheable, + params, + stream=stream, ) for node in nodes: title = _extract(node, "name") @@ -1840,7 +1847,9 @@ class Artist(_Taggable): if limit: params["limit"] = limit - return self._get_things("getTopAlbums", "album", Album, params, cacheable, stream=stream) + return self._get_things( + "getTopAlbums", "album", Album, params, cacheable, stream=stream + ) def get_top_tracks(self, limit=None, cacheable=True, stream=True): """Returns a list of the most played Tracks by this artist.""" @@ -1848,7 +1857,9 @@ class Artist(_Taggable): if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) + return self._get_things( + "getTopTracks", "track", Track, params, cacheable, stream=stream + ) def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the URL of the artist page on the network. @@ -1922,7 +1933,9 @@ class Country(_BaseObject): if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) + return self._get_things( + "getTopTracks", "track", Track, params, cacheable, stream=stream + ) def get_url(self, domain_name=DOMAIN_ENGLISH): """Returns the URL of the country page on the network. @@ -2051,7 +2064,9 @@ class Tag(_Chartable): if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) + return self._get_things( + "getTopTracks", "track", Track, params, cacheable, stream=stream + ) def get_top_artists(self, limit=None, cacheable=True): """Returns a sequence of the most played artists.""" @@ -2244,7 +2259,9 @@ class User(_Chartable): date = _extract(track_node, "date") album = _extract(track_node, "album") timestamp = track_node.getElementsByTagName("date")[0].getAttribute("uts") - return PlayedTrack(Track(track_artist, title, self.network), album, date, timestamp) + return PlayedTrack( + Track(track_artist, title, self.network), album, date, timestamp + ) def get_name(self, properly_capitalized=False): """Returns the user name.""" @@ -2276,7 +2293,12 @@ class User(_Chartable): def _get_artist_tracks(): for track_node in _collect_nodes( - None, self, self.ws_prefix + ".getArtistTracks", cacheable, params, stream=stream, + None, + self, + self.ws_prefix + ".getArtistTracks", + cacheable, + params, + stream=stream, ): yield self._extract_played_track(track_node=track_node) @@ -2311,7 +2333,12 @@ class User(_Chartable): params["limit"] = limit for track in _collect_nodes( - limit, self, self.ws_prefix + ".getLovedTracks", cacheable, params, stream=stream + limit, + self, + self.ws_prefix + ".getLovedTracks", + cacheable, + params, + stream=stream, ): try: artist = _extract(track, "name", 1) @@ -2351,7 +2378,15 @@ class User(_Chartable): return Track(artist, title, self.network, self.name, info=info) - def get_recent_tracks(self, limit=10, cacheable=True, time_from=None, time_to=None, stream=True, now_playing=False): + def get_recent_tracks( + self, + limit=10, + cacheable=True, + time_from=None, + time_to=None, + stream=True, + now_playing=False, + ): """ Returns this user's played track as a sequence of PlayedTrack objects in reverse order of playtime, all the way back to the first track. @@ -2388,7 +2423,7 @@ class User(_Chartable): self.ws_prefix + ".getRecentTracks", cacheable, params, - stream=stream + stream=stream, ): if track_node.hasAttribute("nowplaying") and not now_playing: continue # to prevent the now playing track from sneaking in @@ -2537,7 +2572,9 @@ class User(_Chartable): return seq - def get_top_tracks(self, period=PERIOD_OVERALL, limit=None, cacheable=True, stream=True): + def get_top_tracks( + self, period=PERIOD_OVERALL, limit=None, cacheable=True, stream=True + ): """Returns the top tracks played by a user. * period: The period of time. Possible values: o PERIOD_OVERALL @@ -2553,7 +2590,9 @@ class User(_Chartable): if limit: params["limit"] = limit - return self._get_things("getTopTracks", "track", Track, params, cacheable, stream=stream) + return self._get_things( + "getTopTracks", "track", Track, params, cacheable, stream=stream + ) def get_track_scrobbles(self, artist, track, cacheable=False, stream=True): """ @@ -2566,7 +2605,12 @@ class User(_Chartable): def _get_track_scrobbles(): for track_node in _collect_nodes( - None, self, self.ws_prefix + ".getTrackScrobbles", cacheable, params, stream=stream + None, + self, + self.ws_prefix + ".getTrackScrobbles", + cacheable, + params, + stream=stream, ): yield self._extract_played_track(track_node) @@ -2669,7 +2713,9 @@ class AlbumSearch(_Search): """Search for an album by name.""" def __init__(self, album_name, network): - super().__init__(ws_prefix="album", search_terms={"album": album_name}, network=network) + super().__init__( + ws_prefix="album", search_terms={"album": album_name}, network=network + ) def get_next_page(self): """Returns the next page of results as a sequence of Album objects.""" @@ -2694,7 +2740,9 @@ class ArtistSearch(_Search): """Search for an artist by artist name.""" def __init__(self, artist_name, network): - super().__init__(ws_prefix="artist", search_terms={"artist": artist_name}, network=network) + super().__init__( + ws_prefix="artist", search_terms={"artist": artist_name}, network=network + ) def get_next_page(self): """Returns the next page of results as a sequence of Artist objects.""" @@ -2721,7 +2769,11 @@ class TrackSearch(_Search): """ def __init__(self, artist_name, track_title, network): - super().__init__(ws_prefix="track", search_terms={"track": track_title, "artist": artist_name}, network=network) + super().__init__( + ws_prefix="track", + search_terms={"track": track_title, "artist": artist_name}, + network=network, + ) def get_next_page(self): """Returns the next page of results as a sequence of Track objects.""" @@ -2811,7 +2863,9 @@ def _collect_nodes(limit, sender, method_name, cacheable, params=None, stream=Fa main = doc.documentElement.childNodes[0] if main.hasAttribute("totalPages") or main.hasAttribute("totalpages"): - total_pages = _number(main.getAttribute("totalPages") or main.getAttribute("totalpages")) + total_pages = _number( + main.getAttribute("totalPages") or main.getAttribute("totalpages") + ) else: raise Exception("No total pages attribute") diff --git a/tests/test_artist.py b/tests/test_artist.py index 8f9a97e..befa778 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -2,9 +2,9 @@ """ Integration (not unit) tests for pylast.py """ +import pylast import pytest -import pylast from .test_pylast import WRITE_TEST, TestPyLastWithLastFm diff --git a/tests/test_network.py b/tests/test_network.py index 7c2e68d..bdf9435 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -5,9 +5,9 @@ Integration (not unit) tests for pylast.py import re import time +import pylast import pytest -import pylast from .test_pylast import WRITE_TEST, TestPyLastWithLastFm diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 20d1604..b17fc0c 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -6,11 +6,10 @@ import os import sys import time +import pylast import pytest from flaky import flaky -import pylast - WRITE_TEST = sys.version_info[:2] == (3, 8) diff --git a/tests/test_track.py b/tests/test_track.py index 8ab6faa..fe8eb83 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -4,9 +4,9 @@ Integration (not unit) tests for pylast.py """ import time +import pylast import pytest -import pylast from .test_pylast import WRITE_TEST, TestPyLastWithLastFm diff --git a/tests/test_user.py b/tests/test_user.py index 5415cc8..ddf1509 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -4,13 +4,14 @@ Integration (not unit) tests for pylast.py """ import calendar import datetime as dt +import inspect import os import re import warnings +import pylast import pytest -import pylast from .test_pylast import TestPyLastWithLastFm @@ -361,7 +362,9 @@ class TestPyLastUser(TestPyLastWithLastFm): utc_end = calendar.timegm(end.utctimetuple()) # Act - tracks = lastfm_user.get_recent_tracks(time_from=utc_start, time_to=utc_end, stream=False) + tracks = lastfm_user.get_recent_tracks( + time_from=utc_start, time_to=utc_end, stream=False + ) # Assert assert len(tracks) == 1 @@ -387,6 +390,23 @@ class TestPyLastUser(TestPyLastWithLastFm): assert str(tracks[0].track.artist) == "Seun Kuti & Egypt 80" assert str(tracks[0].track.title) == "Struggles Sounds" + def test_get_recent_tracks_is_streamable(self): + # Arrange + lastfm_user = self.network.get_user("bbc6music") + start = dt.datetime(2020, 2, 15, 15, 00) + end = dt.datetime(2020, 2, 15, 15, 40) + + utc_start = calendar.timegm(start.utctimetuple()) + utc_end = calendar.timegm(end.utctimetuple()) + + # Act + tracks = lastfm_user.get_recent_tracks( + time_from=utc_start, time_to=utc_end, limit=None, stream=True + ) + + # Assert + assert inspect.isgenerator(tracks) + def test_get_playcount(self): # Arrange user = self.network.get_user("RJ") From a769b611f029ffb59c4b76e8bd31eaa5fbea384b Mon Sep 17 00:00:00 2001 From: kvanzuijlen Date: Tue, 14 Jul 2020 03:17:27 +0200 Subject: [PATCH 06/11] Added -e argument for editable installs and added quotes for some shells Co-authored-by: Hugo van Kemenade --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ec2e8f..0559b64 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE To run all unit and integration tests: ```sh -pip install .[tests] +pip install -e ".[tests]" pytest ``` From 136b7f1cef9296c985b522ff08c8a66264b3e3e7 Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen Date: Tue, 14 Jul 2020 03:42:44 +0200 Subject: [PATCH 07/11] Made stream=False the default instead of stream=True --- src/pylast/__init__.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 8b4cdd5..c4f3541 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1841,7 +1841,7 @@ class Artist(_Taggable): return artists - def get_top_albums(self, limit=None, cacheable=True, stream=True): + def get_top_albums(self, limit=None, cacheable=True, stream=False): """Returns a list of the top albums.""" params = self._get_params() if limit: @@ -1851,7 +1851,7 @@ class Artist(_Taggable): "getTopAlbums", "album", Album, params, cacheable, stream=stream ) - def get_top_tracks(self, limit=None, cacheable=True, stream=True): + def get_top_tracks(self, limit=None, cacheable=True, stream=False): """Returns a list of the most played Tracks by this artist.""" params = self._get_params() if limit: @@ -1927,7 +1927,7 @@ class Country(_BaseObject): return _extract_top_artists(doc, self) - def get_top_tracks(self, limit=None, cacheable=True, stream=True): + def get_top_tracks(self, limit=None, cacheable=True, stream=False): """Returns a sequence of the most played tracks""" params = self._get_params() if limit: @@ -1990,7 +1990,7 @@ class Library(_BaseObject): """Returns the user who owns this library.""" return self.user - def get_artists(self, limit=50, cacheable=True, stream=True): + def get_artists(self, limit=50, cacheable=True, stream=False): """ Returns a sequence of Album objects if limit==None it will return all (may take a while) @@ -2058,7 +2058,7 @@ class Tag(_Chartable): return _extract_top_albums(doc, self.network) - def get_top_tracks(self, limit=None, cacheable=True, stream=True): + def get_top_tracks(self, limit=None, cacheable=True, stream=False): """Returns a list of the most played Tracks for this tag.""" params = self._get_params() if limit: @@ -2273,7 +2273,7 @@ class User(_Chartable): return self.name - def get_artist_tracks(self, artist, cacheable=False, stream=True): + def get_artist_tracks(self, artist, cacheable=False, stream=False): """ Deprecated by Last.fm. Get a list of tracks by a given artist scrobbled by this user, @@ -2304,7 +2304,7 @@ class User(_Chartable): return _get_artist_tracks() if stream else list(_get_artist_tracks()) - def get_friends(self, limit=50, cacheable=False, stream=True): + def get_friends(self, limit=50, cacheable=False, stream=False): """Returns a list of the user's friends. """ def _get_friends(): @@ -2315,13 +2315,13 @@ class User(_Chartable): return _get_friends() if stream else list(_get_friends()) - def get_loved_tracks(self, limit=50, cacheable=True, stream=True): + def get_loved_tracks(self, limit=50, cacheable=True, stream=False): """ Returns this user's loved track as a sequence of LovedTrack objects in reverse order of their timestamp, all the way back to the first track. If limit==None, it will try to pull all the available data. - If stream=True, it will yield tracks as soon as a page has been retrieved. + If stream=False, it will yield tracks as soon as a page has been retrieved. This method uses caching. Enable caching only if you're pulling a large amount of data. @@ -2384,7 +2384,7 @@ class User(_Chartable): cacheable=True, time_from=None, time_to=None, - stream=True, + stream=False, now_playing=False, ): """ @@ -2573,7 +2573,7 @@ class User(_Chartable): return seq def get_top_tracks( - self, period=PERIOD_OVERALL, limit=None, cacheable=True, stream=True + self, period=PERIOD_OVERALL, limit=None, cacheable=True, stream=False ): """Returns the top tracks played by a user. * period: The period of time. Possible values: @@ -2594,7 +2594,7 @@ class User(_Chartable): "getTopTracks", "track", Track, params, cacheable, stream=stream ) - def get_track_scrobbles(self, artist, track, cacheable=False, stream=True): + def get_track_scrobbles(self, artist, track, cacheable=False, stream=False): """ Get a list of this user's scrobbles of this artist's track, including scrobble time. From 0999501600fd7c3fff68b07aa93d8d894f03ac9a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 29 Dec 2020 21:24:05 +0200 Subject: [PATCH 08/11] Fix comment --- src/pylast/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index c4f3541..fc71011 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -2321,7 +2321,7 @@ class User(_Chartable): reverse order of their timestamp, all the way back to the first track. If limit==None, it will try to pull all the available data. - If stream=False, it will yield tracks as soon as a page has been retrieved. + If stream=True, it will yield tracks as soon as a page has been retrieved. This method uses caching. Enable caching only if you're pulling a large amount of data. From 8be8c4efb6905ef97e18f2ecd75b95fd9f67e08c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 29 Dec 2020 21:32:29 +0200 Subject: [PATCH 09/11] No need to set param with default --- tests/test_artist.py | 6 +++--- tests/test_network.py | 4 ++-- tests/test_pylast.py | 4 ++-- tests/test_user.py | 22 ++++++++++------------ 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/test_artist.py b/tests/test_artist.py index befa778..e389010 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -78,7 +78,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_tracks(limit=2, stream=False) + things = artist.get_top_tracks(limit=2) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) @@ -101,7 +101,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_albums(limit=limit, stream=False) + things = artist.get_top_albums(limit=limit) # Assert assert len(things) == 1 @@ -113,7 +113,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): artist = self.network.get_top_artists(limit=1)[0].item # Act - things = artist.get_top_albums(limit=limit, stream=False) + things = artist.get_top_albums(limit=limit) # Assert assert len(things) == 50 diff --git a/tests/test_network.py b/tests/test_network.py index bdf9435..cebc7c6 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -153,7 +153,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): country = self.network.get_country("Croatia") # Act - things = country.get_top_tracks(limit=2, stream=False) + things = country.get_top_tracks(limit=2) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) @@ -171,7 +171,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): tag = self.network.get_tag("blues") # Act - things = tag.get_top_tracks(limit=2, stream=False) + things = tag.get_top_tracks(limit=2) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) diff --git a/tests/test_pylast.py b/tests/test_pylast.py index b17fc0c..da5d816 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -94,8 +94,8 @@ class TestPyLastWithLastFm(PyLastTestCase): func = getattr(thing, function_name, None) # Act - result1 = func(limit=1, cacheable=False, stream=False) - result2 = func(limit=1, cacheable=True, stream=False) + result1 = func(limit=1, cacheable=False) + result2 = func(limit=1, cacheable=True) result3 = list(func(limit=1)) # Assert diff --git a/tests/test_user.py b/tests/test_user.py index ddf1509..2b1d8fc 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -143,10 +143,10 @@ class TestPyLastUser(TestPyLastWithLastFm): user = self.network.get_user("test-user") # Act/Assert - assert len(user.get_loved_tracks(limit=20, stream=False)) == 20 - assert len(user.get_loved_tracks(limit=100, stream=False)) <= 100 - assert len(user.get_loved_tracks(limit=None, stream=False)) >= 23 - assert len(user.get_loved_tracks(limit=0, stream=False)) >= 23 + assert len(user.get_loved_tracks(limit=20)) == 20 + assert len(user.get_loved_tracks(limit=100)) <= 100 + assert len(user.get_loved_tracks(limit=None)) >= 23 + assert len(user.get_loved_tracks(limit=0)) >= 23 def test_user_is_hashable(self): # Arrange @@ -211,7 +211,7 @@ class TestPyLastUser(TestPyLastWithLastFm): lastfm_user = self.network.get_user("RJ") # Act - things = lastfm_user.get_top_tracks(limit=2, stream=False) + things = lastfm_user.get_top_tracks(limit=2) # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) @@ -362,9 +362,7 @@ class TestPyLastUser(TestPyLastWithLastFm): utc_end = calendar.timegm(end.utctimetuple()) # Act - tracks = lastfm_user.get_recent_tracks( - time_from=utc_start, time_to=utc_end, stream=False - ) + tracks = lastfm_user.get_recent_tracks(time_from=utc_start, time_to=utc_end) # Assert assert len(tracks) == 1 @@ -382,7 +380,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Act tracks = lastfm_user.get_recent_tracks( - time_from=utc_start, time_to=utc_end, limit=None, stream=False + time_from=utc_start, time_to=utc_end, limit=None ) # Assert @@ -469,7 +467,7 @@ class TestPyLastUser(TestPyLastWithLastFm): user = self.network.get_user("bbc6music") # Act - scrobbles = user.get_track_scrobbles(artist, title, stream=False) + scrobbles = user.get_track_scrobbles(artist, title) # Assert assert len(scrobbles) > 0 @@ -483,7 +481,7 @@ class TestPyLastUser(TestPyLastWithLastFm): user = self.network.get_user("bbc6music") # Act - result1 = user.get_track_scrobbles(artist, title, cacheable=False, stream=False) + result1 = user.get_track_scrobbles(artist, title, cacheable=False) result2 = list(user.get_track_scrobbles(artist, title, cacheable=True)) result3 = list(user.get_track_scrobbles(artist, title)) @@ -500,4 +498,4 @@ class TestPyLastUser(TestPyLastWithLastFm): match="Deprecated - This type of request is no longer supported", ): warnings.filterwarnings("ignore", category=DeprecationWarning) - lastfm_user.get_artist_tracks(artist="Test Artist", stream=False) + lastfm_user.get_artist_tracks(artist="Test Artist") From b992d2613826ef2d2830842751622c058e55d45f Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen Date: Tue, 29 Dec 2020 21:19:46 +0100 Subject: [PATCH 10/11] Bugfix for creation of temporary files --- src/pylast/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 1a24622..af426e2 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -421,9 +421,7 @@ class _Network: """ if not file_path: - file = tempfile.TemporaryFile(prefix="pylast_tmp_") - file.close() - file_path = file.name + file_path = tempfile.mkstemp(prefix="pylast_tmp_") self.cache_backend = _ShelfCacheBackend(file_path) From c851b82a1d37e9a3842781b5c3303971af412fe8 Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen Date: Tue, 29 Dec 2020 22:12:43 +0100 Subject: [PATCH 11/11] Reverted temporary files change --- src/pylast/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index af426e2..05c0361 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -421,7 +421,7 @@ class _Network: """ if not file_path: - file_path = tempfile.mkstemp(prefix="pylast_tmp_") + file_path = tempfile.mktemp(prefix="pylast_tmp_") self.cache_backend = _ShelfCacheBackend(file_path)