def communicate(self, raw: str, wait=True) -> str: eof = False response = '' data = raw.strip() + '\n' num_lines = data.count('\n') try: self._ssl_sock.sendall(data.encode('utf-8')) if not wait: return None while not eof: response += self._ssl_sock.recv().decode('utf-8') eof = response.count("\n") == num_lines or not response return response except socket.error as e: if 'read operation timed out' in str(e): raise Error("Timed out waiting for CLI response. " "Perhaps the tunnel endpoint is incorrect, " "or the LMS CLI is down?") else: print_d("Couldn't communicate with Squeezebox ({error!r})", error=e) self.failures += 1 if self.failures >= self._MAX_FAILURES: self.is_connected = False self._ssl_sock.close() raise Error("Too many Squeezebox failures. Disconnecting") return None
def connected(client, userdata, flags, rc): print_d("Connected to {client}. Subscribing to {topic}", client=self.client, topic=self.resp_topic) result, mid = self.client.subscribe(self.resp_topic, qos=1) if result != MQTT_ERR_SUCCESS: raise Error("Couldn't subscribe to '{topic}'", self.resp_topic)
def playlists(self): if not self.__playlists: resp = self.__a_request("playlists 0 255", raw=True) self.__playlists = [v for k, v in self.__pairs_from(resp) if k == 'playlist'] print_d(with_example("Loaded %d LMS playlists", self.__playlists)) return self.__playlists
def connect_cli(): global telnet telnet = telnetlib.Telnet(host=MQTT_SETTINGS.internal_server_hostname, port=LMS_SETTINGS.cli_port, timeout=5) print_d("Connected to Squeezeserver CLI") return telnet
def genres(self): if not self.__genres: resp = self.__a_request("genres 0 255", raw=True) self.__genres = [v for k, v in self.__pairs_from(resp) if k == 'genre'] print_d(with_example("Loaded %d LMS genres", self.__genres)) return self.__genres
def __init__(self, transport, user=None, password=None, cur_player_id=None, debug=False): self.transport = transport self._debug = debug self.user = user self.password = password if user and password: self.log_in() print_d("Authenticated with %s!" % self) self.players = {} self.refresh_status() players = list(self.players.values()) if not players: raise SqueezeboxException("Uh-oh. No players found.") if not cur_player_id: self.cur_player_id = players[0].id elif cur_player_id not in self.players: print_w("Couldn't find player {id} (found: {all}). " "Check your DEFAULT_PLAYER config.", id=cur_player_id, all=", ".join(list(self.players.keys()))) self.cur_player_id = players[0].id else: self.cur_player_id = cur_player_id print_d("Current player is now: {}", self.players[self.cur_player_id]) self.__genres = [] self.__playlists = [] self.__favorites = []
def __enter__(self): self.socket.settimeout(3) print_d("Creating test TCP server") self.thread = threading.Thread(target=self.serve_forever) print_d("Starting test server") self.thread.start() return self
def stop(self): print_d("Killing {what}.", what=self) self.client.on_message = None self.client.on_subscribe = None # Don't unsubscribe-and-run (disconnect). Causes issues with brokers. # self.client.unsubscribe(self.resp_topic) self.client.disconnect() return super().stop()
def handle(self): try: data = self.request.recv(1024).decode('utf-8') except UnicodeDecodeError: data = "(invalid)" response = response_for(data) print_d("> \"%s\"\n%s" % (data.strip(), response)) self.request.sendall(response.encode('utf-8'))
def favorites(self): if not self.__favorites: resp = self.__a_request("favorites items 0 255 want_url:1", raw=True) self.__favorites = {d['name']: d for d in self._groups(resp, 'name') if d['isaudio']} print_d(with_example("Loaded %d LMS faves", self.__favorites)) return self.__favorites
def __a_request(self, line, raw=False, wait=True): reply = self._request([line], raw=raw, wait=wait) if reply and len(reply): return reply[0] if self.user and self.password: print_d("Command failed. Trying to re-log in.") self.log_in() reply = self._request([line], raw=raw, wait=wait) if reply and len(reply): return reply[0] raise SqueezeboxException("Unprocessable command or login error")
def test_all_handler(self): fake_output = FakeTransport() server = Server(transport=fake_output) alexa = SqueezeAlexa(server=server) for name, func in handler._handlers.items(): print_d(">>> Testing %s() <<<" % func.__name__) session = {'sessionId': None} intent = {'requestId': 'abcd', 'slots': {}} raw = func(alexa, intent, session, None) response = raw['response'] assert 'directives' in response or 'outputSpeech' in response assert 'shouldEndSession' in response
def __new__(cls, *args, **kwargs): if not cls._INSTANCE: print_d("Creating new server instance") cls._INSTANCE = super().__new__(cls) cls._CREATION_TIME = time.time() return cls._INSTANCE if time.time() - cls._CREATION_TIME > cls._MAX_CACHE_SECS: print_d("Recreating stale server instance") del cls._INSTANCE cls._CREATION_TIME = time.time() cls._INSTANCE = super().__new__(cls) return cls._INSTANCE
def connect(self, host=None, port=None, keepalive=30, bind_address=""): host = host or self.settings.hostname port = port or self.settings.port check_listening(host, port, msg="check your MQTT settings") ret = super().connect(host=host, port=port, keepalive=keepalive, bind_address=bind_address) if MQTT_ERR_SUCCESS == ret: print_d("Connecting to {}", self.settings) return ret raise Error("Couldn't connect to {}".format(self.settings))
def speech_response(title=None, speech=None, reprompt_text=None, end=True, store=None, text=None, use_ssml=False): speechlet_response = speech_fragment(speech=speech or title, title=title, reprompt_text=reprompt_text, text=text, end=end, use_ssml=use_ssml) print_d("Returning {response}", response=speechlet_response) return _build_response(speechlet_response, store=store)
def __init__(self, client: CustomClient, req_topic: str, resp_topic: str): def subscribed(client, userdata, mind, granted_qos): self.is_connected = True print_d("MQTT/TLS transport to {client} initialised. (@QoS {qos})", client=client, qos=granted_qos) super().__init__() self.client = client self.req_topic = req_topic self.resp_topic = resp_topic self.client.on_subscribe = subscribed self.client.on_message = self._on_message self.message = [] print_d("Created transport: {self!r}", self=self)
def create(self, mqtt_client=None): if self.mqtt_settings.configured: s = self.mqtt_settings print_d("Found MQTT config, so setting up MQTT transport.") client = mqtt_client or CustomClient(s) return MqttTransport(client, req_topic=s.topic_req, resp_topic=s.topic_resp) print_d("Defaulting to SSL transport") s = self.ssl_config return SslSocketTransport(hostname=s.server_hostname, port=s.port, ca_file=s.ca_file_path, cert_file=s.cert_file_path, verify_hostname=s.verify_server_hostname)
def communicate(self, data, wait=True): stripped = data.rstrip('\n') if data.startswith('serverstatus'): print_d("Faking server status...") return ( '{orig} player%20count:1 playerid:{pid} name:{name}\n'.format( orig=stripped, name=self.player_name, pid=self.player_id)) elif ' status ' in stripped: print_d("Faking player status...") return stripped + A_REAL_STATUS elif 'login ' in stripped: return 'login %s ******' % stripped.split()[1].replace(' ', '%20') elif ' time ' in data: return '%s %.3f' % (stripped.rstrip('?'), FAKE_LENGTH) elif 'favorites items ' in data: return stripped + REAL_FAVES return stripped + ' OK\n'
def create_transport(ssl_config=SSL_SETTINGS, mqtt_settings=MQTT_SETTINGS): if mqtt_settings.configured: s = mqtt_settings print_d("Found MQTT config, so setting up MQTT transport.") client = CustomClient(s) transport = MqttTransport(client, req_topic=s.TOPIC_REQ, resp_topic=s.TOPIC_RESP) transport.start() return transport print_d("Defaulting to SSL transport") s = ssl_config return SslSocketTransport(hostname=s.server_hostname, port=s.port, ca_file=s.ca_file_path, cert_file=s.cert_file_path, verify_hostname=s.verify_server_hostname)
def _request(self, lines, raw=False, wait=True) -> List[str]: """ Send multiple pipelined requests to the server, if connected, and return their responses, assuming order is maintained (which seems safe). """ if not (lines and len(lines)): return [] lines = [l.rstrip() for l in lines] first_word = lines[0].split()[0] if not (self.transport.is_connected or first_word == 'login'): raise SqueezeboxException( "Can't do '{cmd}', {transport} is not connected".format( cmd=first_word, transport=self.transport)) if self._debug: print_d("<<<< " + "\n..<< ".join(lines)) request = "\n".join(lines) raw_response = self.transport.communicate(request, wait=wait) if not wait: return [] if not raw_response: raise SqueezeboxException( "No further response from %s. Login problem?" % self) raw_response = raw_response.rstrip("\n") response = raw_response if raw else self._unquote(raw_response) if self._debug: print_d(">>>> " + "\n..>> ".join(response.splitlines())) def start_point(text): if first_word == 'login': return 6 delta = -1 if text.endswith('?') else 1 return len(self._unquote(text) if raw else text) + delta resp_lines = response.splitlines() if len(lines) != len(resp_lines): raise ValueError("Response problem: %s != %s" % (lines, resp_lines)) return [ resp_line[start_point(line):] for line, resp_line in zip(lines, resp_lines) ]
def communicate(self, data, wait=True): eof = False response = '' num_lines = data.count("\n") try: self._ssl_sock.sendall(data.encode('utf-8')) if not wait: return None while not eof: response += self._ssl_sock.recv().decode('utf-8') eof = response.count("\n") == num_lines or not response return response except socket.error as e: print_d("Couldn't communicate with Squeezebox ({!r})", e) self.failures += 1 if self.failures >= self._MAX_FAILURES: print_w("Too many Squeezebox failures. Disconnecting") self.is_connected = False return None
def communicate(self, raw: str, wait=True) -> str: data = raw.strip() + '\n' num_lines = data.count('\n') self._clear() ret = self.client.publish(self.req_topic, data.encode('utf-8'), qos=1 if wait else 0) if not wait: return None ret.wait_for_publish() if ret.rc != MQTT_ERR_SUCCESS: raise Error("Error publishing message: {}", error_string(ret.rc)) print_d("Published to {topic} OK. Waiting for {num} line(s)...", topic=self.req_topic, num=num_lines) wait_for(lambda s: len(s.response_lines) >= num_lines, context=self, what="response from mqtt-squeeze", timeout=5) return "\n".join(m.decode('utf-8') for m in self.response_lines)
def __init__(self, transport, user=None, password=None, cur_player_id=None, debug=False): self.transport = transport self._debug = debug self.user = user self.password = password if user and password: self.log_in() print_d("Authenticated with %s!" % self) self.players = {} self.refresh_status() self.cur_player_id = pid = cur_player_id or list(self.players)[0] print_d("Default player is now %s" % (self.players[pid], )) self.__genres = [] self.__playlists = [] self.__favorites = [] self._created_time = time.time()
def get_track_details(self, offset=0, player_id=None) -> Dict[str, List]: """Returns a dict of details, for current (offset=0) or future (offset>0) playlist tracks""" pid = player_id or self.cur_player_id index = offset + 1 cmd = "status - %d tags:%s" % (index, DETAILS_TAGS) responses = self.player_request(cmd, pid, raw=True) items = next(self._groups(responses)).items() def values_for(tag: str, value: str) -> List[str]: return ([value] if tag in ('title', 'album') else [v.strip() for v in value.split(',')]) TAGS = { 'title', 'genre', 'genres', 'album', 'trackartist', 'artist', 'albumartist', 'composer' } details = {k: values_for(k, v) for k, v in items if v and k in TAGS} if 'genres' in details: details['genre'] = details['genres'] del details['genres'] print_d("Processed details: {d}", d=details) return details
def start(self): def connected(client, userdata, flags, rc): print_d("Connected to {client}. Subscribing to {topic}", client=self.client, topic=self.resp_topic) result, mid = self.client.subscribe(self.resp_topic, qos=1) if result != MQTT_ERR_SUCCESS: raise Error("Couldn't subscribe to '{topic}'", self.resp_topic) def disconnected(client, userdata, rc): print_d("Disconnected from {client}", client=self.client) self.is_connected = False self.is_connected = self.client.connected if self.is_connected: print_d("Already connected, great!") return self.client.on_connect = connected self.client.on_disconnect = disconnected assert self.client.loop_start() != MQTT_ERR_INVAL self.client.connect() wait_for(lambda s: s.is_connected, what="connection", context=self) return self
def _request(self, lines, raw=False, wait=True) -> List[str]: """ Send multiple pipelined requests to the server, if connected, and return their responses, assuming order is maintained (which seems safe). """ if not (lines and len(lines)): return [] lines = [l.rstrip() for l in lines] match = RESPONSE_CMD_REGEX.match(lines[0]) # If we can't match, then take the first two words (for debugging) first_word = (match.group(2) if match else ' '.join(lines[0].split()[:2])) if not (self.transport.is_connected or first_word == 'login'): try: print_w("Transport wasn't connected - trying to restart") self.transport.start() except Exception: raise SqueezeboxException( "Can't do '{cmd}', {transport} is not connected".format( cmd=first_word, transport=self.transport)) if self._debug: print_d("<<<< " + "\n..<< ".join(lines)) request = "\n".join(lines) raw_response = self.transport.communicate(request, wait=wait) if not wait: return [] if not raw_response: raise SqueezeboxException( "No further response from %s. Login problem?" % self) raw_response = raw_response.rstrip("\n") response = raw_response if raw else self._unquote(raw_response) if self._debug: print_d(">>>> " + "\n..>> ".join(response.splitlines())) def start_point(text): if first_word == 'login': return 6 delta = -1 if text.endswith('?') else 1 return len(self._unquote(text) if raw else text) + delta resp_lines = response.splitlines() if len(lines) != len(resp_lines): print_d("Got mismatched response: {lines} vs {resp_lines}", lines=lines, resp_lines=resp_lines) raise Error("Transport response problem: got %d lines, not %d" % (len(resp_lines), len(lines))) return [ resp_line[start_point(line):] for line, resp_line in zip(lines, resp_lines) ]
def refresh_status(self): """ Updates the list of the Squeezebox players available and other server metadata.""" print_d("Refreshing server and player statuses...") response = self.__a_request("serverstatus 0 99", raw=True) self.players = {} for data in self._groups(response, 'playerid', extra_bools=['power', 'connected']): self.players[data['playerid']] = SqueezeboxPlayerSettings(data) print_d("Found %d player(s): %s" % (len(self.players), [p['name'] for p in self.players.values()])) if self._debug: print_d("Player(s): %s", self.players.values())
def refresh_status(self): """ Updates the list of the Squeezebox players available and other server metadata.""" print_d("Refreshing server and player statuses.") response = self.__a_request("serverstatus 0 99", raw=True) self.players = {} for data in self._groups(response, 'playerid', extra_bools=['power', 'connected']): if data.get('connected', False): self.players[data['playerid']] = SqueezeboxPlayerSettings(data) print_d("Found {total} connected player(s): {players}", total=len(self.players), players=[p.get('name', _("Unknown player")) for p in self.players.values()]) if self._debug: print_d("Player(s): {players}", players=self.players.values())
def on_message(client, userdata, message): num_lines = message.payload.count(b'\n') msg = message.payload.decode('utf-8') if DEBUG: print_d(">>> {msg} (@QoS {qos})", msg=msg.strip(), qos=message.qos) telnet.write(message.payload.strip() + b'\n') resp_lines = [] while len(resp_lines) < num_lines: resp_lines.append(telnet.read_until(b'\n').strip()) rsp = b'\n'.join(resp_lines) if rsp: if DEBUG: print_d("<<< {}", rsp.decode('utf-8')) client.publish(MQTT_SETTINGS.topic_resp, rsp, qos=1) else: print_d("No reply")
def create(self, *args, **kwargs): instance = type(self)._INSTANCE if instance and self._too_old(): print_d("Killing stale server instance.") instance.disconnect() instance = self._INSTANCE = None # Fall through if not instance or not instance.connected: print_d("Creating new server instance") transport = self.transport_factory.create() transport.start() inst = type(self)._INSTANCE = Server(transport, *args, **kwargs) type(self)._CREATION_TIME = time.time() return inst print_d("Reusing cached instance {object}", object=instance) return instance