diff --git a/.gitignore b/.gitignore index 427088f..f01ca9a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ __pycache__ *.csv .idea .spot +data scratch.py \ No newline at end of file diff --git a/alarm.py b/alarm.py index 2b1067a..333bc50 100644 --- a/alarm.py +++ b/alarm.py @@ -42,9 +42,9 @@ if __name__ == '__main__': try: - network = Network(NetworkUser(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTREFRESH'])) + network = Network(NetworkUser(os.environ['SPOT_CLIENT'], + os.environ['SPOT_SECRET'], + os.environ['SPOT_REFRESH'])) network.user.refresh_access_token() found = False diff --git a/backup.py b/backup.py index 3f8e7ee..abff38c 100644 --- a/backup.py +++ b/backup.py @@ -29,9 +29,9 @@ if __name__ == '__main__': # try: - network = Network(NetworkUser(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTREFRESH'])) + network = Network(NetworkUser(os.environ['SPOT_CLIENT'], + os.environ['SPOT_SECRET'], + os.environ['SPOT_REFRESH'])) network.user.refresh_access_token() playlists = network.get_user_playlists() diff --git a/generate_playlists.py b/generate_playlists.py index faa1308..c02c37b 100644 --- a/generate_playlists.py +++ b/generate_playlists.py @@ -122,9 +122,9 @@ def go(): logger.critical('none to execute, terminating') return - net = Network(NetworkUser(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTREFRESH'])) + net = Network(NetworkUser(os.environ['SPOT_CLIENT'], + os.environ['SPOT_SECRET'], + os.environ['SPOT_REFRESH'])) net.user.refresh_access_token() engine = PlaylistEngine(net) diff --git a/getaccesstoken.py b/getaccesstoken.py index 914547b..d025462 100644 --- a/getaccesstoken.py +++ b/getaccesstoken.py @@ -15,9 +15,9 @@ logger.addHandler(stream_handler) if __name__ == '__main__': - network = Network(NetworkUser(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTREFRESH'])) + network = Network(NetworkUser(os.environ['SPOT_CLIENT'], + os.environ['SPOT_SECRET'], + os.environ['SPOT_REFRESH'])) network.user.refresh_access_token() print(network.user.access_token) diff --git a/listener.py b/listener.py new file mode 100644 index 0000000..6a94592 --- /dev/null +++ b/listener.py @@ -0,0 +1,45 @@ +import os +import logging + +import click +from spotframework.listener.cmd import ListenCmd +from spotframework.net.network import Network, NetworkUser + +logger = logging.getLogger('spotframework') + +log_format = '%(asctime)s %(levelname)s %(name)s:%(funcName)s - %(message)s' + +file_handler = logging.FileHandler("data/listener.log") +formatter = logging.Formatter(log_format) +file_handler.setFormatter(formatter) + +stream_log_format = '%(levelname)s %(name)s:%(funcName)s - %(message)s' +stream_formatter = logging.Formatter(stream_log_format) + +stream_handler = logging.StreamHandler() +stream_handler.setFormatter(stream_formatter) + +logger.addHandler(stream_handler) +logger.addHandler(file_handler) + + +@click.command() +@click.option('-v', '--verbose', is_flag=True) +@click.option('--client-id', prompt=True, default=lambda: os.environ.get('SPOT_CLIENT', '')) +@click.option('--client-secret', prompt=True, default=lambda: os.environ.get('SPOT_SECRET', '')) +@click.option('--refresh-token', prompt=True, default=lambda: os.environ.get('SPOT_REFRESH', '')) +def listen(verbose, client_id, client_secret, refresh_token): + if verbose: + stream_handler.setLevel('DEBUG') + else: + stream_handler.setLevel('WARNING') + + net = Network(NetworkUser(client_id=client_id, + client_secret=client_secret, + refresh_token=refresh_token).refresh_access_token()) + cmd = ListenCmd(net, stream_handler) + cmd.cmdloop() + + +if __name__ == '__main__': + listen() diff --git a/requirements.txt b/requirements.txt index 697d2ac..4e5b19b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -certifi==2019.6.16 +certifi==2019.11.28 chardet==3.0.4 +Click==7.0 idna==2.8 requests==2.22.0 -tabulate==0.8.3 -urllib3==1.25.3 +tabulate==0.8.6 +urllib3==1.25.7 diff --git a/sort_playlist.py b/sort_playlist.py index 12e8acb..e7e7b47 100644 --- a/sort_playlist.py +++ b/sort_playlist.py @@ -28,9 +28,9 @@ logger.addHandler(stream_handler) def go(playlist_name): - net = Network(NetworkUser(os.environ['SPOTCLIENT'], - os.environ['SPOTSECRET'], - os.environ['SPOTREFRESH'])) + net = Network(NetworkUser(os.environ['SPOT_CLIENT'], + os.environ['SPOT_SECRET'], + os.environ['SPOT_REFRESH'])) net.user.refresh_access_token() engine = PlaylistEngine(net) diff --git a/spotframework/listener/__init__.py b/spotframework/listener/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spotframework/listener/cmd.py b/spotframework/listener/cmd.py new file mode 100644 index 0000000..3964145 --- /dev/null +++ b/spotframework/listener/cmd.py @@ -0,0 +1,153 @@ +from cmd import Cmd +import functools +import logging +from typing import Optional + +from spotframework.net.network import Network +from spotframework.listener.thread import ListenerThread + +logger = logging.getLogger(__name__) + + +def check_thread(func): + @functools.wraps(func) + def check_thread_wrapper(self, *args, **kwargs): + if self.listen_thread is not None: + return func(self, *args, **kwargs) + else: + print('>> not running') + + return check_thread_wrapper + + +def check_last_dataset(func): + @functools.wraps(func) + def check_last_dataset_wrapper(self, *args, **kwargs): + if self.last_dataset is not None: + return func(self, *args, **kwargs) + else: + print('>> not running') + + return check_last_dataset_wrapper + + +class ListenCmd(Cmd): + intro = 'listener... ? for help' + prompt = '(listen)> ' + + dt_format = "%-H:%M:%S %-d %b %-y" + + def __init__(self, net: Network, log_stream_handler=None): + Cmd.__init__(self) + self.net = net + self.listen_thread: Optional[ListenerThread] = None + self.last_dataset = None + + self.verbose = False + self.log_stream_handler = log_stream_handler + + def start_listener(self): + if self.listen_thread is not None: + logger.info('restarting') + print('>> restarting listener') + self.stop_listener() + + logger.info('starting') + print('>> starting listener') + self.listen_thread = ListenerThread(self.net) + self.listen_thread.on_playback_change.append(lambda x: print(f'playback changed -> {x}')) + self.listen_thread.start() + + @check_thread + def stop_listener(self): + logger.info('stopping') + print('>> stopping listener') + self.last_dataset = self.listen_thread.recent_tracks + self.listen_thread.stop() + + def print_tracks(self, tracks): + [print(f'({i.played_at.strftime(self.dt_format)}) {i.name} / {i.artists_names}') + for i in tracks] + + def print(self): + if self.listen_thread is not None: + print('now playing:', self.listen_thread.now_playing) + self.print_tracks(self.listen_thread.recent_tracks) + elif self.last_dataset is not None: + self.print_tracks(self.last_dataset) + else: + print('>> no tracks to print') + + @check_thread + def set_poll_interval(self, value): + if value.isdigit(): + logger.info(f'setting polling interval to {value}') + print(f'>> interval set to {value}') + self.listen_thread.sleep_interval = int(value) + else: + logger.error(f'{value} is not a valid interval') + print('>> not a number') + + def do_interval(self, args): + """set polling interval""" + self.set_poll_interval(args) + + def do_print(self, args): + """print recently played tracks""" + self.print() + + def do_p(self, args): + """print recently played tracks""" + self.print() + + def do_listen(self, args): + """start listener""" + self.start_listener() + + def do_l(self, args): + """start listener""" + self.start_listener() + + def do_stop(self, args): + """stop listener""" + self.stop_listener() + + def do_e(self, args): + """stop listener""" + self.stop_listener() + + def do_verbose(self, args): + """toggle verbosity""" + if self.log_stream_handler is not None: + self.verbose = not self.verbose + if self.verbose: + self.log_stream_handler.setLevel('DEBUG') + else: + self.log_stream_handler.setLevel('WARNING') + + @check_thread + def do_quit(self, args): + """stop and quit""" + logger.info('quitting') + self.stop_listener() + self.listen_thread.join() + exit(0) + + @check_thread + def do_q(self, args): + """stop and quit""" + logger.info('quitting') + self.stop_listener() + self.listen_thread.join() + exit(0) + + @check_thread + def do_status(self, args): + """stop and quit""" + if self.listen_thread is not None: + if self.listen_thread.is_alive(): + print('>> running') + else: + print('>> thread dead') + else: + print('>> not running') diff --git a/spotframework/listener/thread.py b/spotframework/listener/thread.py new file mode 100644 index 0000000..9def839 --- /dev/null +++ b/spotframework/listener/thread.py @@ -0,0 +1,65 @@ +import threading +import logging +from typing import Optional + +from spotframework.net.network import Network +from spotframework.model.service import CurrentlyPlaying + +logger = logging.getLogger(__name__) + + +class ListenerThread(threading.Thread): + + def __init__(self, + net: Network, + sleep_interval: int = 5, + request_size: int = 5): + super(ListenerThread, self).__init__() + self._stop_event = threading.Event() + + self.net = net + + self.sleep_interval = sleep_interval + self.request_size = request_size + + self.recent_tracks = [] + self.prev_now_playing: Optional[CurrentlyPlaying] = None + self.now_playing: Optional[CurrentlyPlaying] = net.get_player() + + self.on_playback_change = [] + + def stop(self): + logger.info('stopping thread') + self._stop_event.set() + + def stopped(self): + return self._stop_event.is_set() + + def update_now_playing(self): + live_now_playing = self.net.get_player() + + if self.now_playing is None and live_now_playing is None: + return + + if live_now_playing != self.now_playing: + self.prev_now_playing = self.now_playing + self.now_playing = live_now_playing + for func in self.on_playback_change: + func(live_now_playing) + else: + self.now_playing = live_now_playing + + def update_recent_tracks(self): + logger.debug('pulling tracks') + tracks = self.net.get_recently_played_tracks(response_limit=self.request_size) + for track in tracks: + if track.played_at not in [i.played_at for i in self.recent_tracks]: + self.recent_tracks.append(track) + + self.recent_tracks.sort(key=lambda x: x.played_at) + + def run(self): + while not self.stopped(): + self.update_recent_tracks() + self.update_now_playing() + self._stop_event.wait(self.sleep_interval) diff --git a/spotframework/model/service.py b/spotframework/model/service.py index d46e5d2..4343495 100644 --- a/spotframework/model/service.py +++ b/spotframework/model/service.py @@ -25,7 +25,7 @@ class Context: return f'Context: {self.object_type} uri({self.uri})' def __str__(self): - return f'{self.object_type} / {self.uri}' + return str(self.uri) class Device: @@ -110,4 +110,4 @@ class CurrentlyPlaying: else: playing = '(paused)' - return f'{playing} {self.track} on {self.device} ({self._format_duration(self.progress_ms)})' + return f'{playing} {self.track} on {self.device} from {self.context} ({self._format_duration(self.progress_ms)})'