diff --git a/handler.py b/handler.py index b05c77a..5e10d7f 100644 --- a/handler.py +++ b/handler.py @@ -30,10 +30,11 @@ def _(s): factory = ServerFactory(TransportFactory()) -def get_server(): +def get_server(deviceId): return factory.create(user=LMS_SETTINGS.username, password=LMS_SETTINGS.password, cur_player_id=LMS_SETTINGS.default_player, + deviceId=deviceId, debug=LMS_SETTINGS.debug) @@ -41,8 +42,20 @@ def lambda_handler(event, context, server=None): """ Route the incoming request based on type (LaunchRequest, IntentRequest, etc.) The JSON body of the request is provided in the event parameter. """ + deviceId = '' + echomaps = {} try: - sqa = SqueezeAlexa(server=server or get_server(), + if event: + deviceId = event['context']['System']['device']['deviceId'] + server = server or get_server(deviceId) + echomaps = server.get_echomaps() + except KeyError as e: + if not SKILL_SETTINGS.use_spoken_errors: + raise e + if deviceId in echomaps: + server.cur_player_id = echomaps[deviceId]['name'] + try: + sqa = SqueezeAlexa(server=server, app_id=SKILL_SETTINGS.application_id) return sqa.handle(event, context) except Exception as e: diff --git a/metadata/intents/v1/locale/en_GB/intents.json b/metadata/intents/v1/locale/en_GB/intents.json index 3254ebf..1fd7a1e 100644 --- a/metadata/intents/v1/locale/en_GB/intents.json +++ b/metadata/intents/v1/locale/en_GB/intents.json @@ -143,6 +143,18 @@ "what we are listening to in the {Player}" ] }, + { + "name": "PlayIntent", + "slots": [ + { + "name": "Player", + "type": "PLAYER" + } + ], + "samples": [ + "Play on {Player}" + ] + }, { "name": "SelectPlayerIntent", "slots": [ @@ -174,7 +186,10 @@ "turn {Player} player on", "switch {Player} on", "switch on {Player}", - "switch {Player} player on" + "switch {Player} player on", + "power {Player} on", + "power on {Player}", + "power {Player} player on" ] }, { @@ -191,7 +206,10 @@ "turn {Player} player off", "switch {Player} off", "switch off {Player}", - "switch {Player} player off" + "switch {Player} player off", + "power {Player} off", + "power off {Player}", + "power {Player} player off" ] }, { @@ -215,12 +233,12 @@ } ], "samples": [ - "play some {Genre}", - "play some {Genre} and {SecondaryGenre}", - "play some {Genre} and some {SecondaryGenre}", - "play some {Genre} some {SecondaryGenre} and some {TertiaryGenre}", - "play a mix of {Genre} and {SecondaryGenre}", - "play a mix of {Genre} {SecondaryGenre} and {TertiaryGenre}" + "play some {Genre} on {Player}", + "play some {Genre} and {SecondaryGenre} on {Player}", + "play some {Genre} and some {SecondaryGenre} on {Player}", + "play some {Genre} some {SecondaryGenre} and some {TertiaryGenre} on {Player}", + "play a mix of {Genre} and {SecondaryGenre} on {Player}", + "play a mix of {Genre} {SecondaryGenre} and {TertiaryGenre} on {Player}" ] }, { @@ -241,6 +259,51 @@ "play my {Playlist} playlist in {Player}" ] }, + { + "name": "SetEchoMapIntent", + "slots": [ + { + "name": "Player", + "type": "PLAYER" + } + ], + "samples": [ + "Set the default player to {Player}", + "I'm using {Player}", + "I'm in {Player}", + "Set the player in this room to {Player}", + "I am using {Player}", + "I am in {Player}" + ] + }, + { + "name": "DelEchoMapPlayerIntent", + "slots": [ + { + "name": "Player", + "type": "PLAYER" + } + ], + "samples": [ + "Delete the default echo device for player {Player}", + "Delete the default echo device for {Player}", + "Delete the default echo for player {Player}", + "Delete the default echo for {Player}", + "Delete the default association for player {Player}", + "Delete the default association for {Player}" + ] + }, + { + "name": "DelEchoMapDeviceIntent", + "slots": [], + "samples": [ + "Delete the default player here", + "Delete the default player for this room", + "Delete the default player for this echo device", + "Delete the default player for this echo", + "Delete the default player" + ] + }, { "name": "AllOnIntent", "samples": [ diff --git a/squeezealexa/alexa/intents.py b/squeezealexa/alexa/intents.py index 81b50bd..ffbd699 100644 --- a/squeezealexa/alexa/intents.py +++ b/squeezealexa/alexa/intents.py @@ -54,3 +54,6 @@ class Custom(object): for s in ["SetVolume", "SetVolumePercent"]) NOW_PLAYING, SELECT_PLAYER = ("%sIntent" % s for s in ["NowPlaying", "SelectPlayer"]) + (SET_ECHOMAP, DEL_ECHOMAP_PLAYER, + DEL_ECHOMAP_DEVICE) = ("%sIntent" % s for s in ["SetEchoMap", + "DelEchoMapPlayer", "DelEchoMapDevice"]) diff --git a/squeezealexa/main.py b/squeezealexa/main.py index 9163659..17b1889 100644 --- a/squeezealexa/main.py +++ b/squeezealexa/main.py @@ -212,6 +212,68 @@ def on_select_player(self, intent, session, pid=None): return speech_response(title, speech, reprompt_text=reprompt, end=False) + @handler.handle(Custom.SET_ECHOMAP) + def on_set_echomap(self, intent, session, pid=None): + srv = self._server + srv.set_echomap(pid, srv.deviceId) + if pid: + player = srv.players[pid] + return self.smart_response( + text=(_("Set the default player for current Echo " + "to {player}").format(player=player.name)), + speech=(_("I have set the default player for " + "current Echo to {player}") + .format(player=player.name))) + speech = (_("I only found these players: {players}. " + "Could you try again?") + .format(players=human_join(srv.player_names))) + reprompt = (_("You can select a player by saying \"{utterance}\" " + "and then the player name.") + .format(utterance=Utterances.SELECT_PLAYER)) + try: + player = intent['slots']['Player']['value'] + title = (_("No player called \"{name}\"").format(name=player)) + except KeyError: + title = "Didn't recognise a player name" + return speech_response(title, speech, reprompt_text=reprompt, + end=False) + + @handler.handle(Custom.DEL_ECHOMAP_PLAYER) + def on_del_echomap_player(self, intent, session, pid=None): + srv = self._server + srv.del_echomap(pid) + if pid: + player = srv.players[pid] + text = (_("Removed the default associations for player {player}") + .format(player=player.name)) + speech = (_("I have removed the default associations " + "for player {player}") + .format(player=player.name)) + return self.smart_response(text, speech) + speech = (_("I only found these players: {players}. " + "Could you try again?") + .format(players=human_join(srv.player_names))) + reprompt = (_("You can select a player by saying \"{utterance}\" " + "and then the player name.") + .format(utterance=Utterances.SELECT_PLAYER)) + try: + player = intent['slots']['Player']['value'] + title = (_("No player called \"{name}\"").format(name=player)) + except KeyError: + title = "Didn't recognise a player name" + return speech_response(title, speech, reprompt_text=reprompt, + end=False) + + @handler.handle(Custom.DEL_ECHOMAP_DEVICE) + def on_del_echomap_device(self, intent, session, pid=None): + srv = self._server + srv.del_echomap(False, srv.deviceId) + return self.smart_response( + text=_("Removed the default player for " + "the current Echo device"), + speech=_("I have removed the default player for " + "the current Echo device")) + @handler.handle(Audio.SHUFFLE_ON) @handler.handle(CustomAudio.SHUFFLE_ON) def on_shuffle_on(self, intent, session, pid=None): diff --git a/squeezealexa/squeezebox/server.py b/squeezealexa/squeezebox/server.py index 8090d43..00c2693 100644 --- a/squeezealexa/squeezebox/server.py +++ b/squeezealexa/squeezebox/server.py @@ -93,12 +93,13 @@ class Server(object): _MAX_FAILURES = 3 def __init__(self, transport, user=None, password=None, - cur_player_id=None, debug=False): + cur_player_id=None, deviceId=None, debug=False): self.transport = transport self._debug = debug self.user = user self.password = password + self.deviceId = deviceId if user and password: self.log_in() print_d("Authenticated with %s!" % self) @@ -121,6 +122,7 @@ def __init__(self, transport, user=None, password=None, self.__genres = [] self.__playlists = [] self.__favorites = [] + self.__echomaps = {} @property def connected(self): @@ -388,6 +390,85 @@ def __str__(self): def __del__(self): self.disconnect() + def get_squeezealexa_favorite_id(self): + resp = self.__a_request("favorites items 0 255", + raw=True) + favorites = {d['name']: d + for d in self._groups(resp, 'id') + if d['hasitems']} + if 'squeeze-alexa' not in favorites: + print_d("Creating squeeze-alexa container in Favorites...") + resp = self.__a_request("favorites addlevel title:squeeze-alexa", + raw=True) + resp = self.__a_request("favorites items 0 255", + raw=True) + favorites = {d['name']: d + for d in self._groups(resp, 'id') + if d['hasitems']} + if 'squeeze-alexa' in favorites: + id = favorites['squeeze-alexa']['id'] + print_d("Found squeeze-alexa favorite id={id}", id=id) + return id + else: + return False + + def get_echomaps(self): + """ Updates the list of the Squeezebox players available and other + server metadata.""" + id = self.get_squeezealexa_favorite_id() + resp = self.__a_request("favorites items 0 255 item_id:%s want_url:1" % + id, raw=True) + self.__echomaps = {d['url']: d for d in self._groups(resp, 'id') + if d['isaudio']} + print_d(with_example("Loaded {num} Echo Maps", self.__echomaps)) + return self.__echomaps + + def set_echomap(self, player, deviceId): + self.__echomaps = self.get_echomaps() + id = self.get_squeezealexa_favorite_id() + if deviceId not in self.__echomaps: + print_d("Setting new Echo Map from {player} to {deviceId}", + player=player, deviceId=deviceId) + self.__a_request("favorites add item_id:%s.0 title:%s" + "url:%s" % (id, player, deviceId), + raw=True) + elif self.__echomaps[deviceId]['name'] != player: + print_d("Deleting outdated Echo Map with {id}", + id=self.__echomaps[deviceId]['id']) + self.__a_request("favorites delete item_id:%s" % + self.__echomaps[deviceId]['id'], + raw=True) + print_d("Setting new Echo Map from {player} to {deviceId}", + player=player, deviceId=deviceId) + self.__a_request("favorites add item_id:%s.0 title:%s " + "url:%s" % (id, player, deviceId), + raw=True) + + def del_echomap(self, player=False, deviceId=False): + self.__echomaps = self.get_echomaps() + if deviceId: + print_d("Trying to remove default player for {deviceId}", + deviceId=deviceId) + while deviceId in self.__echomaps: + print_d("Deleting Echo Map with ID {id}", + id=self.__echomaps[deviceId]['id']) + self.__a_request("favorites delete item_id:%s" % + self.__echomaps[deviceId]['id'], + raw=True) + self.__echomaps = self.get_echomaps() + if player: + inverted_echomaps = {v['name']: {'deviceId': k, 'id': v['id']} + for k, v in self.__echomaps.items()} + while player in inverted_echomaps: + print_d("Deleting Echo Map with ID {id}", + id=inverted_echomaps[player]['id']) + self.__a_request("favorites delete item_id:%s" % + inverted_echomaps[player]['id'], + raw=True) + self.__echomaps = self.get_echomaps() + inverted_echomaps = {v['name']: {'deviceId': k, 'id': v['id']} + for k, v in self.__echomaps.items()} + def people_from(details: Dict, default=None) -> Union[str, None]: genres = {g.lower() for g in details.get('genre', [])}