filter json by dataclass keys, multi-type uris on network methods, introducing playlists

This commit is contained in:
aj 2020-08-07 10:59:46 +01:00
parent 18fbedf115
commit cceba0bd78
13 changed files with 322 additions and 190 deletions

View File

@ -35,7 +35,7 @@ if __name__ == '__main__':
for playlist in playlists: for playlist in playlists:
try: try:
playlist.tracks = network.get_playlist_tracks(playlist.uri) playlist.tracks = network.get_playlist_tracks(uri=playlist.uri)
csvwrite.export_playlist(playlist, totalpath) csvwrite.export_playlist(playlist, totalpath)
except SpotifyNetworkException: except SpotifyNetworkException:
logger.exception(f'error occured during {playlist.name} track retrieval') logger.exception(f'error occured during {playlist.name} track retrieval')

View File

@ -145,9 +145,10 @@ class PlaylistEngine:
counter_track = track counter_track = track
if counter_track != tracks_to_sort[0]: if counter_track != tracks_to_sort[0]:
self.net.reorder_playlist_tracks(playlist.uri, self.net.reorder_playlist_tracks(uri=playlist.uri,
i + tracks_to_sort.index(counter_track), range_start=i + tracks_to_sort.index(counter_track),
1, i) range_length=1,
insert_before=i)
tracks_to_sort.remove(counter_track) tracks_to_sort.remove(counter_track)
def execute_playlist(self, def execute_playlist(self,
@ -179,7 +180,7 @@ class PlaylistEngine:
logger.error('no string generated') logger.error('no string generated')
return None return None
resp = self.net.change_playlist_details(uri, description=string) resp = self.net.change_playlist_details(uri=uri, description=string)
if resp: if resp:
return resp return resp
else: else:
@ -230,7 +231,7 @@ class PlaylistSource(TrackSource):
playlist: FullPlaylist) -> None: playlist: FullPlaylist) -> None:
logger.info(f"pulling tracks for {playlist.name}") logger.info(f"pulling tracks for {playlist.name}")
tracks = self.net.get_playlist_tracks(playlist.uri) tracks = self.net.get_playlist_tracks(uri=playlist.uri)
if tracks and len(tracks) > 0: if tracks and len(tracks) > 0:
playlist.tracks = tracks playlist.tracks = tracks
else: else:
@ -263,7 +264,7 @@ class PlaylistSource(TrackSource):
if playlist: if playlist:
playlists.append(playlist) playlists.append(playlist)
else: else:
playlist = self.net.get_playlist(uri) playlist = self.net.get_playlist(uri=uri)
if playlist: if playlist:
playlists.append(playlist) playlists.append(playlist)
self.playlists.append(playlist) self.playlists.append(playlist)

View File

@ -0,0 +1,29 @@
import logging
logger = logging.getLogger(__name__)
def init_with_key_filter(class_type: type, dict_obj: dict = None, merge_unrecognised_keys: bool = True, **kwargs):
if '__dataclass_fields__' not in class_type.__dict__:
logger.error(f'{class_type} not a dataclass')
return
if dict_obj is None:
dict_obj = dict()
filtered_dict = dict()
unrecognised_keys = dict()
for i, j in {**dict_obj, **kwargs}.items():
if i in class_type.__dict__['__dataclass_fields__'].keys():
filtered_dict[i] = j
else:
unrecognised_keys[i] = j
logger.warning(f'unrecognised key found for {class_type}: {i} {type(j)}')
obj = class_type(**filtered_dict)
if merge_unrecognised_keys:
for i, j in unrecognised_keys.items():
setattr(obj, i, j)
return obj

View File

@ -3,11 +3,15 @@ from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import List, Union from typing import List, Union
import logging
from spotframework.model.uri import Uri from spotframework.model.uri import Uri
import spotframework.model.artist import spotframework.model.artist
import spotframework.model.service import spotframework.model.service
import spotframework.model.track import spotframework.model.track
from spotframework.model import init_with_key_filter
logger = logging.getLogger(__name__)
@dataclass @dataclass
class SimplifiedAlbum: class SimplifiedAlbum:
@ -39,14 +43,14 @@ class SimplifiedAlbum:
self.uri = Uri(self.uri) self.uri = Uri(self.uri)
if self.uri: if self.uri:
if self.uri.object_type != Uri.ObjectType.album: if self.uri.object_type not in [Uri.ObjectType.album, Uri.ObjectType.show]:
raise TypeError('provided uri not for an album') raise TypeError('provided uri not for an album')
if all((isinstance(i, dict) for i in self.artists)): if all((isinstance(i, dict) for i in self.artists)):
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists] self.artists = [init_with_key_filter(spotframework.model.artist.SimplifiedArtist, i) for i in self.artists]
if all((isinstance(i, dict) for i in self.images)): if all((isinstance(i, dict) for i in self.images)):
self.images = [spotframework.model.service.Image(**i) for i in self.images] self.images = [init_with_key_filter(spotframework.model.service.Image, i) for i in self.images]
if isinstance(self.release_date, str): if isinstance(self.release_date, str):
if self.release_date_precision == 'year': if self.release_date_precision == 'year':
@ -55,6 +59,11 @@ class SimplifiedAlbum:
self.release_date = datetime.strptime(self.release_date, '%Y-%m') self.release_date = datetime.strptime(self.release_date, '%Y-%m')
elif self.release_date_precision == 'day': elif self.release_date_precision == 'day':
self.release_date = datetime.strptime(self.release_date, '%Y-%m-%d') self.release_date = datetime.strptime(self.release_date, '%Y-%m-%d')
else:
logger.error(f'invalid release date type {self.release_date_precision} - {self.release_date}')
elif self.release_date is None and self.release_date_precision is None: # for podcasts
self.release_date = datetime(year=1900, month=1, day=1)
@property @property
def artists_names(self) -> str: def artists_names(self) -> str:
@ -82,33 +91,10 @@ class AlbumFull(SimplifiedAlbum):
tracks: List[spotframework.model.track.SimplifiedTrack] = None tracks: List[spotframework.model.track.SimplifiedTrack] = None
def __post_init__(self): def __post_init__(self):
super().__post_init__()
if isinstance(self.album_type, str):
self.album_type = SimplifiedAlbum.Type[self.album_type]
if isinstance(self.uri, str):
self.uri = Uri(self.uri)
if self.uri:
if self.uri.object_type != Uri.ObjectType.album:
raise TypeError('provided uri not for an album')
if all((isinstance(i, dict) for i in self.artists)):
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists]
if all((isinstance(i, dict) for i in self.images)):
self.images = [spotframework.model.service.Image(**i) for i in self.images]
if all((isinstance(i, dict) for i in self.tracks)): if all((isinstance(i, dict) for i in self.tracks)):
self.tracks = [spotframework.model.track.SimplifiedTrack(**i) for i in self.tracks] self.tracks = [init_with_key_filter(spotframework.model.track.SimplifiedTrack, i) for i in self.tracks]
if isinstance(self.release_date, str):
if self.release_date_precision == 'year':
self.release_date = datetime.strptime(self.release_date, '%Y')
elif self.release_date_precision == 'month':
self.release_date = datetime.strptime(self.release_date, '%Y-%m')
elif self.release_date_precision == 'day':
self.release_date = datetime.strptime(self.release_date, '%Y-%m-%d')
@dataclass @dataclass
@ -118,7 +104,7 @@ class LibraryAlbum:
def __post_init__(self): def __post_init__(self):
if isinstance(self.album, dict): if isinstance(self.album, dict):
self.album = AlbumFull(**self.album) self.album = init_with_key_filter(AlbumFull, self.album)
if isinstance(self.added_at, str): if isinstance(self.added_at, str):
self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z') self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z')

View File

@ -3,6 +3,8 @@ from typing import List, Union
from spotframework.model.uri import Uri from spotframework.model.uri import Uri
from spotframework.model.service import Image from spotframework.model.service import Image
from spotframework.model import init_with_key_filter
@dataclass @dataclass
class SimplifiedArtist: class SimplifiedArtist:
@ -18,7 +20,7 @@ class SimplifiedArtist:
self.uri = Uri(self.uri) self.uri = Uri(self.uri)
if self.uri: if self.uri:
if self.uri.object_type != Uri.ObjectType.artist: if self.uri.object_type not in [Uri.ObjectType.artist, Uri.ObjectType.show]:
raise TypeError('provided uri not for an artist') raise TypeError('provided uri not for an artist')
def __str__(self): def __str__(self):
@ -32,12 +34,7 @@ class ArtistFull(SimplifiedArtist):
popularity: int popularity: int
def __post_init__(self): def __post_init__(self):
if isinstance(self.uri, str): super().__post_init__()
self.uri = Uri(self.uri)
if self.uri:
if self.uri.object_type != Uri.ObjectType.artist:
raise TypeError('provided uri not for an artist')
if all((isinstance(i, dict) for i in self.images)): if all((isinstance(i, dict) for i in self.images)):
self.images = [Image(**i) for i in self.images] self.images = [init_with_key_filter(Image, i) for i in self.images]

View File

@ -3,6 +3,7 @@ from spotframework.model.user import PublicUser
from spotframework.model.track import TrackFull, PlaylistTrack from spotframework.model.track import TrackFull, PlaylistTrack
from spotframework.model.uri import Uri from spotframework.model.uri import Uri
from spotframework.model.service import Image from spotframework.model.service import Image
from spotframework.model import init_with_key_filter
from tabulate import tabulate from tabulate import tabulate
from typing import List, Union from typing import List, Union
import logging import logging
@ -39,10 +40,10 @@ class SimplifiedPlaylist:
raise TypeError('provided uri not for a playlist') raise TypeError('provided uri not for a playlist')
if all((isinstance(i, dict) for i in self.images)): if all((isinstance(i, dict) for i in self.images)):
self.images = [Image(**i) for i in self.images] self.images = [init_with_key_filter(Image, i) for i in self.images]
if isinstance(self.owner, dict): if isinstance(self.owner, dict):
self.owner = PublicUser(**self.owner) self.owner = init_with_key_filter(PublicUser, self.owner)
def has_tracks(self) -> bool: def has_tracks(self) -> bool:
return bool(len(self.tracks) > 0) return bool(len(self.tracks) > 0)
@ -131,10 +132,10 @@ class FullPlaylist(SimplifiedPlaylist):
raise TypeError('provided uri not for a playlist') raise TypeError('provided uri not for a playlist')
if all((isinstance(i, dict) for i in self.images)): if all((isinstance(i, dict) for i in self.images)):
self.images = [Image(**i) for i in self.images] self.images = [init_with_key_filter(Image, i) for i in self.images]
if isinstance(self.owner, dict): if isinstance(self.owner, dict):
self.owner = PublicUser(**self.owner) self.owner = init_with_key_filter(PublicUser, self.owner)
def __str__(self): def __str__(self):
prefix = f'\n==={self.name}===\n\n' if self.name is not None else '' prefix = f'\n==={self.name}===\n\n' if self.name is not None else ''

View File

@ -0,0 +1,110 @@
from typing import List, Union
from dataclasses import dataclass
from datetime import datetime
from spotframework.model import init_with_key_filter
from spotframework.model.service import Image
from spotframework.model.uri import Uri
@dataclass
class ResumePoint:
fully_played: bool
resume_position_ms: int
@dataclass
class SimplifiedEpisode:
audio_preview_url: str
description: str
duration_ms: int
explicit: bool
external_urls: dict
href: str
id: str
images: List[Image]
is_externally_hosted: bool
is_playable: bool
languages: List[str]
name: str
release_date: datetime
release_date_precision: str
resume_point: ResumePoint
type: str
uri: Union[str, Uri]
def __post_init__(self):
if isinstance(self.uri, str):
self.uri = Uri(self.uri)
if self.uri:
if self.uri.object_type != Uri.ObjectType.episode:
raise TypeError('provided uri not for an episode')
if isinstance(self.resume_point, ResumePoint):
self.resume_point = init_with_key_filter(ResumePoint, self.resume_point)
if all((isinstance(i, dict) for i in self.images)):
self.images = [init_with_key_filter(Image, i) for i in self.images]
if isinstance(self.release_date, str):
if self.release_date_precision == 'year':
self.release_date = datetime.strptime(self.release_date, '%Y')
elif self.release_date_precision == 'month':
self.release_date = datetime.strptime(self.release_date, '%Y-%m')
elif self.release_date_precision == 'day':
self.release_date = datetime.strptime(self.release_date, '%Y-%m-%d')
@dataclass
class SimplifiedShow:
available_markets: List[str]
copyrights: List[dict]
description: str
explicit: bool
external_urls: dict
href: str
id: str
images: List[Image]
is_externally_hosted: bool
languages: List[str]
media_type: str
name: str
publisher: str
type: str
uri: Union[str, Uri]
def __post_init__(self):
if isinstance(self.uri, str):
self.uri = Uri(self.uri)
if self.uri:
if self.uri.object_type != Uri.ObjectType.episode:
raise TypeError('provided uri not for an episode')
if all((isinstance(i, dict) for i in self.images)):
self.images = [init_with_key_filter(Image, i) for i in self.images]
@dataclass
class EpisodeFull(SimplifiedEpisode):
show: SimplifiedShow
def __post_init__(self):
super().__post_init__()
if isinstance(self.show, SimplifiedShow):
self.show = init_with_key_filter(SimplifiedShow, self.show)
@dataclass
class ShowFull(SimplifiedShow):
episodes: List[SimplifiedEpisode]
@dataclass
class SavedShow:
added_at: datetime
show: ShowFull
def __post_init__(self):
if isinstance(self.show, ShowFull):
self.show = init_with_key_filter(ShowFull, self.show)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from typing import Union, List from typing import Union, List
from datetime import datetime from datetime import datetime
from dataclasses import dataclass, field from dataclasses import dataclass, field
import logging
import spotframework.model import spotframework.model
from spotframework.model.uri import Uri from spotframework.model.uri import Uri
@ -10,6 +11,11 @@ import spotframework.model.album
import spotframework.model.artist import spotframework.model.artist
import spotframework.model.service import spotframework.model.service
import spotframework.model.user import spotframework.model.user
from spotframework.model.podcast import EpisodeFull
from spotframework.model import init_with_key_filter
logger = logging.getLogger(__name__)
@dataclass @dataclass
@ -37,11 +43,11 @@ class SimplifiedTrack:
self.uri = Uri(self.uri) self.uri = Uri(self.uri)
if self.uri: if self.uri:
if self.uri.object_type != Uri.ObjectType.track: if self.uri.object_type not in [Uri.ObjectType.track, Uri.ObjectType.episode]:
raise TypeError('provided uri not for a track') raise TypeError('provided uri not for a track')
if all((isinstance(i, dict) for i in self.artists)): if all((isinstance(i, dict) for i in self.artists)):
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists] self.artists = [init_with_key_filter(spotframework.model.artist.SimplifiedArtist, i) for i in self.artists]
@property @property
def artists_names(self) -> str: def artists_names(self) -> str:
@ -71,18 +77,10 @@ class TrackFull(SimplifiedTrack):
return self.album.artists_names return self.album.artists_names
def __post_init__(self): def __post_init__(self):
if isinstance(self.uri, str): super().__post_init__()
self.uri = Uri(self.uri)
if self.uri:
if self.uri.object_type != Uri.ObjectType.track:
raise TypeError('provided uri not for a track')
if all((isinstance(i, dict) for i in self.artists)):
self.artists = [spotframework.model.artist.SimplifiedArtist(**i) for i in self.artists]
if isinstance(self.album, dict): if isinstance(self.album, dict):
self.album = spotframework.model.album.SimplifiedAlbum(**self.album) self.album = init_with_key_filter(spotframework.model.album.SimplifiedAlbum, self.album)
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, TrackFull) and other.uri == self.uri return isinstance(other, TrackFull) and other.uri == self.uri
@ -95,7 +93,7 @@ class LibraryTrack:
def __post_init__(self): def __post_init__(self):
if isinstance(self.track, dict): if isinstance(self.track, dict):
self.track = TrackFull(**self.track) self.track = init_with_key_filter(TrackFull, self.track)
if isinstance(self.added_at, str): if isinstance(self.added_at, str):
self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z') self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z')
@ -107,15 +105,31 @@ class PlaylistTrack:
added_by: spotframework.model.user.PublicUser added_by: spotframework.model.user.PublicUser
is_local: bool is_local: bool
primary_color: str primary_color: str
track: TrackFull track: Union[TrackFull, EpisodeFull]
video_thumbnail: dict video_thumbnail: dict
def __post_init__(self): def __post_init__(self):
if isinstance(self.track, dict): if isinstance(self.track, dict):
self.track = TrackFull(**self.track)
# below seems more intuitive, currently parsing episode to track/album/artist structure for
# serialising over api, below could be implemented
# obj_type = None
# if self.track['type'] == 'track':
# obj_type = TrackFull
#
# if self.track['type'] == 'episode':
# obj_type = EpisodeFull
#
# if obj_type is None:
# raise TypeError(f'unkown obj type found {self.track["type"]}')
obj_type = TrackFull
self.track = init_with_key_filter(obj_type, self.track)
if isinstance(self.added_by, dict): if isinstance(self.added_by, dict):
self.added_by = spotframework.model.user.PublicUser(**self.added_by) self.added_by = init_with_key_filter(spotframework.model.user.PublicUser, self.added_by)
if isinstance(self.added_at, str): if isinstance(self.added_at, str):
self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z') self.added_at = datetime.strptime(self.added_at, '%Y-%m-%dT%H:%M:%S%z')
@ -129,9 +143,9 @@ class PlayedTrack:
def __post_init__(self): def __post_init__(self):
if isinstance(self.context, dict): if isinstance(self.context, dict):
self.context = Context(**self.context) self.context = init_with_key_filter(Context, self.context)
if isinstance(self.track, dict): if isinstance(self.track, dict):
self.track = TrackFull(**self.track) self.track = init_with_key_filter(TrackFull, self.track)
if isinstance(self.played_at, str): if isinstance(self.played_at, str):
self.played_at = datetime.strptime(self.played_at, '%Y-%m-%dT%H:%M:%S%z') self.played_at = datetime.strptime(self.played_at, '%Y-%m-%dT%H:%M:%S%z')
@ -345,13 +359,13 @@ class CurrentlyPlaying:
def __post_init__(self): def __post_init__(self):
if isinstance(self.context, Context): if isinstance(self.context, Context):
self.context = Context(**self.context) self.context = init_with_key_filter(Context, self.context)
if isinstance(self.item, spotframework.model.track.SimplifiedTrack): if isinstance(self.item, spotframework.model.track.SimplifiedTrack):
self.item = spotframework.model.track.SimplifiedTrack(**self.item) self.item = init_with_key_filter(spotframework.model.track.SimplifiedTrack, self.item)
if isinstance(self.device, Device): if isinstance(self.device, Device):
self.device = Device(**self.device) self.device = init_with_key_filter(Device, self.device)
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, CurrentlyPlaying) and other.item == self.item and other.context == self.context return isinstance(other, CurrentlyPlaying) and other.item == self.item and other.context == self.context
@ -389,7 +403,7 @@ class Recommendations:
def __post_init__(self): def __post_init__(self):
if all((isinstance(i, dict) for i in self.seeds)): if all((isinstance(i, dict) for i in self.seeds)):
self.seeds = [RecommendationsSeed(**i) for i in self.seeds] self.seeds = [init_with_key_filter(RecommendationsSeed, i) for i in self.seeds]
if all((isinstance(i, dict) for i in self.tracks)): if all((isinstance(i, dict) for i in self.tracks)):
self.tracks = [spotframework.model.track.TrackFull(**i) for i in self.tracks] self.tracks = [init_with_key_filter(spotframework.model.track.TrackFull, i) for i in self.tracks]

View File

@ -9,6 +9,8 @@ class Uri:
artist = 3 artist = 3
user = 4 user = 4
playlist = 5 playlist = 5
episode = 6
show = 7
def __init__(self, input_string: str): def __init__(self, input_string: str):
self.object_type = None self.object_type = None

View File

@ -2,6 +2,7 @@ from typing import Union, List
from dataclasses import dataclass, field from dataclasses import dataclass, field
from spotframework.model.uri import Uri from spotframework.model.uri import Uri
from spotframework.model.service import Image from spotframework.model.service import Image
from spotframework.model import init_with_key_filter
@dataclass @dataclass
@ -26,7 +27,7 @@ class PublicUser:
raise TypeError('provided uri not for a user') raise TypeError('provided uri not for a user')
if all((isinstance(i, dict) for i in self.images)): if all((isinstance(i, dict) for i in self.images)):
self.images = [Image(**i) for i in self.images] self.images = [init_with_key_filter(Image, i) for i in self.images]
def __str__(self): def __str__(self):
return f'{self.id}' return f'{self.id}'
@ -47,5 +48,5 @@ class PrivateUser(PublicUser):
raise TypeError('provided uri not for a user') raise TypeError('provided uri not for a user')
if all((isinstance(i, dict) for i in self.images)): if all((isinstance(i, dict) for i in self.images)):
self.images = [Image(**i) for i in self.images] self.images = [init_with_key_filter(Image, i) for i in self.images]

View File

@ -8,14 +8,19 @@ from typing import List, Optional, Union
import datetime import datetime
from json import JSONDecodeError from json import JSONDecodeError
from spotframework.model.artist import ArtistFull
from spotframework.model.user import PublicUser
from spotframework.net.user import NetworkUser from spotframework.net.user import NetworkUser
from spotframework.model import init_with_key_filter
from spotframework.model.user import PublicUser
from spotframework.model.playlist import SimplifiedPlaylist, FullPlaylist from spotframework.model.playlist import SimplifiedPlaylist, FullPlaylist
from spotframework.model.artist import ArtistFull
from spotframework.model.album import AlbumFull, LibraryAlbum, SimplifiedAlbum
from spotframework.model.track import SimplifiedTrack, TrackFull, PlaylistTrack, PlayedTrack, LibraryTrack, \ from spotframework.model.track import SimplifiedTrack, TrackFull, PlaylistTrack, PlayedTrack, LibraryTrack, \
AudioFeatures, Device, CurrentlyPlaying, Recommendations AudioFeatures, Device, CurrentlyPlaying, Recommendations
from spotframework.model.album import AlbumFull, LibraryAlbum, SimplifiedAlbum from spotframework.model.podcast import SimplifiedEpisode, EpisodeFull
from spotframework.model.uri import Uri from spotframework.model.uri import Uri
from spotframework.util.decorators import inject_uri
limit = 50 limit = 50
@ -245,9 +250,9 @@ class Network:
def refresh_user_info(self): def refresh_user_info(self):
self.user.user = self.get_current_user() self.user.user = self.get_current_user()
@inject_uri(uris=False)
def get_playlist(self, def get_playlist(self,
uri: Uri = None, uri: Uri,
uri_string: str = None,
tracks: bool = True) -> FullPlaylist: tracks: bool = True) -> FullPlaylist:
"""get playlist object with tracks for uri """get playlist object with tracks for uri
@ -257,16 +262,10 @@ class Network:
:return: playlist object :return: playlist object
""" """
if uri is None and uri_string is None:
raise NameError('no uri provided')
if uri_string is not None:
uri = Uri(uri_string)
logger.info(f"retrieving {uri}") logger.info(f"retrieving {uri}")
resp = self.get_request(f'playlists/{uri.object_id}') resp = self.get_request(f'playlists/{uri.object_id}')
playlist = FullPlaylist(**resp) playlist = init_with_key_filter(FullPlaylist, resp)
if resp.get('tracks') and tracks: if resp.get('tracks') and tracks:
if 'next' in resp['tracks']: if 'next' in resp['tracks']:
@ -275,10 +274,10 @@ class Network:
track_pager = PageCollection(net=self, page=resp['tracks']) track_pager = PageCollection(net=self, page=resp['tracks'])
track_pager.continue_iteration() track_pager.continue_iteration()
playlist.tracks = [PlaylistTrack(**i) for i in track_pager.items] playlist.tracks = [init_with_key_filter(PlaylistTrack, i) for i in track_pager.items]
else: else:
logger.debug(f'parsing {len(resp.get("tracks"))} tracks for {uri}') logger.debug(f'parsing {len(resp.get("tracks"))} tracks for {uri}')
playlist.tracks = [PlaylistTrack(**i) for i in resp.get('tracks', [])] playlist.tracks = [init_with_key_filter(PlaylistTrack, i) for i in resp.get('tracks', [])]
return playlist return playlist
@ -309,7 +308,7 @@ class Network:
public=public, public=public,
collaborative=collaborative, collaborative=collaborative,
description=description) description=description)
return FullPlaylist(**req) return init_with_key_filter(FullPlaylist, req)
def get_playlists(self, response_limit: int = None) -> Optional[List[SimplifiedPlaylist]]: def get_playlists(self, response_limit: int = None) -> Optional[List[SimplifiedPlaylist]]:
"""get current users playlists """get current users playlists
@ -325,7 +324,7 @@ class Network:
pager.total_limit = response_limit pager.total_limit = response_limit
pager.iterate() pager.iterate()
return_items = [SimplifiedPlaylist(**i) for i in pager.items] return_items = [init_with_key_filter(SimplifiedPlaylist, i) for i in pager.items]
if len(return_items) == 0: if len(return_items) == 0:
logger.error('no playlists returned') logger.error('no playlists returned')
@ -346,7 +345,7 @@ class Network:
pager.total_limit = response_limit pager.total_limit = response_limit
pager.iterate() pager.iterate()
return_items = [LibraryAlbum(**i) for i in pager.items] return_items = [init_with_key_filter(LibraryAlbum, i) for i in pager.items]
if len(return_items) == 0: if len(return_items) == 0:
logger.error('no albums returned') logger.error('no albums returned')
@ -367,7 +366,7 @@ class Network:
pager.total_limit = response_limit pager.total_limit = response_limit
pager.iterate() pager.iterate()
return_items = [LibraryTrack(**i) for i in pager.items] return_items = [init_with_key_filter(LibraryTrack, i) for i in pager.items]
if len(return_items) == 0: if len(return_items) == 0:
logger.error('no tracks returned') logger.error('no tracks returned')
@ -394,9 +393,9 @@ class Network:
else: else:
logger.error('no playlists returned to filter') logger.error('no playlists returned to filter')
@inject_uri(uris=False)
def get_playlist_tracks(self, def get_playlist_tracks(self,
uri: Uri = None, uri: Uri,
uri_string: str = None,
response_limit: int = None) -> List[PlaylistTrack]: response_limit: int = None) -> List[PlaylistTrack]:
"""get list of playlists tracks for uri """get list of playlists tracks for uri
@ -406,12 +405,6 @@ class Network:
:return: list of playlist tracks if available :return: list of playlist tracks if available
""" """
if uri is None and uri_string is None:
raise NameError('no uri provided')
if uri_string is not None:
uri = Uri(uri_string)
logger.info(f"paging tracks for {uri}") logger.info(f"paging tracks for {uri}")
pager = PageCollection(net=self, url=f'playlists/{uri.object_id}/tracks', name='getPlaylistTracks') pager = PageCollection(net=self, url=f'playlists/{uri.object_id}/tracks', name='getPlaylistTracks')
@ -419,7 +412,7 @@ class Network:
pager.total_limit = response_limit pager.total_limit = response_limit
pager.iterate() pager.iterate()
return_items = [PlaylistTrack(**i) for i in pager.items] return_items = [init_with_key_filter(PlaylistTrack, i) for i in pager.items]
if len(return_items) == 0: if len(return_items) == 0:
logger.error('no tracks returned') logger.error('no tracks returned')
@ -435,7 +428,7 @@ class Network:
if len(resp['devices']) == 0: if len(resp['devices']) == 0:
logger.error('no devices returned') logger.error('no devices returned')
return [Device(**i) for i in resp['devices']] return [init_with_key_filter(Device, i) for i in resp['devices']]
def get_recently_played_tracks(self, def get_recently_played_tracks(self,
response_limit: int = None, response_limit: int = None,
@ -468,7 +461,7 @@ class Network:
pager.total_limit = 20 pager.total_limit = 20
pager.continue_iteration() pager.continue_iteration()
return [PlayedTrack(**i) for i in pager.items] return [init_with_key_filter(PlayedTrack, i) for i in pager.items]
def get_player(self) -> CurrentlyPlaying: def get_player(self) -> CurrentlyPlaying:
"""get currently playing snapshot (player)""" """get currently playing snapshot (player)"""
@ -476,7 +469,7 @@ class Network:
logger.info("polling player") logger.info("polling player")
resp = self.get_request('me/player') resp = self.get_request('me/player')
return CurrentlyPlaying(**resp) return init_with_key_filter(CurrentlyPlaying, resp)
def get_device_id(self, device_name: str) -> Optional[str]: def get_device_id(self, device_name: str) -> Optional[str]:
"""return device id of device as searched for by name """return device id of device as searched for by name
@ -498,27 +491,20 @@ class Network:
logger.info(f"getting current user") logger.info(f"getting current user")
resp = self.get_request('me') resp = self.get_request('me')
return PublicUser(**resp) return init_with_key_filter(PublicUser, resp)
def change_playback_device(self, device_id: str): def change_playback_device(self, device_id: str):
"""migrate playback to different device""" """migrate playback to different device"""
logger.info(f'shifting playback to {device_id}') logger.info(f'shifting playback to {device_id}')
self.put_request('me/player', device_ids=[device_id], play=True) self.put_request('me/player', device_ids=[device_id], play=True)
@inject_uri(uri_optional=True, uris_optional=True)
def play(self, def play(self,
uri: Uri = None, uri: Uri = None,
uri_string: str = None,
uris: List[Uri] = None, uris: List[Uri] = None,
uri_strings: List[str] = None,
deviceid: str = None): deviceid: str = None):
"""begin playback""" """begin playback"""
if uri_string is not None:
uri = Uri(uri_string)
if uri_strings is not None:
uris = [Uri(i) for i in uri_strings]
logger.info(f"{uri}{' ' + deviceid if deviceid is not None else ''}") logger.info(f"{uri}{' ' + deviceid if deviceid is not None else ''}")
if deviceid is not None: if deviceid is not None:
@ -600,25 +586,19 @@ class Network:
else: else:
logger.error(f"{volume} not accepted value") logger.error(f"{volume} not accepted value")
@inject_uri
def replace_playlist_tracks(self, def replace_playlist_tracks(self,
uri: Uri = None, uri: Uri,
uri_string: str = None, uris: List[Uri]) -> Optional[List[str]]:
uris: List[Uri] = None,
uri_strings: List[str] = None) -> Optional[List[str]]:
if uri_string is not None:
uri = Uri(uri_string)
if uri_strings is not None:
uris = [Uri(i) for i in uri_strings]
logger.info(f"replacing {uri} with {'0' if uris is None else len(uris)} tracks") logger.info(f"replacing {uri} with {'0' if uris is None else len(uris)} tracks")
self.put_request(f'playlists/{uri.object_id}/tracks', uris=[str(i) for i in uris[:100]]) self.put_request(f'playlists/{uri.object_id}/tracks', uris=[str(i) for i in uris[:100]])
if len(uris) > 100: if len(uris) > 100:
return self.add_playlist_tracks(uri, uris[100:]) return self.add_playlist_tracks(uri=uri, uris=uris[100:])
@inject_uri(uris=False)
def change_playlist_details(self, def change_playlist_details(self,
uri: Uri, uri: Uri,
name: str = None, name: str = None,
@ -638,6 +618,7 @@ class Network:
collaborative=collaborative, collaborative=collaborative,
description=description) description=description)
@inject_uri
def add_playlist_tracks(self, uri: Uri, uris: List[Uri]) -> List[str]: def add_playlist_tracks(self, uri: Uri, uris: List[Uri]) -> List[str]:
logger.info(f"adding {len(uris)} tracks to {uri}") logger.info(f"adding {len(uris)} tracks to {uri}")
@ -648,7 +629,7 @@ class Network:
] ]
if len(uris) > 100: if len(uris) > 100:
snapshot_ids += self.add_playlist_tracks(uri, uris[100:]) snapshot_ids += self.add_playlist_tracks(uri=uri, uris=uris[100:])
return snapshot_ids return snapshot_ids
@ -673,7 +654,7 @@ class Network:
if len(params) == 1: if len(params) == 1:
logger.warning('update dictionairy length 0') logger.warning('update dictionairy length 0')
else: else:
return Recommendations(**self.get_request('recommendations', params=params)) return init_with_key_filter(Recommendations, self.get_request('recommendations', params=params))
def write_playlist_object(self, def write_playlist_object(self,
playlist: FullPlaylist, playlist: FullPlaylist,
@ -686,22 +667,23 @@ class Network:
self.replace_playlist_tracks(uri=playlist.uri, uris=[]) self.replace_playlist_tracks(uri=playlist.uri, uris=[])
elif playlist.tracks: elif playlist.tracks:
if append_tracks: if append_tracks:
self.add_playlist_tracks(playlist.uri, [i.uri for i in playlist.tracks if self.add_playlist_tracks(uri=playlist.uri, uris=[i.uri for i in playlist.tracks if
isinstance(i, SimplifiedTrack)]) isinstance(i, SimplifiedTrack)])
else: else:
self.replace_playlist_tracks(uri=playlist.uri, uris=[i.uri for i in playlist.tracks if self.replace_playlist_tracks(uri=playlist.uri, uris=[i.uri for i in playlist.tracks if
isinstance(i, SimplifiedTrack)]) isinstance(i, SimplifiedTrack)])
if playlist.name or playlist.collaborative or playlist.public or playlist.description: if playlist.name or playlist.collaborative or playlist.public or playlist.description:
self.change_playlist_details(playlist.uri, self.change_playlist_details(uri=playlist.uri,
playlist.name, name=playlist.name,
playlist.public, public=playlist.public,
playlist.collaborative, collaborative=playlist.collaborative,
playlist.description) description=playlist.description)
else: else:
logger.error('playlist has no id') logger.error('playlist has no id')
@inject_uri(uris=False)
def reorder_playlist_tracks(self, def reorder_playlist_tracks(self,
uri: Uri, uri: Uri,
range_start: int, range_start: int,
@ -725,6 +707,7 @@ class Network:
range_length=range_length, range_length=range_length,
insert_before=insert_before) insert_before=insert_before)
@inject_uri(uri=False)
def get_track_audio_features(self, uris: List[Uri]) -> Optional[List[AudioFeatures]]: def get_track_audio_features(self, uris: List[Uri]) -> Optional[List[AudioFeatures]]:
logger.info(f'getting {len(uris)} features') logger.info(f'getting {len(uris)} features')
@ -734,7 +717,7 @@ class Network:
resp = self.get_request(url='audio-features', ids=','.join(i.object_id for i in chunk)) resp = self.get_request(url='audio-features', ids=','.join(i.object_id for i in chunk))
if resp.get('audio_features', None): if resp.get('audio_features', None):
return [AudioFeatures(**i) for i in resp['audio_features']] return [init_with_key_filter(AudioFeatures, i) for i in resp['audio_features']]
else: else:
logger.error('no audio features included') logger.error('no audio features included')
@ -747,7 +730,7 @@ class Network:
logger.info(f'populating {len(tracks)} features') logger.info(f'populating {len(tracks)} features')
if isinstance(tracks, SimplifiedTrack): if isinstance(tracks, SimplifiedTrack):
audio_features = self.get_track_audio_features([tracks.uri]) audio_features = self.get_track_audio_features(uris=[tracks.uri])
if audio_features: if audio_features:
if len(audio_features) == 1: if len(audio_features) == 1:
@ -760,7 +743,7 @@ class Network:
elif isinstance(tracks, List): elif isinstance(tracks, List):
if all(isinstance(i, SimplifiedTrack) for i in tracks): if all(isinstance(i, SimplifiedTrack) for i in tracks):
audio_features = self.get_track_audio_features([i.uri for i in tracks]) audio_features = self.get_track_audio_features(uris=[i.uri for i in tracks])
if audio_features: if audio_features:
if len(audio_features) != len(tracks): if len(audio_features) != len(tracks):
@ -775,15 +758,8 @@ class Network:
else: else:
raise TypeError('must provide either single or list of spotify tracks') raise TypeError('must provide either single or list of spotify tracks')
def get_tracks(self, @inject_uri(uri=False)
uris: List[Uri] = None, def get_tracks(self, uris: List[Uri]) -> List[TrackFull]:
uri_strings: List[str] = None) -> List[TrackFull]:
if uris is None and uri_strings is None:
raise NameError('no uris provided')
if uri_strings is not None:
uris = [Uri(i) for i in uri_strings]
logger.info(f'getting {len(uris)} tracks') logger.info(f'getting {len(uris)} tracks')
@ -795,31 +771,21 @@ class Network:
for chunk in chunked_uris: for chunk in chunked_uris:
resp = self.get_request(url='tracks', ids=','.join([i.object_id for i in chunk])) resp = self.get_request(url='tracks', ids=','.join([i.object_id for i in chunk]))
if resp: if resp:
tracks += [TrackFull(**i) for i in resp.get('tracks', [])] tracks += [init_with_key_filter(TrackFull, i) for i in resp.get('tracks', [])]
return tracks return tracks
def get_track(self, uri: Uri = None, uri_string: str = None) -> Optional[TrackFull]: @inject_uri(uris=False)
def get_track(self, uri) -> Optional[TrackFull]:
if uri is None and uri_string is None: track = self.get_tracks(uris=[uri])
raise NameError('no uri provided')
if uri_string is not None:
uri = Uri(uri_string)
track = self.get_tracks([uri])
if len(track) == 1: if len(track) == 1:
return track[0] return track[0]
else: else:
return None return None
def get_albums(self, uris: List[Uri] = None, uri_strings: List[str] = None) -> List[AlbumFull]: @inject_uri(uri=False)
def get_albums(self, uris: List[Uri]) -> List[AlbumFull]:
if uris is None and uri_strings is None:
raise NameError('no uris provided')
if uri_strings is not None:
uris = [Uri(i) for i in uri_strings]
logger.info(f'getting {len(uris)} albums') logger.info(f'getting {len(uris)} albums')
@ -831,31 +797,21 @@ class Network:
for chunk in chunked_uris: for chunk in chunked_uris:
resp = self.get_request(url='albums', ids=','.join([i.object_id for i in chunk])) resp = self.get_request(url='albums', ids=','.join([i.object_id for i in chunk]))
if resp: if resp:
albums += [AlbumFull(**i) for i in resp.get('albums', [])] albums += [init_with_key_filter(AlbumFull, i) for i in resp.get('albums', [])]
return albums return albums
def get_album(self, uri: Uri = None, uri_string: str = None) -> Optional[AlbumFull]: @inject_uri(uris=False)
def get_album(self, uri: Uri) -> Optional[AlbumFull]:
if uri is None and uri_string is None: album = self.get_albums(uris=[uri])
raise NameError('no uri provided')
if uri_string is not None:
uri = Uri(uri_string)
album = self.get_albums([uri])
if len(album) == 1: if len(album) == 1:
return album[0] return album[0]
else: else:
return None return None
def get_artists(self, uris: List[Uri] = None, uri_strings: List[str] = None) -> List[ArtistFull]: @inject_uri(uri=False)
def get_artists(self, uris) -> List[ArtistFull]:
if uris is None and uri_strings is None:
raise NameError('no uris provided')
if uri_strings is not None:
uris = [Uri(i) for i in uri_strings]
logger.info(f'getting {len(uris)} artists') logger.info(f'getting {len(uris)} artists')
@ -867,19 +823,14 @@ class Network:
for chunk in chunked_uris: for chunk in chunked_uris:
resp = self.get_request(url='artists', ids=','.join([i.object_id for i in chunk])) resp = self.get_request(url='artists', ids=','.join([i.object_id for i in chunk]))
if resp: if resp:
artists += [ArtistFull(**i) for i in resp.get('artists', [])] artists += [init_with_key_filter(ArtistFull, i) for i in resp.get('artists', [])]
return artists return artists
def get_artist(self, uri: Uri = None, uri_string: str = None) -> Optional[ArtistFull]: @inject_uri(uris=False)
def get_artist(self, uri) -> Optional[ArtistFull]:
if uri is None and uri_string is None: artist = self.get_artists(uris=[uri])
raise NameError('no uri provided')
if uri_string is not None:
uri = Uri(uri_string)
artist = self.get_artists([uri])
if len(artist) == 1: if len(artist) == 1:
return artist[0] return artist[0]
else: else:
@ -914,10 +865,10 @@ class Network:
type=','.join([i.name for i in query_types]), type=','.join([i.name for i in query_types]),
limit=response_limit) limit=response_limit)
albums = [SimplifiedAlbum(**i) for i in resp.get('albums', {}).get('items', [])] albums = [init_with_key_filter(SimplifiedAlbum, i) for i in resp.get('albums', {}).get('items', [])]
artists = [ArtistFull(**i) for i in resp.get('artists', {}).get('items', [])] artists = [init_with_key_filter(ArtistFull, i) for i in resp.get('artists', {}).get('items', [])]
tracks = [TrackFull(**i) for i in resp.get('tracks', {}).get('items', [])] tracks = [init_with_key_filter(TrackFull, i) for i in resp.get('tracks', {}).get('items', [])]
playlists = [SimplifiedPlaylist(**i) for i in resp.get('playlists', {}).get('items', [])] playlists = [init_with_key_filter(SimplifiedPlaylist, i) for i in resp.get('playlists', {}).get('items', [])]
return SearchResponse(tracks=tracks, albums=albums, artists=artists, playlists=playlists) return SearchResponse(tracks=tracks, albums=albums, artists=artists, playlists=playlists)
@ -996,7 +947,7 @@ class PageCollection:
self.iterate(page.next) self.iterate(page.next)
def add_page(self, page_dict): def add_page(self, page_dict):
page = Page(**page_dict) page = init_with_key_filter(Page, page_dict)
self.pages.append(page) self.pages.append(page)
return page return page

View File

@ -15,3 +15,13 @@ def validate_uri_string(uri_string: str):
return uri return uri
except ValueError: except ValueError:
return False return False
def get_uri(uri_in):
if isinstance(uri_in, str):
return Uri(input_string=uri_in)
if isinstance(uri_in, Uri):
return uri_in
raise TypeError(f'invalid uri type provided - {type(uri_in)}')

View File

@ -1,5 +1,8 @@
import functools import functools
import logging import logging
from spotframework.util import get_uri
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -14,3 +17,30 @@ def debug(func):
print(f"{func.__name__!r} -> {value!r}") print(f"{func.__name__!r} -> {value!r}")
return value return value
return wrapper_debug return wrapper_debug
def inject_uri(_func=None, *, uri=True, uris=True, uri_optional=False, uris_optional=False):
def decorator_inject_uri(func):
@functools.wraps(func)
def inject_uri_wrapper(*args, **kwargs):
if uri:
if uri_optional:
kwargs['uri'] = get_uri(kwargs['uri']) if kwargs.get('uri') else None
else:
kwargs['uri'] = get_uri(kwargs['uri'])
if uris:
if uris_optional:
kwargs['uris'] = [get_uri(i) for i in kwargs['uris']] if kwargs.get('uris') else None
else:
kwargs['uris'] = [get_uri(i) for i in kwargs['uris']]
return func(*args, **kwargs)
return inject_uri_wrapper
if _func is None:
return decorator_inject_uri
else:
return decorator_inject_uri(_func)