diff --git a/librespot/core.py b/librespot/core.py index 72fdfe8..27e8c8c 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -834,6 +834,12 @@ class Session(Closeable, MessageListener, SubListener): def device_id(self) -> str: return self.__inner.device_id + def device_name(self) -> str: + return self.__inner.device_name + + def device_type(self) -> Connect.DeviceType: + return self.__inner.device_type + def event(self, resp: MercuryClient.Response) -> None: if resp.uri == "spotify:user:attributes:update": attributes_update = UserAttributesUpdate() diff --git a/librespot/player/__init__.py b/librespot/player/__init__.py index ac83f4a..d82cd14 100644 --- a/librespot/player/__init__.py +++ b/librespot/player/__init__.py @@ -1,5 +1,95 @@ from __future__ import annotations +from librespot import Version from librespot.audio.decoders import AudioQuality +from librespot.core import Session +from librespot.mercury import MercuryRequests +from librespot.proto import Connect_pb2 as Connect +from librespot.structure import Closeable, MessageListener, RequestListener +import concurrent.futures +import logging +import typing +import urllib.parse + + +class DeviceStateHandler(Closeable, MessageListener, RequestListener): + logger = logging.getLogger("Librespot:DeviceStateHandler") + __closing = False + __connection_id: str = None + __device_info: Connect.DeviceInfo + __put_state: Connect.PutStateRequest + __put_state_worker = concurrent.futures.ThreadPoolExecutor() + __session: Session + + def __init__(self, session: Session, conf: PlayerConfiguration): + self.__session = session + self.__device_info = self.initialize_device_info(session, conf) + self.__put_state = Connect.PutStateRequest( + device=Connect.Device( + device_info=self.__device_info, + ), + member_type=Connect.MemberType.CONNECT_STATE, + ) + self.__session.dealer().add_message_listener(self, ["hm://pusher/v1/connections/", "hm://connect-state/v1/connect/volume", "hm://connect-state/v1/cluster"]) + self.__session.dealer().add_request_listener(self, "hm://connect-state/v1/") + + def close(self) -> None: + self.__closing = True + self.__session.dealer().remove_message_listener(self) + self.__session.dealer().remove_request_listener(self) + self.__put_state_worker.shutdown() + + def initialize_device_info(self, session: Session, conf: PlayerConfiguration) -> Connect.Device: + return Connect.DeviceInfo( + can_play=True, + capabilities=Connect.Capabilities( + can_be_player=True, + command_acks=True, + is_controllable=True, + is_observable=True, + gaia_eq_connect_id=True, + needs_full_player_state=False, + supported_types=["audio/episode", "audio/track"], + supports_command_request=True, + supports_gzip_pushes=True, + supports_logout=True, + supports_playlist_v2=True, + supports_rename=False, + supports_transfer_command=True, + volume_steps=conf.volume_steps, + ), + client_id=MercuryRequests.keymaster_client_id, + device_id=session.device_id(), + device_software_version=Version.version_string(), + device_type=session.device_type(), + name=session.device_name(), + spirc_version="3.2.6", + volume=conf.initial_volume, + ) + + def put_connect_state(self, request: Connect.PutStateRequest): + self.__session.api().put_connect_state(self.__connection_id, request) + self.logger.info("Put state. [ts: {}, connId: {}, reason: {}]".format(request.client_side_timestamp, self.__connection_id, request.put_state_reason)) + + def update_connection_id(self, newer: str) -> None: + newer = urllib.parse.unquote(newer) + if self.__connection_id is None or self.__connection_id != newer: + self.__connection_id = newer + self.logger.debug("Updated Spotify-Connection-Id: {}".format(newer)) + + + +class Player: + volume_max = 65536 + __conf: PlayerConfiguration + __session: Session + __state: StateWrapper + + def __init__(self, conf: PlayerConfiguration, session: Session): + self.__conf = conf + self.__session = session + + def init_state(self) -> None: + self.__state = StateWrapper(self.__session, self, self.__conf) class PlayerConfiguration: @@ -85,3 +175,20 @@ class PlayerConfiguration: self.initial_volume, self.volume_steps, ) + + +class StateWrapper(MessageListener): + __conf: PlayerConfiguration + __device: DeviceStateHandler + __player: Player + __session: Session + + def __init__(self, session: Session, player: Player, conf: PlayerConfiguration): + self.__session = session + self.__player = player + self.__device = DeviceStateHandler(session, conf) + self.__conf = conf + session.dealer().add_message_listener(self, ["spotify:user:attributes:update", "hm://playlist/", "hm://collection/collection/" + session.username() + "/json"]) + + def on_message(self, uri: str, headers: typing.Dict[str, str], payload: bytes): + pass