added playlist printing, model props, added popularity sorting
This commit is contained in:
parent
0bd9fac8f4
commit
ad9c40ea27
38
spotframework/engine/filter/popularity.py
Normal file
38
spotframework/engine/filter/popularity.py
Normal file
@ -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)
|
@ -1,31 +1,26 @@
|
|||||||
|
from abc import ABC
|
||||||
from .abstract import AbstractProcessor
|
from .abstract import AbstractProcessor
|
||||||
from typing import List
|
from typing import List
|
||||||
from spotframework.model.track import Track
|
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]):
|
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
|
return tracks
|
||||||
|
|
||||||
|
|
||||||
class SortReleaseDate(AbstractProcessor):
|
class SortArtistName(BasicReversibleSort):
|
||||||
|
|
||||||
def process(self, tracks: List[Track]):
|
def process(self, tracks: List[Track]):
|
||||||
tracks.sort(key=lambda x: x.album.release_date, reverse=False)
|
tracks.sort(key=lambda x: x.artists[0].name, reverse=self.reverse)
|
||||||
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)
|
|
||||||
return tracks
|
return tracks
|
||||||
|
@ -7,7 +7,7 @@ from spotframework.engine.filter.added import AddedSince
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from spotframework.model.track import SpotifyTrack
|
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.net.network import Network
|
||||||
from spotframework.engine.filter.abstract import AbstractProcessor
|
from spotframework.engine.filter.abstract import AbstractProcessor
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -40,7 +40,7 @@ class PlaylistEngine:
|
|||||||
logger.error('error getting playlists')
|
logger.error('error getting playlists')
|
||||||
|
|
||||||
def get_playlist_tracks(self,
|
def get_playlist_tracks(self,
|
||||||
playlist: Playlist):
|
playlist: SpotifyPlaylist):
|
||||||
logger.info(f"pulling tracks for {playlist.name}")
|
logger.info(f"pulling tracks for {playlist.name}")
|
||||||
|
|
||||||
tracks = self.net.get_playlist_tracks(playlist.playlist_id)
|
tracks = self.net.get_playlist_tracks(playlist.playlist_id)
|
||||||
|
@ -10,6 +10,14 @@ class Album:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.artists = artists
|
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):
|
def __str__(self):
|
||||||
artists = ' , '.join([i.name for i in self.artists]) if self.artists else 'n/a'
|
artists = ' , '.join([i.name for i in self.artists]) if self.artists else 'n/a'
|
||||||
|
|
||||||
|
@ -1,8 +1,75 @@
|
|||||||
from spotframework.model.user import User
|
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:
|
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,
|
def __init__(self,
|
||||||
playlistid: str,
|
playlistid: str,
|
||||||
|
|
||||||
@ -16,12 +83,11 @@ class Playlist:
|
|||||||
collaborative: bool = None,
|
collaborative: bool = None,
|
||||||
public: bool = None,
|
public: bool = None,
|
||||||
ext_spotify: str = None):
|
ext_spotify: str = None):
|
||||||
self.tracks = []
|
|
||||||
self.name = name
|
super().__init__(name=name, description=description)
|
||||||
|
|
||||||
self.playlist_id = playlistid
|
self.playlist_id = playlistid
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
self.description = description
|
|
||||||
|
|
||||||
self.href = href
|
self.href = href
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
@ -29,9 +95,3 @@ class Playlist:
|
|||||||
self.collaborative = collaborative
|
self.collaborative = collaborative
|
||||||
self.public = public
|
self.public = public
|
||||||
self.ext_spotify = ext_spotify
|
self.ext_spotify = ext_spotify
|
||||||
|
|
||||||
def has_tracks(self):
|
|
||||||
if len(self.tracks) > 0:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
@ -26,6 +26,18 @@ class Track:
|
|||||||
self.duration_ms = duration_ms
|
self.duration_ms = duration_ms
|
||||||
self.explicit = excplicit
|
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):
|
def __str__(self):
|
||||||
album = self.album.name if self.album else 'n/a'
|
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'
|
artists = ' , '.join([i.name for i in self.artists]) if self.artists else 'n/a'
|
||||||
|
@ -5,7 +5,7 @@ import time
|
|||||||
from typing import List
|
from typing import List
|
||||||
from . import const
|
from . import const
|
||||||
from spotframework.net.parse import parse
|
from spotframework.net.parse import parse
|
||||||
from spotframework.model.playlist import Playlist
|
from spotframework.model.playlist import SpotifyPlaylist
|
||||||
|
|
||||||
limit = 50
|
limit = 50
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ class Network:
|
|||||||
|
|
||||||
if retry_after:
|
if retry_after:
|
||||||
logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds')
|
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)
|
return self._make_get_request(method, url, params, headers)
|
||||||
else:
|
else:
|
||||||
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
|
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
|
||||||
@ -60,7 +60,7 @@ class Network:
|
|||||||
|
|
||||||
if retry_after:
|
if retry_after:
|
||||||
logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds')
|
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)
|
return self._make_post_request(method, url, params, json, headers)
|
||||||
else:
|
else:
|
||||||
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
|
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
|
||||||
@ -87,7 +87,7 @@ class Network:
|
|||||||
|
|
||||||
if retry_after:
|
if retry_after:
|
||||||
logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds')
|
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)
|
return self._make_put_request(method, url, params, json, headers)
|
||||||
else:
|
else:
|
||||||
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
|
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
|
||||||
@ -106,7 +106,7 @@ class Network:
|
|||||||
|
|
||||||
if tracks is not None:
|
if tracks is not None:
|
||||||
|
|
||||||
playlist = Playlist(playlistid)
|
playlist = SpotifyPlaylist(playlistid)
|
||||||
playlist.tracks += tracks
|
playlist.tracks += tracks
|
||||||
|
|
||||||
return playlist
|
return playlist
|
||||||
@ -415,7 +415,7 @@ class Network:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def write_playlist_object(self,
|
def write_playlist_object(self,
|
||||||
playlist: Playlist,
|
playlist: SpotifyPlaylist,
|
||||||
append_tracks: bool = False):
|
append_tracks: bool = False):
|
||||||
|
|
||||||
if playlist.playlist_id:
|
if playlist.playlist_id:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from spotframework.model.artist import Artist, SpotifyArtist
|
from spotframework.model.artist import Artist, SpotifyArtist
|
||||||
from spotframework.model.album import Album, SpotifyAlbum
|
from spotframework.model.album import Album, SpotifyAlbum
|
||||||
from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack
|
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
|
from spotframework.model.user import User
|
||||||
|
|
||||||
|
|
||||||
@ -175,12 +175,12 @@ def parse_playlist(playlist_dict):
|
|||||||
public = playlist_dict.get('public', None)
|
public = playlist_dict.get('public', None)
|
||||||
uri = playlist_dict.get('uri', None)
|
uri = playlist_dict.get('uri', None)
|
||||||
|
|
||||||
return Playlist(playlistid=playlist_id,
|
return SpotifyPlaylist(playlistid=playlist_id,
|
||||||
name=name,
|
name=name,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
description=description,
|
description=description,
|
||||||
href=href,
|
href=href,
|
||||||
uri=uri,
|
uri=uri,
|
||||||
collaborative=collaborative,
|
collaborative=collaborative,
|
||||||
public=public,
|
public=public,
|
||||||
ext_spotify=ext_spotify)
|
ext_spotify=ext_spotify)
|
||||||
|
@ -2,6 +2,7 @@ import requests
|
|||||||
from spotframework.model.user import User
|
from spotframework.model.user import User
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -21,6 +22,15 @@ class NetworkUser(User):
|
|||||||
self.refresh_info()
|
self.refresh_info()
|
||||||
|
|
||||||
def refresh_token(self):
|
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")
|
idsecret = b64encode(bytes(self.client_id + ':' + self.client_secret, "utf-8")).decode("ascii")
|
||||||
headers = {'Authorization': 'Basic %s' % idsecret}
|
headers = {'Authorization': 'Basic %s' % idsecret}
|
||||||
@ -33,7 +43,20 @@ class NetworkUser(User):
|
|||||||
logger.debug('token refreshed')
|
logger.debug('token refreshed')
|
||||||
self.accesstoken = req.json()['access_token']
|
self.accesstoken = req.json()['access_token']
|
||||||
else:
|
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):
|
def refresh_info(self):
|
||||||
info = self.get_info()
|
info = self.get_info()
|
||||||
@ -64,4 +87,17 @@ class NetworkUser(User):
|
|||||||
logger.debug(f'retrieved {req.status_code}')
|
logger.debug(f'retrieved {req.status_code}')
|
||||||
return req.json()
|
return req.json()
|
||||||
else:
|
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}')
|
||||||
|
Loading…
Reference in New Issue
Block a user