diff --git a/spotframework/engine/filter/popularity.py b/spotframework/engine/filter/popularity.py new file mode 100644 index 0000000..9746ff4 --- /dev/null +++ b/spotframework/engine/filter/popularity.py @@ -0,0 +1,38 @@ +from spotframework.engine.filter.abstract import AbstractProcessor +from typing import List +from spotframework.model.track import Track, SpotifyTrack + + +class SortPopularity(AbstractProcessor): + + def __init__(self, + names: List[str] = None, + keep_malformed_type: bool = True): + super().__init__(names) + self.keep_malformed_type = keep_malformed_type + + def sort(self, tracks: List[SpotifyTrack]): + tracks.sort(key=lambda x: x.popularity, reverse=True) + + def process(self, tracks: List[Track]): + return_tracks = [] + malformed_tracks = [] + + for track in tracks: + if isinstance(track, SpotifyTrack): + return_tracks.append(track) + else: + malformed_tracks.append(track) + + self.sort(return_tracks) + + if self.keep_malformed_type: + return_tracks += malformed_tracks + + return return_tracks + + +class SortReversePopularity(SortPopularity): + + def sort(self, tracks: List[SpotifyTrack]): + tracks.sort(key=lambda x: x.popularity, reverse=False) diff --git a/spotframework/engine/filter/sort.py b/spotframework/engine/filter/sort.py index cd19ebb..49ea780 100644 --- a/spotframework/engine/filter/sort.py +++ b/spotframework/engine/filter/sort.py @@ -1,31 +1,26 @@ +from abc import ABC from .abstract import AbstractProcessor from typing import List from spotframework.model.track import Track -class SortReverseReleaseDate(AbstractProcessor): +class BasicReversibleSort(AbstractProcessor, ABC): + def __init__(self, + names: List[str] = None, + reverse: bool = False): + super().__init__(names) + self.reverse = reverse + + +class SortReleaseDate(BasicReversibleSort): def process(self, tracks: List[Track]): - tracks.sort(key=lambda x: x.album.release_date, reverse=True) + tracks.sort(key=lambda x: x.album.release_date, reverse=self.reverse) return tracks -class SortReleaseDate(AbstractProcessor): +class SortArtistName(BasicReversibleSort): def process(self, tracks: List[Track]): - tracks.sort(key=lambda x: x.album.release_date, reverse=False) - return tracks - - -class SortArtistName(AbstractProcessor): - - def process(self, tracks: List[Track]): - tracks.sort(key=lambda x: x.artists[0].name, reverse=False) - return tracks - - -class SortReverseArtistName(AbstractProcessor): - - def process(self, tracks: List[Track]): - tracks.sort(key=lambda x: x.artists[0].name, reverse=True) + tracks.sort(key=lambda x: x.artists[0].name, reverse=self.reverse) return tracks diff --git a/spotframework/engine/playlistengine.py b/spotframework/engine/playlistengine.py index 8d6851f..3a12b5c 100644 --- a/spotframework/engine/playlistengine.py +++ b/spotframework/engine/playlistengine.py @@ -7,7 +7,7 @@ from spotframework.engine.filter.added import AddedSince from typing import List from spotframework.model.track import SpotifyTrack -from spotframework.model.playlist import Playlist +from spotframework.model.playlist import SpotifyPlaylist from spotframework.net.network import Network from spotframework.engine.filter.abstract import AbstractProcessor from datetime import datetime @@ -40,7 +40,7 @@ class PlaylistEngine: logger.error('error getting playlists') def get_playlist_tracks(self, - playlist: Playlist): + playlist: SpotifyPlaylist): logger.info(f"pulling tracks for {playlist.name}") tracks = self.net.get_playlist_tracks(playlist.playlist_id) diff --git a/spotframework/model/album.py b/spotframework/model/album.py index f22ac8a..898187b 100644 --- a/spotframework/model/album.py +++ b/spotframework/model/album.py @@ -10,6 +10,14 @@ class Album: self.name = name self.artists = artists + @property + def artists_names(self): + return self._join_strings([i.name for i in self.artists]) + + @staticmethod + def _join_strings(string_list: List[str]): + return ' , '.join(string_list) + def __str__(self): artists = ' , '.join([i.name for i in self.artists]) if self.artists else 'n/a' diff --git a/spotframework/model/playlist.py b/spotframework/model/playlist.py index 42bfaa9..26009bc 100644 --- a/spotframework/model/playlist.py +++ b/spotframework/model/playlist.py @@ -1,8 +1,75 @@ from spotframework.model.user import User +from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack +from tabulate import tabulate +from typing import List +import logging + +logger = logging.getLogger(__name__) class Playlist: + def __init__(self, + name: str = None, + description: str = None): + self._tracks = [] + self.name = name + self.description = description + + def has_tracks(self): + if len(self.tracks) > 0: + return True + else: + return False + + def __len__(self): + return len(self.tracks) + + @property + def tracks(self): + return self._tracks + + @tracks.setter + def tracks(self, value: List[Track]): + tracks = [] + not_tracks = [] + + for track in value: + if isinstance(track, Track): + tracks.append(track) + else: + not_tracks.append(track) + + if len(not_tracks) > 0: + logger.error('playlist tracks must be off type Track') + + self._tracks = tracks + + def __str__(self): + headers = ['name', 'album', 'artist', 'added at', 'popularity', 'uri'] + + rows = [] + for track in self.tracks: + track_row = [track.name, + track.album.name, + track.artists_names, + track.added_at if isinstance(track, PlaylistTrack) else '', + track.popularity if isinstance(track, SpotifyTrack) else '', + track.uri if isinstance(track, SpotifyTrack) else ''] + + rows.append(track_row) + + table = tabulate(rows, headers=headers, showindex='always', tablefmt="fancy_grid") + + prefix = f'\n==={self.name}===\n\n' if self.name is not None else '' + + table = prefix + table + '\n' + f'total: {len(self)}' + + return table + + +class SpotifyPlaylist(Playlist): + def __init__(self, playlistid: str, @@ -16,12 +83,11 @@ class Playlist: collaborative: bool = None, public: bool = None, ext_spotify: str = None): - self.tracks = [] - self.name = name + + super().__init__(name=name, description=description) self.playlist_id = playlistid self.owner = owner - self.description = description self.href = href self.uri = uri @@ -29,9 +95,3 @@ class Playlist: self.collaborative = collaborative self.public = public self.ext_spotify = ext_spotify - - def has_tracks(self): - if len(self.tracks) > 0: - return True - else: - return False diff --git a/spotframework/model/track.py b/spotframework/model/track.py index 1a77292..5f540d9 100644 --- a/spotframework/model/track.py +++ b/spotframework/model/track.py @@ -26,6 +26,18 @@ class Track: self.duration_ms = duration_ms self.explicit = excplicit + @property + def artists_names(self): + return self._join_strings([i.name for i in self.artists]) + + @property + def album_artists_names(self): + return self.album.artists_names + + @staticmethod + def _join_strings(string_list: List[str]): + return ' , '.join(string_list) + def __str__(self): album = self.album.name if self.album else 'n/a' artists = ' , '.join([i.name for i in self.artists]) if self.artists else 'n/a' diff --git a/spotframework/net/network.py b/spotframework/net/network.py index 43bd5cb..5ad8e09 100644 --- a/spotframework/net/network.py +++ b/spotframework/net/network.py @@ -5,7 +5,7 @@ import time from typing import List from . import const from spotframework.net.parse import parse -from spotframework.model.playlist import Playlist +from spotframework.model.playlist import SpotifyPlaylist limit = 50 @@ -33,7 +33,7 @@ class Network: if retry_after: logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds') - time.sleep(int(retry_after)) + time.sleep(int(retry_after) + 1) return self._make_get_request(method, url, params, headers) else: logger.error(f'{method} rate limit reached: cannot find Retry-After header') @@ -60,7 +60,7 @@ class Network: if retry_after: logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds') - time.sleep(int(retry_after)) + time.sleep(int(retry_after) + 1) return self._make_post_request(method, url, params, json, headers) else: logger.error(f'{method} rate limit reached: cannot find Retry-After header') @@ -87,7 +87,7 @@ class Network: if retry_after: logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds') - time.sleep(int(retry_after)) + time.sleep(int(retry_after) + 1) return self._make_put_request(method, url, params, json, headers) else: logger.error(f'{method} rate limit reached: cannot find Retry-After header') @@ -106,7 +106,7 @@ class Network: if tracks is not None: - playlist = Playlist(playlistid) + playlist = SpotifyPlaylist(playlistid) playlist.tracks += tracks return playlist @@ -415,7 +415,7 @@ class Network: return None def write_playlist_object(self, - playlist: Playlist, + playlist: SpotifyPlaylist, append_tracks: bool = False): if playlist.playlist_id: diff --git a/spotframework/net/parse/parse.py b/spotframework/net/parse/parse.py index 2cd4e5a..8766a13 100644 --- a/spotframework/net/parse/parse.py +++ b/spotframework/net/parse/parse.py @@ -1,7 +1,7 @@ from spotframework.model.artist import Artist, SpotifyArtist from spotframework.model.album import Album, SpotifyAlbum from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack -from spotframework.model.playlist import Playlist +from spotframework.model.playlist import SpotifyPlaylist from spotframework.model.user import User @@ -175,12 +175,12 @@ def parse_playlist(playlist_dict): public = playlist_dict.get('public', None) uri = playlist_dict.get('uri', None) - return Playlist(playlistid=playlist_id, - name=name, - owner=owner, - description=description, - href=href, - uri=uri, - collaborative=collaborative, - public=public, - ext_spotify=ext_spotify) + return SpotifyPlaylist(playlistid=playlist_id, + name=name, + owner=owner, + description=description, + href=href, + uri=uri, + collaborative=collaborative, + public=public, + ext_spotify=ext_spotify) diff --git a/spotframework/net/user.py b/spotframework/net/user.py index 27c638f..baaa8a9 100644 --- a/spotframework/net/user.py +++ b/spotframework/net/user.py @@ -2,6 +2,7 @@ import requests from spotframework.model.user import User from base64 import b64encode import logging +import time logger = logging.getLogger(__name__) @@ -21,6 +22,15 @@ class NetworkUser(User): self.refresh_info() def refresh_token(self): + + if self.refreshtoken is None: + raise NameError('no refresh token to query') + + if self.client_id is None: + raise NameError('no client id') + + if self.client_secret is None: + raise NameError('no client secret') idsecret = b64encode(bytes(self.client_id + ':' + self.client_secret, "utf-8")).decode("ascii") headers = {'Authorization': 'Basic %s' % idsecret} @@ -33,7 +43,20 @@ class NetworkUser(User): logger.debug('token refreshed') self.accesstoken = req.json()['access_token'] else: - logger.error(f'http error {req.status_code}') + + if req.status_code == 429: + retry_after = req.headers.get('Retry-After', None) + + if retry_after: + logger.warning(f'refresh_token rate limit reached: retrying in {retry_after} seconds') + time.sleep(int(retry_after) + 1) + return self.refresh_token() + else: + logger.error(f'refresh_token rate limit reached: cannot find Retry-After header') + + else: + error_text = req.json()['error']['message'] + logger.error(f'refresh_token get {req.status_code} {error_text}') def refresh_info(self): info = self.get_info() @@ -64,4 +87,17 @@ class NetworkUser(User): logger.debug(f'retrieved {req.status_code}') return req.json() else: - logger.error(f'http error {req.status_code}') + + if req.status_code == 429: + retry_after = req.headers.get('Retry-After', None) + + if retry_after: + logger.warning(f'get_info rate limit reached: retrying in {retry_after} seconds') + time.sleep(int(retry_after) + 1) + return self.get_info() + else: + logger.error(f'get_info rate limit reached: cannot find Retry-After header') + + else: + error_text = req.json()['error']['message'] + logger.error(f'get_info get {req.status_code} {error_text}')