diff --git a/alarm.py b/alarm.py index f737bad..b0d9b48 100644 --- a/alarm.py +++ b/alarm.py @@ -43,7 +43,6 @@ if __name__ == '__main__': 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 490d8ff..6fd69cd 100644 --- a/backup.py +++ b/backup.py @@ -31,7 +31,6 @@ if __name__ == '__main__': network = Network(NetworkUser(os.environ['SPOTCLIENT'], os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], os.environ['SPOTREFRESH'])) playlists = network.get_user_playlists() diff --git a/generate_playlists.py b/generate_playlists.py index 760ee5a..e0d4beb 100644 --- a/generate_playlists.py +++ b/generate_playlists.py @@ -124,7 +124,6 @@ def go(): net = Network(NetworkUser(os.environ['SPOTCLIENT'], os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], os.environ['SPOTREFRESH'])) engine = PlaylistEngine(net) diff --git a/getaccesstoken.py b/getaccesstoken.py index ff37caa..b46159b 100644 --- a/getaccesstoken.py +++ b/getaccesstoken.py @@ -17,7 +17,6 @@ if __name__ == '__main__': network = Network(NetworkUser(os.environ['SPOTCLIENT'], os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], os.environ['SPOTREFRESH'])) print(network.user.access_token) diff --git a/sort_playlist.py b/sort_playlist.py index bc8338b..29410c0 100644 --- a/sort_playlist.py +++ b/sort_playlist.py @@ -30,7 +30,6 @@ def go(playlist_name): net = Network(NetworkUser(os.environ['SPOTCLIENT'], os.environ['SPOTSECRET'], - os.environ['SPOTACCESS'], os.environ['SPOTREFRESH'])) engine = PlaylistEngine(net) diff --git a/spotframework/engine/processor/abstract.py b/spotframework/engine/processor/abstract.py index 820192f..1f43431 100644 --- a/spotframework/engine/processor/abstract.py +++ b/spotframework/engine/processor/abstract.py @@ -26,13 +26,7 @@ class BatchSingleProcessor(AbstractProcessor, ABC): return track def process_batch(self, tracks: List[Track]) -> List[Track]: - processed = [] - - for track in tracks: - processed_track = self.process_single(track) - processed.append(processed_track) - - return processed + return [self.process_single(track) for track in tracks] def process(self, tracks: List[Track]) -> List[Track]: return [i for i in self.process_batch(tracks) if i is not None] diff --git a/spotframework/model/playlist.py b/spotframework/model/playlist.py index 34676e6..49a5425 100644 --- a/spotframework/model/playlist.py +++ b/spotframework/model/playlist.py @@ -26,6 +26,12 @@ class Playlist: def __len__(self): return len(self.tracks) + def __getitem__(self, item) -> Track: + return self.tracks[item] + + def __iter__(self): + return iter(self.tracks) + @property def tracks(self) -> List[Track]: return self._tracks diff --git a/spotframework/model/service.py b/spotframework/model/service.py new file mode 100644 index 0000000..71abe2d --- /dev/null +++ b/spotframework/model/service.py @@ -0,0 +1,91 @@ +from datetime import datetime +from spotframework.model.track import Track +from enum import Enum + + +class Context: + def __init__(self, + uri: str, + object_type: str = None, + href: str = None, + external_spot: str = None): + self.uri = uri + self.object_type = object_type + self.href = href + self.external_spot = external_spot + + def __repr__(self): + return f'Context: {self.object_type} uri({self.uri})' + + def __str__(self): + return f'{self.object_type} / {self.uri}' + + +class Device: + + class DeviceType(Enum): + COMPUTER = 1 + TABLET = 2 + SMARTPHONE = 3 + SPEAKER = 4 + TV = 5 + AVR = 6 + STB = 7 + AUDIODONGLE = 8 + GAMECONSOLE = 9 + CASTVIDEO = 10 + CASTAUDIO = 11 + AUTOMOBILE = 12 + UNKNOWN = 13 + + def __init__(self, + device_id: str, + is_active: bool, + is_private_session: bool, + is_restricted: bool, + name: str, + object_type: DeviceType, + volume: int): + self.device_id = device_id + self.is_active = is_active + self.is_private_session = is_private_session + self.is_restricted = is_restricted + self.name = name + self.object_type = object_type + self.volume = volume + + def __repr__(self): + return f'Device: {self.name} active({self.is_active}) type({self.object_type}) vol({self.volume})' + + def __str__(self): + return self.name + + +class CurrentlyPlaying: + def __init__(self, + context: Context, + timestamp: datetime, + progress_ms: int, + is_playing: bool, + track: Track, + device: Device, + shuffle: bool, + repeat: bool, + currently_playing_type: str): + self.context = context + self.timestamp = timestamp + self.progress_ms = progress_ms + self.is_playing = is_playing + self.track = track + self.device = device + self.shuffle = shuffle + self.repeat = repeat + self.currently_playing_type = currently_playing_type + + def __repr__(self): + return f'CurrentlyPlaying: is_playing({self.is_playing}) progress({self.progress_ms}) ' \ + f'context({self.context}) track({self.track}) device({self.device}) shuffle({self.shuffle}) ' \ + f'repeat({self.repeat}) time({self.timestamp})' + + def __str__(self): + return f'playing: {self.is_playing} - {self.track} on {self.device}' diff --git a/spotframework/net/network.py b/spotframework/net/network.py index 0d2bb36..c05532f 100644 --- a/spotframework/net/network.py +++ b/spotframework/net/network.py @@ -8,6 +8,7 @@ from spotframework.net.parse import parse from spotframework.net.user import NetworkUser from spotframework.model.playlist import SpotifyPlaylist from spotframework.model.track import Track, PlaylistTrack +from spotframework.model.service import CurrentlyPlaying, Device from requests.models import Response limit = 50 @@ -28,7 +29,11 @@ class Network: if 200 <= req.status_code < 300: logger.debug(f'{method} get {req.status_code}') - return req.json() + + if req.status_code != 204: + return req.json() + else: + return None else: if req.status_code == 429: @@ -215,37 +220,55 @@ class Network: return tracks - def get_available_devices(self) -> Optional[dict]: + def get_available_devices(self) -> Optional[List[Device]]: logger.info("retrieving") resp = self._make_get_request('getAvailableDevices', 'me/player/devices') if resp: - return resp + return [parse.parse_device(i) for i in resp['devices']] else: logger.error('no devices returned') return None - def get_player(self) -> Optional[dict]: + def get_player(self) -> Optional[CurrentlyPlaying]: logger.info("retrieved") resp = self._make_get_request('getPlayer', 'me/player') if resp: - return resp + return parse.parse_currently_playing(resp) else: - logger.error('no player returned') + logger.info('no player returned') return None def get_device_id(self, devicename) -> Optional[str]: logger.info(f"{devicename}") - resp = self.get_available_devices() - if resp: - return next((i for i in resp['devices'] if i['name'] == devicename), None)['id'] + devices = self.get_available_devices() + if devices: + device = next((i for i in devices if i.name == devicename), None) + if device: + return device.device_id + else: + logger.error(f'{devicename} not found') else: logger.error('no devices returned') + + def change_playback_device(self, device_id): + + logger.info(device_id) + + json = { + 'device_ids': [device_id], + 'play': True + } + + resp = self._make_put_request('changePlaybackDevice', 'me/player', json=json) + if resp: + return True + else: return None def play(self, uri=None, uris=None, deviceid=None) -> Optional[Response]: @@ -260,14 +283,12 @@ class Network: if uri and uris: raise Exception('wont take both context uri and uris') + payload = dict() + if uri: - payload = {'context_uri': uri} - + payload['context_uri'] = uri if uris: - payload = {'uris': uris[:200]} - - if not uri and not uris: - raise Exception('need either context uri or uris') + payload['uris'] = uris[:200] req = self._make_put_request('play', 'me/player/play', params=params, json=payload) if req: @@ -305,6 +326,21 @@ class Network: else: logger.error('error skipping') + def previous(self, deviceid=None) -> Optional[Response]: + + logger.info(f"{deviceid if deviceid is not None else ''}") + + if deviceid is not None: + params = {'device_id': deviceid} + else: + params = None + + req = self._make_post_request('previous', 'me/player/previous', params=params) + if req: + return req + else: + logger.error('error reversing') + def set_shuffle(self, state, deviceid=None) -> Optional[Response]: logger.info(f"{state}{' ' + deviceid if deviceid is not None else ''}") diff --git a/spotframework/net/parse/parse.py b/spotframework/net/parse/parse.py index b89c353..02aec4f 100644 --- a/spotframework/net/parse/parse.py +++ b/spotframework/net/parse/parse.py @@ -3,6 +3,8 @@ from spotframework.model.album import SpotifyAlbum from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack from spotframework.model.playlist import SpotifyPlaylist from spotframework.model.user import User +from spotframework.model.service import Context, CurrentlyPlaying, Device +import datetime def parse_artist(artist_dict) -> SpotifyArtist: @@ -184,3 +186,33 @@ def parse_playlist(playlist_dict) -> SpotifyPlaylist: collaborative=collaborative, public=public, ext_spotify=ext_spotify) + + +def parse_context(context_dict) -> Context: + return Context(object_type=context_dict['type'], + href=context_dict['href'], + external_spot=context_dict['external_urls']['spotify'], + uri=context_dict['uri']) + + +def parse_currently_playing(play_dict) -> CurrentlyPlaying: + return CurrentlyPlaying(context=parse_context(play_dict['context']) if play_dict['context'] is not None else None, + timestamp=datetime.datetime.fromtimestamp(play_dict['timestamp']/1000), + progress_ms=play_dict['progress_ms'], + is_playing=play_dict['is_playing'], + track=parse_track(play_dict['item']), + device=parse_device(play_dict['device']), + shuffle=play_dict['shuffle_state'], + repeat=play_dict['repeat_state'], + currently_playing_type=play_dict['currently_playing_type']) + + +def parse_device(device_dict) -> Device: + return Device(device_id=device_dict['id'], + is_active=device_dict['is_active'], + is_private_session=device_dict['is_private_session'], + is_restricted=device_dict['is_restricted'], + name=device_dict['name'], + object_type=Device.DeviceType[device_dict['type'].upper()], + volume=device_dict['volume_percent']) + diff --git a/spotframework/net/user.py b/spotframework/net/user.py index df1b28d..c934988 100644 --- a/spotframework/net/user.py +++ b/spotframework/net/user.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) class NetworkUser(User): - def __init__(self, client_id, client_secret, access_token, refresh_token): + def __init__(self, client_id, client_secret, refresh_token, access_token=None): super().__init__('') self.accesstoken = access_token diff --git a/spotframework/player/__init__.py b/spotframework/player/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spotframework/player/player.py b/spotframework/player/player.py new file mode 100644 index 0000000..ab590df --- /dev/null +++ b/spotframework/player/player.py @@ -0,0 +1,97 @@ +from spotframework.net.network import Network +from spotframework.model.track import SpotifyTrack +from spotframework.model.service import Context, Device +from typing import List +import logging +logger = logging.getLogger(__name__) + + +class Player: + + def __init__(self, + net: Network): + self.net = net + self.user = net.user + self.last_status = None + + def __str__(self): + return f'{self.user.username} - {self.status}' + + def __repr__(self): + return f'Player: {self.user} - {self.status}' + + @property + def available_devices(self): + return self.net.get_available_devices() + + @property + def status(self): + new_status = self.net.get_player() + if new_status: + self.last_status = new_status + return self.last_status + + def play(self, + context: Context = None, + tracks: List[SpotifyTrack] = None, + device: Device = None): + if context and tracks: + raise Exception('cant execute context and track list') + if context: + if device: + self.net.play(uri=context.uri, deviceid=device.device_id) + else: + self.net.play(uri=context.uri) + elif tracks: + if device: + self.net.play(uris=[i.uri for i in tracks], deviceid=device.device_id) + else: + self.net.play(uris=[i.uri for i in tracks]) + else: + self.net.play() + + def change_device(self, device: Device): + self.net.change_playback_device(device.device_id) + + def pause(self): + self.net.pause() + + def toggle_playback(self): + status = self.status + if status: + if status.is_playing: + self.pause() + else: + self.play() + else: + logger.warning('no current playback, playing') + self.play() + + def next(self): + self.net.next() + + def previous(self): + self.net.previous() + + def shuffle(self, state=None): + if state is not None: + if isinstance(state, bool): + self.net.set_shuffle(state) + else: + raise TypeError(f'{state} is not bool') + else: + status = self.status + if status.shuffle: + self.shuffle(state=False) + else: + self.shuffle(state=True) + + def set_volume(self, value: int, device: Device = None): + + if 0 <= int(value) <= 100: + if device: + self.net.set_volume(value, deviceid=device.device_id) + else: + self.net.set_volume(value) + else: + logger.error(f'{value} not between 0 and 100')