From 0bd9fac8f4b241d50b0f60f046a7ecad2a15ebe7 Mon Sep 17 00:00:00 2001 From: aj Date: Wed, 4 Sep 2019 17:45:26 +0100 Subject: [PATCH] fully objectified model --- .gcloudignore | 21 -- .gitignore | 2 + alarm.py | 10 +- backup.py | 12 +- generateplaylists.py | 15 +- getaccesstoken.py | 10 +- spotframework/engine/filter/abstract.py | 47 +++++ .../engine/filter/abstractprocessor.py | 11 -- spotframework/engine/filter/added.py | 47 +++++ spotframework/engine/filter/addedbefore.py | 17 -- spotframework/engine/filter/addedsince.py | 17 -- spotframework/engine/filter/deduplicate.py | 45 +++++ .../engine/filter/deduplicatebyid.py | 13 -- .../engine/filter/deduplicatebyname.py | 19 -- spotframework/engine/filter/randomsample.py | 17 -- spotframework/engine/filter/shuffle.py | 18 +- spotframework/engine/filter/sort.py | 31 +++ .../engine/filter/sortreversereleasedate.py | 8 - spotframework/engine/playlistengine.py | 69 ++++--- spotframework/io/csv.py | 16 +- spotframework/model/album.py | 50 +++++ spotframework/model/artist.py | 32 +++ spotframework/model/playlist.py | 27 ++- spotframework/model/track.py | 100 ++++++++++ spotframework/model/user.py | 21 ++ spotframework/net/network.py | 48 +++-- spotframework/net/parse/__init__.py | 0 spotframework/net/parse/parse.py | 186 ++++++++++++++++++ spotframework/net/user.py | 27 ++- 29 files changed, 737 insertions(+), 199 deletions(-) delete mode 100644 .gcloudignore create mode 100644 spotframework/engine/filter/abstract.py delete mode 100644 spotframework/engine/filter/abstractprocessor.py create mode 100644 spotframework/engine/filter/added.py delete mode 100644 spotframework/engine/filter/addedbefore.py delete mode 100644 spotframework/engine/filter/addedsince.py create mode 100644 spotframework/engine/filter/deduplicate.py delete mode 100644 spotframework/engine/filter/deduplicatebyid.py delete mode 100644 spotframework/engine/filter/deduplicatebyname.py delete mode 100644 spotframework/engine/filter/randomsample.py create mode 100644 spotframework/engine/filter/sort.py delete mode 100644 spotframework/engine/filter/sortreversereleasedate.py create mode 100644 spotframework/model/album.py create mode 100644 spotframework/model/artist.py create mode 100644 spotframework/model/track.py create mode 100644 spotframework/model/user.py create mode 100644 spotframework/net/parse/__init__.py create mode 100644 spotframework/net/parse/parse.py diff --git a/.gcloudignore b/.gcloudignore deleted file mode 100644 index 15d4407..0000000 --- a/.gcloudignore +++ /dev/null @@ -1,21 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud Platform -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -env -.idea -.spot - -node_modules -#!include:.gitignore diff --git a/.gitignore b/.gitignore index 4b05691..29a072c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__ *.csv .idea .spot + +scratch.py \ No newline at end of file diff --git a/alarm.py b/alarm.py index 6425b6e..a836921 100644 --- a/alarm.py +++ b/alarm.py @@ -1,4 +1,4 @@ -from spotframework.net.user import User +from spotframework.net.user import NetworkUser from spotframework.net.network import Network import spotframework.net.const as const import spotframework.io.json as json @@ -33,10 +33,10 @@ if __name__ == '__main__': try: - network = Network(User(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], - os.environ['SPOTREFRESH'])) + network = Network(NetworkUser(os.environ['SPOTCLIENT'], + os.environ['SPOTSECRET'], + os.environ['SPOTACCESS'], + os.environ['SPOTREFRESH'])) found = False diff --git a/backup.py b/backup.py index 6376126..0fc4cfc 100644 --- a/backup.py +++ b/backup.py @@ -1,4 +1,4 @@ -from spotframework.net.user import User +from spotframework.net.user import NetworkUser from spotframework.net.network import Network import spotframework.io.csv as csvwrite @@ -21,14 +21,14 @@ if __name__ == '__main__': try: - network = Network(User(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], - os.environ['SPOTREFRESH'])) + network = Network(NetworkUser(os.environ['SPOTCLIENT'], + os.environ['SPOTSECRET'], + os.environ['SPOTACCESS'], + os.environ['SPOTREFRESH'])) playlists = network.get_user_playlists() for playlist in playlists: - playlist.tracks = network.get_playlist_tracks(playlist.playlistid) + playlist.tracks = network.get_playlist_tracks(playlist.playlist_id) path = sys.argv[1] diff --git a/generateplaylists.py b/generateplaylists.py index f705092..c7bc7ef 100644 --- a/generateplaylists.py +++ b/generateplaylists.py @@ -1,13 +1,12 @@ import spotframework.net.const as const from spotframework.net.network import Network -from spotframework.net.user import User +from spotframework.net.user import NetworkUser import spotframework.io.json as json import spotframework.util.monthstrings as monthstrings from spotframework.engine.playlistengine import PlaylistEngine from spotframework.engine.filter.shuffle import Shuffle -from spotframework.engine.filter.sortreversereleasedate import SortReverseReleaseDate -from spotframework.engine.filter.deduplicatebyid import DeduplicateByID -from spotframework.engine.filter.deduplicatebyname import DeduplicateByName +from spotframework.engine.filter.sort import SortReverseReleaseDate +from spotframework.engine.filter.deduplicate import DeduplicateByID, DeduplicateByName import os import datetime @@ -115,10 +114,10 @@ def go(): logger.critical('none to execute, terminating') return - net = Network(User(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], - os.environ['SPOTREFRESH'])) + net = Network(NetworkUser(os.environ['SPOTCLIENT'], + os.environ['SPOTSECRET'], + os.environ['SPOTACCESS'], + os.environ['SPOTREFRESH'])) engine = PlaylistEngine(net) engine.load_user_playlists() diff --git a/getaccesstoken.py b/getaccesstoken.py index a509eaa..7ab8f94 100644 --- a/getaccesstoken.py +++ b/getaccesstoken.py @@ -1,13 +1,13 @@ -from spotframework.net.user import User +from spotframework.net.user import NetworkUser from spotframework.net.network import Network import os if __name__ == '__main__': - network = Network(User(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], - os.environ['SPOTREFRESH'])) + network = Network(NetworkUser(os.environ['SPOTCLIENT'], + os.environ['SPOTSECRET'], + os.environ['SPOTACCESS'], + os.environ['SPOTREFRESH'])) print(network.user.access_token) diff --git a/spotframework/engine/filter/abstract.py b/spotframework/engine/filter/abstract.py new file mode 100644 index 0000000..b14c752 --- /dev/null +++ b/spotframework/engine/filter/abstract.py @@ -0,0 +1,47 @@ +from abc import ABC, abstractmethod +from typing import List +from spotframework.model.track import Track + + +class AbstractProcessor(ABC): + + def __init__(self, names: List[str] = None): + self.playlist_names = names + + def has_targets(self): + if self.playlist_names: + return True + else: + return False + + @abstractmethod + def process(self, tracks: List[Track]): + pass + + +class AbstractTestFilter(AbstractProcessor, ABC): + + def __init__(self, + names: List[str] = None, + keep_failed: bool = True): + super().__init__(names) + self.keep_failed = keep_failed + + @abstractmethod + def logic_test(self, track: Track): + pass + + def process(self, tracks: List[Track]): + + return_tracks = [] + malformed_tracks = [] + + for track in tracks: + if self.logic_test(track): + return_tracks.append(track) + else: + malformed_tracks.append(track) + if self.keep_failed: + return_tracks += malformed_tracks + + return return_tracks diff --git a/spotframework/engine/filter/abstractprocessor.py b/spotframework/engine/filter/abstractprocessor.py deleted file mode 100644 index be35f61..0000000 --- a/spotframework/engine/filter/abstractprocessor.py +++ /dev/null @@ -1,11 +0,0 @@ -from abc import ABC, abstractmethod - - -class AbstractProcessor(ABC): - - def __init__(self, names=[]): - self.playlist_names = names - - @abstractmethod - def process(self, tracks): - pass diff --git a/spotframework/engine/filter/added.py b/spotframework/engine/filter/added.py new file mode 100644 index 0000000..582103f --- /dev/null +++ b/spotframework/engine/filter/added.py @@ -0,0 +1,47 @@ +from abc import ABC, abstractmethod +from .abstract import AbstractProcessor +import datetime +from typing import List +from spotframework.model.track import Track, PlaylistTrack + + +class Added(AbstractProcessor, ABC): + + def __init__(self, + boundary: datetime.datetime, + names: List[str] = None, + keep_malformed_type: bool = True): + super().__init__(names) + self.boundary = boundary + self.keep_malformed_type = keep_malformed_type + + @abstractmethod + def check_date(self, track: PlaylistTrack): + pass + + def process(self, tracks: List[Track]): + + return_tracks = [] + malformed_tracks = [] + + for track in tracks: + if isinstance(track, PlaylistTrack): + if self.check_date(track): + return_tracks.append(track) + else: + malformed_tracks.append(track) + + if self.keep_malformed_type: + return_tracks += malformed_tracks + + return return_tracks + + +class AddedBefore(Added): + def check_date(self, track: PlaylistTrack): + return track.added_at < self.boundary + + +class AddedSince(Added): + def check_date(self, track: PlaylistTrack): + return track.added_at > self.boundary diff --git a/spotframework/engine/filter/addedbefore.py b/spotframework/engine/filter/addedbefore.py deleted file mode 100644 index 2898a55..0000000 --- a/spotframework/engine/filter/addedbefore.py +++ /dev/null @@ -1,17 +0,0 @@ -from .abstractprocessor import AbstractProcessor -import datetime - - -class AddedBefore(AbstractProcessor): - - def __init__(self, boundary, names=[]): - super().__init__(names) - self.boundary = boundary - - def check_date(self, track): - added_at = datetime.datetime.fromisoformat(track['added_at'].replace('T', ' ').replace('Z', '')) - - return added_at < self.boundary - - def process(self, tracks): - return [i for i in tracks if self.check_date(i)] diff --git a/spotframework/engine/filter/addedsince.py b/spotframework/engine/filter/addedsince.py deleted file mode 100644 index 3eec831..0000000 --- a/spotframework/engine/filter/addedsince.py +++ /dev/null @@ -1,17 +0,0 @@ -from .abstractprocessor import AbstractProcessor -import datetime - - -class AddedSince(AbstractProcessor): - - def __init__(self, boundary, names=[]): - super().__init__(names) - self.boundary = boundary - - def check_date(self, track): - added_at = datetime.datetime.fromisoformat(track['added_at'].replace('T', ' ').replace('Z', '')) - - return added_at > self.boundary - - def process(self, tracks): - return [i for i in tracks if self.check_date(i)] diff --git a/spotframework/engine/filter/deduplicate.py b/spotframework/engine/filter/deduplicate.py new file mode 100644 index 0000000..c98c5e1 --- /dev/null +++ b/spotframework/engine/filter/deduplicate.py @@ -0,0 +1,45 @@ +from spotframework.engine.filter.abstract import AbstractProcessor +from typing import List +from spotframework.model.track import Track, SpotifyTrack + + +class DeduplicateByID(AbstractProcessor): + + def __init__(self, + names: List[str] = None, + keep_malformed_type: bool = True): + super().__init__(names) + self.keep_malformed_type = keep_malformed_type + + def process(self, tracks: List[Track]): + return_tracks = [] + malformed_tracks = [] + + for track in tracks: + if isinstance(track, SpotifyTrack): + if track.uri not in [i.uri for i in return_tracks]: + return_tracks.append(track) + else: + malformed_tracks.append(track) + + if self.keep_malformed_type: + return_tracks += malformed_tracks + + return return_tracks + + +class DeduplicateByName(AbstractProcessor): + + def process(self, tracks: List[Track]): + return_tracks = [] + + for to_check in tracks: + + for cache_track in return_tracks: + if to_check.name.lower() == cache_track.name.lower(): + if to_check.artists[0].name.lower() == cache_track.artists[0].name.lower(): + break + else: + return_tracks.append(to_check) + + return return_tracks diff --git a/spotframework/engine/filter/deduplicatebyid.py b/spotframework/engine/filter/deduplicatebyid.py deleted file mode 100644 index d7deb7c..0000000 --- a/spotframework/engine/filter/deduplicatebyid.py +++ /dev/null @@ -1,13 +0,0 @@ -from .abstractprocessor import AbstractProcessor - - -class DeduplicateByID(AbstractProcessor): - - def process(self, tracks): - return_tracks = [] - - for track in tracks: - if track['track']['uri'] not in [i['track']['uri'] for i in return_tracks]: - return_tracks.append(track) - - return return_tracks diff --git a/spotframework/engine/filter/deduplicatebyname.py b/spotframework/engine/filter/deduplicatebyname.py deleted file mode 100644 index 0a1332b..0000000 --- a/spotframework/engine/filter/deduplicatebyname.py +++ /dev/null @@ -1,19 +0,0 @@ -from .abstractprocessor import AbstractProcessor - - -class DeduplicateByName(AbstractProcessor): - - def process(self, tracks): - return_tracks = [] - - for to_check in tracks: - - for cache_track in return_tracks: - if to_check['track']['name'].lower() == cache_track['track']['name'].lower(): - if to_check['track']['artists'][0]['name'].lower() \ - == cache_track['track']['artists'][0]['name'].lower(): - break - else: - return_tracks.append(to_check) - - return return_tracks diff --git a/spotframework/engine/filter/randomsample.py b/spotframework/engine/filter/randomsample.py deleted file mode 100644 index c604e6c..0000000 --- a/spotframework/engine/filter/randomsample.py +++ /dev/null @@ -1,17 +0,0 @@ -from .abstractprocessor import AbstractProcessor - -import random - - -class RandomSample(AbstractProcessor): - - def __init__(self, sample_size, names=[]): - super().__init__(names) - self.sample_size = sample_size - - def process(self, tracks): - - return_tracks = list(tracks) - random.shuffle(return_tracks) - - return return_tracks[:self.sample_size] diff --git a/spotframework/engine/filter/shuffle.py b/spotframework/engine/filter/shuffle.py index 8b267e5..11f0490 100644 --- a/spotframework/engine/filter/shuffle.py +++ b/spotframework/engine/filter/shuffle.py @@ -1,9 +1,23 @@ -from .abstractprocessor import AbstractProcessor +from .abstract import AbstractProcessor import random +from typing import List +from spotframework.model.track import Track class Shuffle(AbstractProcessor): - def process(self, tracks): + def process(self, tracks: List[Track]): random.shuffle(tracks) return tracks + + +class RandomSample(Shuffle): + + def __init__(self, + sample_size: int, + names: List[str] = None): + super().__init__(names) + self.sample_size = sample_size + + def process(self, tracks: List[Track]): + return super().process(tracks)[:self.sample_size] diff --git a/spotframework/engine/filter/sort.py b/spotframework/engine/filter/sort.py new file mode 100644 index 0000000..cd19ebb --- /dev/null +++ b/spotframework/engine/filter/sort.py @@ -0,0 +1,31 @@ +from .abstract import AbstractProcessor +from typing import List +from spotframework.model.track import Track + + +class SortReverseReleaseDate(AbstractProcessor): + + def process(self, tracks: List[Track]): + tracks.sort(key=lambda x: x.album.release_date, reverse=True) + return tracks + + +class SortReleaseDate(AbstractProcessor): + + 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) + return tracks diff --git a/spotframework/engine/filter/sortreversereleasedate.py b/spotframework/engine/filter/sortreversereleasedate.py deleted file mode 100644 index a6f0da7..0000000 --- a/spotframework/engine/filter/sortreversereleasedate.py +++ /dev/null @@ -1,8 +0,0 @@ -from .abstractprocessor import AbstractProcessor - - -class SortReverseReleaseDate(AbstractProcessor): - - def process(self, tracks): - tracks.sort(key=lambda x: x['track']['album']['release_date'], reverse=True) - return tracks diff --git a/spotframework/engine/playlistengine.py b/spotframework/engine/playlistengine.py index 177e148..8d6851f 100644 --- a/spotframework/engine/playlistengine.py +++ b/spotframework/engine/playlistengine.py @@ -3,14 +3,21 @@ import os import logging import spotframework.util.monthstrings as monthstrings -from spotframework.engine.filter.addedsince import AddedSince +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.net.network import Network +from spotframework.engine.filter.abstract import AbstractProcessor +from datetime import datetime logger = logging.getLogger(__name__) class PlaylistEngine: - def __init__(self, net): + def __init__(self, net: Network): self.playlists = [] self.net = net @@ -32,16 +39,24 @@ class PlaylistEngine: else: logger.error('error getting playlists') - def get_playlist_tracks(self, playlist): + def get_playlist_tracks(self, + playlist: Playlist): logger.info(f"pulling tracks for {playlist.name}") - tracks = self.net.get_playlist_tracks(playlist.playlistid) + tracks = self.net.get_playlist_tracks(playlist.playlist_id) if tracks and len(tracks) > 0: playlist.tracks = tracks else: logger.error('error getting tracks') - def make_playlist(self, playlist_parts, processors=[], include_recommendations=False, recommendation_limit=10): + def make_playlist(self, + playlist_parts: List[str], + processors: List[AbstractProcessor] = None, + include_recommendations: bool = False, + recommendation_limit: int = 10): + + if processors is None: + processors = [] tracks = [] @@ -56,39 +71,41 @@ class PlaylistEngine: playlist_tracks = list(play.tracks) - for processor in [i for i in processors if play.name in [j for j in i.playlist_names]]: - playlist_tracks = processor.process(playlist_tracks) + for processor in [i for i in processors if i.has_targets()]: + if play.name in [i for i in processor.playlist_names]: + playlist_tracks = processor.process(playlist_tracks) - tracks += [i for i in playlist_tracks if i['is_local'] is False] + tracks += [i for i in playlist_tracks if i.is_local is False] else: logger.warning(f"requested playlist {part} not found") if 'SLACKHOOK' in os.environ: requests.post(os.environ['SLACKHOOK'], json={"text": f"spot playlists: {part} not found"}) - for processor in [i for i in processors if len(i.playlist_names) <= 0]: + for processor in [i for i in processors if i.has_targets() is False]: tracks = processor.process(tracks) - tracks = [i['track'] for i in tracks] - if include_recommendations: - recommendations = self.net.get_recommendations(tracks=[i['id'] for i in tracks], + recommendations = self.net.get_recommendations(tracks=[i.spotify_id for i in tracks], response_limit=recommendation_limit) if recommendations and len(recommendations) > 0: - tracks += recommendations['tracks'] + tracks += recommendations else: logger.error('error getting recommendations') return tracks def get_recent_playlist(self, - boundary_date, - recent_playlist_parts, - processors=[], - include_recommendations=False, - recommendation_limit=10, - add_this_month=False, - add_last_month=False): + boundary_date: datetime, + recent_playlist_parts: List[str], + processors: List[AbstractProcessor] = None, + include_recommendations: bool = False, + recommendation_limit: int = 10, + add_this_month: bool = False, + add_last_month: bool = False): + + if processors is None: + processors = [] this_month = monthstrings.get_this_month() last_month = monthstrings.get_last_month() @@ -110,16 +127,22 @@ class PlaylistEngine: include_recommendations=include_recommendations, recommendation_limit=recommendation_limit) - def execute_playlist(self, tracks, playlist_id): + def execute_playlist(self, + tracks: List[SpotifyTrack], + playlist_id: str): - resp = self.net.replace_playlist_tracks(playlist_id, [i['uri'] for i in tracks]) + resp = self.net.replace_playlist_tracks(playlist_id, [i.uri for i in tracks]) if resp: return resp else: logger.error('error executing') return None - def change_description(self, playlistparts, playlist_id, overwrite=None, suffix=None): + def change_description(self, + playlistparts: List[str], + playlist_id: str, + overwrite: bool = None, + suffix: str = None): if overwrite: string = overwrite diff --git a/spotframework/io/csv.py b/spotframework/io/csv.py index ea2476c..a1f0fda 100644 --- a/spotframework/io/csv.py +++ b/spotframework/io/csv.py @@ -24,15 +24,15 @@ def export_playlist(playlist, path, name=None): for track in playlist.tracks: trackdict = { - 'name':track['track']['name'], - 'album':track['track']['album']['name'], - 'added':track['added_at'], - 'track id':track['track']['id'], - 'album id':track['track']['album']['id'], - 'added by':track['added_by']['id']} + 'name':track.name, + 'album':track.album.name, + 'added':track.added_at, + 'track id':track.spotify_id, + 'album id':track.album.spotify_id, + 'added by':track.added_by.username} - trackdict['album artist'] = ', '.join(x['name'] for x in track['track']['album']['artists']) + trackdict['album artist'] = ', '.join(x.name for x in track.album.artists) - trackdict['artist'] = ', '.join(x['name'] for x in track['track']['artists']) + trackdict['artist'] = ', '.join(x.name for x in track.artists) writer.writerow(trackdict) diff --git a/spotframework/model/album.py b/spotframework/model/album.py new file mode 100644 index 0000000..f22ac8a --- /dev/null +++ b/spotframework/model/album.py @@ -0,0 +1,50 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +from typing import List +if TYPE_CHECKING: + from spotframework.model.artist import Artist + + +class Album: + def __init__(self, name: str, artists: List[Artist]): + self.name = name + self.artists = artists + + def __str__(self): + artists = ' , '.join([i.name for i in self.artists]) if self.artists else 'n/a' + + return f'{self.name} / {artists}' + + +class SpotifyAlbum(Album): + def __init__(self, + name: str, + artists: List[Artist], + + href: str = None, + spotify_id: str = None, + uri: str = None, + + genres: List[str] = None, + tracks: List = None, + + release_date: str = None, + release_date_precision: str = None, + + label: str = None, + popularity: int = None + ): + super().__init__(name, artists) + + self.href = href + self.spotify_id = spotify_id + self.uri = uri + + self.genres = genres + self.tracks = tracks + + self.release_date = release_date + self.release_date_precision = release_date_precision + + self.label = label + self.popularity = popularity diff --git a/spotframework/model/artist.py b/spotframework/model/artist.py new file mode 100644 index 0000000..1fa89d6 --- /dev/null +++ b/spotframework/model/artist.py @@ -0,0 +1,32 @@ +from typing import List + + +class Artist: + def __init__(self, name: str): + self.name = name + + def __str__(self): + return f'{self.name}' + + +class SpotifyArtist(Artist): + def __init__(self, + name: str, + + href: str = None, + spotify_id: str = None, + uri: str = None, + + genres: List[str] = None, + + popularity: int = None + ): + super().__init__(name) + + self.href = href + self.spotify_id = spotify_id + self.uri = uri + + self.genres = genres + + self.popularity = popularity diff --git a/spotframework/model/playlist.py b/spotframework/model/playlist.py index e01d0b8..42bfaa9 100644 --- a/spotframework/model/playlist.py +++ b/spotframework/model/playlist.py @@ -1,14 +1,35 @@ +from spotframework.model.user import User class Playlist: - def __init__(self, playlistid, uri=None, name=None, userid=None): + def __init__(self, + playlistid: str, + + name: str = None, + owner: User = None, + description: str = None, + + href: str = None, + uri: str = None, + + collaborative: bool = None, + public: bool = None, + ext_spotify: str = None): self.tracks = [] self.name = name - self.playlistid = playlistid - self.userid = userid + + self.playlist_id = playlistid + self.owner = owner + self.description = description + + self.href = href self.uri = uri + self.collaborative = collaborative + self.public = public + self.ext_spotify = ext_spotify + def has_tracks(self): if len(self.tracks) > 0: return True diff --git a/spotframework/model/track.py b/spotframework/model/track.py new file mode 100644 index 0000000..1a77292 --- /dev/null +++ b/spotframework/model/track.py @@ -0,0 +1,100 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +from typing import List +from datetime import datetime +if TYPE_CHECKING: + from spotframework.model.album import Album + from spotframework.model.artist import Artist + from spotframework.model.user import User + + +class Track: + def __init__(self, + name: str, + album: Album, + artists: List[Artist], + + disc_number: int = None, + duration_ms: int = None, + excplicit: bool = None + ): + self.name = name + self.album = album + self.artists = artists + + self.disc_number = disc_number + self.duration_ms = duration_ms + self.explicit = excplicit + + 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' + + return f'{self.name} / {album} / {artists}' + + +class SpotifyTrack(Track): + def __init__(self, + name: str, + album: Album, + artists: List[Artist], + + href: str = None, + spotify_id: str = None, + uri: str = None, + + disc_number: int = None, + duration_ms: int = None, + explicit: bool = None, + is_playable: bool = None, + + popularity: int = None + ): + super().__init__(name=name, album=album, artists=artists, + disc_number=disc_number, + duration_ms=duration_ms, + excplicit=explicit) + + self.href = href + self.spotify_id = spotify_id + self.uri = uri + self.is_playable = is_playable + + self.popularity = popularity + + +class PlaylistTrack(SpotifyTrack): + def __init__(self, + name: str, + album: Album, + artists: List[Artist], + + added_at: str, + added_by: User, + is_local: bool, + + href: str = None, + spotify_id: str = None, + uri: str = None, + + disc_number: int = None, + duration_ms: int = None, + explicit: bool = None, + is_playable: bool = None, + + popularity: int = None + ): + super().__init__(name=name, album=album, artists=artists, + href=href, + spotify_id=spotify_id, + uri=uri, + + disc_number=disc_number, + duration_ms=duration_ms, + explicit=explicit, + is_playable=is_playable, + popularity=popularity) + + self.added_at = datetime.fromisoformat(added_at.replace('T', ' ').replace('Z', '')) + self.added_by = added_by + self.is_local = is_local diff --git a/spotframework/model/user.py b/spotframework/model/user.py new file mode 100644 index 0000000..cbefb73 --- /dev/null +++ b/spotframework/model/user.py @@ -0,0 +1,21 @@ + + +class User: + def __init__(self, + username: str, + + href: str = None, + uri: str = None, + + display_name: str = None, + ext_spotify: str = None): + self.username = username + + self.href = href + self.uri = uri + + self.display_name = display_name + self.ext_spotify = ext_spotify + + def __str__(self): + return f'{self.username}' diff --git a/spotframework/net/network.py b/spotframework/net/network.py index 886c2b6..43bd5cb 100644 --- a/spotframework/net/network.py +++ b/spotframework/net/network.py @@ -2,7 +2,9 @@ import requests import random import logging import time +from typing import List from . import const +from spotframework.net.parse import parse from spotframework.model.playlist import Playlist limit = 50 @@ -96,7 +98,7 @@ class Network: return None - def get_playlist(self, playlistid): + def get_playlist(self, playlistid: str): logger.info(f"{playlistid}") @@ -140,14 +142,7 @@ class Network: if resp: for responseplaylist in resp['items']: - - playlist = Playlist(responseplaylist['id'], responseplaylist['uri']) - playlist.name = responseplaylist['name'] - playlist.userid = responseplaylist['owner']['id'] - - playlists.append(playlist) - - # playlists = playlists + resp['items'] + playlists.append(parse.parse_playlist(responseplaylist)) if resp.get('next', None): more_playlists = self.get_playlists(offset + limit) @@ -167,7 +162,7 @@ class Network: playlists = self.get_playlists() if playlists: - return list(filter(lambda x: x.userid == self.user.username, playlists)) + return list(filter(lambda x: x.owner.username == self.user.username, playlists)) else: logger.error('no playlists returned to filter') return None @@ -184,7 +179,7 @@ class Network: if resp: if resp.get('items', None): - tracks += resp['items'] + tracks += [parse.parse_track(i) for i in resp.get('items', None)] else: logger.warning(f'{playlistid} no items returned') @@ -366,7 +361,7 @@ class Network: logger.error('error updating details') return None - def add_playlist_tracks(self, playlistid, uris): + def add_playlist_tracks(self, playlistid: str, uris: List[str]): logger.info(f"{playlistid}") @@ -410,7 +405,34 @@ class Network: else: resp = self._make_get_request('getRecommendations', 'recommendations', params=params) if resp: - return resp + if 'tracks' in resp: + return [parse.parse_track(i) for i in resp['tracks']] + else: + logger.error('no tracks returned') + return None else: logger.error('error getting recommendations') return None + + def write_playlist_object(self, + playlist: Playlist, + append_tracks: bool = False): + + if playlist.playlist_id: + if playlist.tracks == -1: + self.replace_playlist_tracks(playlist.playlist_id, []) + elif playlist.tracks: + if append_tracks: + self.add_playlist_tracks(playlist.playlist_id, [i.uri for i in playlist.tracks]) + else: + self.replace_playlist_tracks(playlist.playlist_id, [i.uri for i in playlist.tracks]) + + if playlist.name or playlist.collaborative or playlist.public or playlist.description: + self.change_playlist_details(playlist.playlist_id, + playlist.name, + playlist.public, + playlist.collaborative, + playlist.description) + + else: + logger.error('playlist has no id') diff --git a/spotframework/net/parse/__init__.py b/spotframework/net/parse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spotframework/net/parse/parse.py b/spotframework/net/parse/parse.py new file mode 100644 index 0000000..2cd4e5a --- /dev/null +++ b/spotframework/net/parse/parse.py @@ -0,0 +1,186 @@ +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.user import User + + +def parse_artist(artist_dict) -> Artist: + + name = artist_dict.get('name', None) + + href = artist_dict.get('href', None) + spotify_id = artist_dict.get('id', None) + uri = artist_dict.get('uri', None) + + genres = artist_dict.get('genres', None) + popularity = artist_dict.get('popularity', None) + + if name is None: + raise KeyError('artist name not found') + + return SpotifyArtist(name, + href=href, + spotify_id=spotify_id, + uri=uri, + + genres=genres, + popularity=popularity) + + +def parse_album(album_dict) -> Album: + + name = album_dict.get('name', None) + if name is None: + raise KeyError('album name not found') + + artists = [parse_artist(i) for i in album_dict.get('artists', [])] + + href = album_dict.get('href', None) + spotify_id = album_dict.get('id', None) + uri = album_dict.get('uri', None) + + genres = album_dict.get('genres', None) + tracks = [parse_track(i) for i in album_dict.get('tracks', [])] + + release_date = album_dict.get('release_date', None) + release_date_precision = album_dict.get('release_date_precision', None) + + label = album_dict.get('label', None) + popularity = album_dict.get('popularity', None) + + return SpotifyAlbum(name=name, + artists=artists, + + href=href, + spotify_id=spotify_id, + uri=uri, + + genres=genres, + tracks=tracks, + + release_date=release_date, + release_date_precision=release_date_precision, + + label=label, + popularity=popularity) + + +def parse_track(track_dict) -> Track: + + if 'track' in track_dict: + track = track_dict.get('track', None) + else: + track = track_dict + + name = track.get('name', None) + if name is None: + raise KeyError('track name not found') + + if track.get('album', None): + album = parse_album(track['album']) + else: + album = None + + # print(album.name) + + artists = [parse_artist(i) for i in track.get('artists', [])] + + href = track.get('href', None) + spotify_id = track.get('id', None) + uri = track.get('uri', None) + + disc_number = track.get('disc_number', None) + duration_ms = track.get('duration_ms', None) + explicit = track.get('explicit', None) + is_playable = track.get('is_playable', None) + + popularity = track.get('popularity', None) + + added_by = parse_user(track_dict.get('added_by')) if track_dict.get('added_by', None) else None + added_at = track_dict.get('added_at', None) + is_local = track_dict.get('is_local', None) + + # print(album.name) + + if added_at or added_by or is_local: + return PlaylistTrack(name=name, + album=album, + artists=artists, + + added_at=added_at, + added_by=added_by, + is_local=is_local, + + href=href, + spotify_id=spotify_id, + uri=uri, + + disc_number=disc_number, + duration_ms=duration_ms, + explicit=explicit, + is_playable=is_playable, + + popularity=popularity) + else: + return SpotifyTrack(name=name, + album=album, + artists=artists, + + href=href, + spotify_id=spotify_id, + uri=uri, + + disc_number=disc_number, + duration_ms=duration_ms, + explicit=explicit, + is_playable=is_playable, + + popularity=popularity) + + +def parse_user(user_dict): + display_name = user_dict.get('display_name', None) + + spotify_id = user_dict.get('id', None) + href = user_dict.get('href', None) + uri = user_dict.get('uri', None) + + return User(spotify_id, + href=href, + uri=uri, + display_name=display_name) + + +def parse_playlist(playlist_dict): + + collaborative = playlist_dict.get('collaborative', None) + + ext_spotify = None + if playlist_dict.get('external_urls', None): + if playlist_dict['external_urls'].get('spotify', None): + ext_spotify = playlist_dict['external_urls']['spotify'] + + href = playlist_dict.get('href', None) + playlist_id = playlist_dict.get('id', None) + description = playlist_dict.get('description', None) + + name = playlist_dict.get('name', None) + + if playlist_dict.get('owner', None): + owner = parse_user(playlist_dict.get('owner')) + else: + owner = None + + 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) diff --git a/spotframework/net/user.py b/spotframework/net/user.py index a8181f7..27c638f 100644 --- a/spotframework/net/user.py +++ b/spotframework/net/user.py @@ -1,13 +1,16 @@ import requests +from spotframework.model.user import User from base64 import b64encode import logging logger = logging.getLogger(__name__) -class User: +class NetworkUser(User): def __init__(self, client_id, client_secret, access_token, refresh_token): + super().__init__('') + self.accesstoken = access_token self.refreshtoken = refresh_token @@ -15,8 +18,7 @@ class User: self.client_secret = client_secret self.refresh_token() - - self.username = self.get_info()['id'] + self.refresh_info() def refresh_token(self): @@ -33,6 +35,25 @@ class User: else: logger.error(f'http error {req.status_code}') + def refresh_info(self): + info = self.get_info() + + if info.get('display_name', None): + self.display_name = info['display_name'] + + if info.get('external_urls', None): + if info['external_urls'].get('spotify', None): + self.ext_spotify = info['external_urls']['spotify'] + + if info.get('href', None): + self.href = info['href'] + + if info.get('id', None): + self.username = info['id'] + + if info.get('uri', None): + self.uri = info['uri'] + def get_info(self): headers = {'Authorization': 'Bearer %s' % self.accesstoken}