first commit
This commit is contained in:
205
librespot_player/__init__.py
Normal file
205
librespot_player/__init__.py
Normal file
@@ -0,0 +1,205 @@
|
||||
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:
|
||||
# Audio
|
||||
preferred_quality: AudioQuality
|
||||
enable_normalisation: bool
|
||||
normalisation_pregain: float
|
||||
autoplay_enabled: bool
|
||||
crossfade_duration: int
|
||||
preload_enabled: bool
|
||||
|
||||
# Volume
|
||||
initial_volume: int
|
||||
volume_steps: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
preferred_quality: AudioQuality,
|
||||
enable_normalisation: bool,
|
||||
normalisation_pregain: float,
|
||||
autoplay_enabled: bool,
|
||||
crossfade_duration: int,
|
||||
preload_enabled: bool,
|
||||
initial_volume: int,
|
||||
volume_steps: int,
|
||||
):
|
||||
self.preferred_quality = preferred_quality
|
||||
self.enable_normalisation = enable_normalisation
|
||||
self.normalisation_pregain = normalisation_pregain
|
||||
self.autoplay_enabled = autoplay_enabled
|
||||
self.crossfade_duration = crossfade_duration
|
||||
self.preload_enabled = preload_enabled
|
||||
self.initial_volume = initial_volume
|
||||
self.volume_steps = volume_steps
|
||||
|
||||
class Builder:
|
||||
preferred_quality: AudioQuality = AudioQuality.NORMAL
|
||||
enable_normalisation: bool = True
|
||||
normalisation_pregain: float = 3.0
|
||||
autoplay_enabled: bool = True
|
||||
crossfade_duration: int = 0
|
||||
preload_enabled: bool = True
|
||||
|
||||
# Volume
|
||||
initial_volume: int = 65536
|
||||
volume_steps: int = 64
|
||||
|
||||
def set_preferred_quality(
|
||||
self, preferred_quality: AudioQuality) -> __class__:
|
||||
self.preferred_quality = preferred_quality
|
||||
return self
|
||||
|
||||
def set_enable_normalisation(self,
|
||||
enable_normalisation: bool) -> __class__:
|
||||
self.enable_normalisation = enable_normalisation
|
||||
return self
|
||||
|
||||
def set_normalisation_pregain(
|
||||
self, normalisation_pregain: float) -> __class__:
|
||||
self.normalisation_pregain = normalisation_pregain
|
||||
return self
|
||||
|
||||
def set_autoplay_enabled(self, autoplay_enabled: bool) -> __class__:
|
||||
self.autoplay_enabled = autoplay_enabled
|
||||
return self
|
||||
|
||||
def set_crossfade_duration(self, crossfade_duration: int) -> __class__:
|
||||
self.crossfade_duration = crossfade_duration
|
||||
return self
|
||||
|
||||
def set_preload_enabled(self, preload_enabled: bool) -> __class__:
|
||||
self.preload_enabled = preload_enabled
|
||||
return self
|
||||
|
||||
def build(self) -> PlayerConfiguration:
|
||||
return PlayerConfiguration(
|
||||
self.preferred_quality,
|
||||
self.enable_normalisation,
|
||||
self.normalisation_pregain,
|
||||
self.autoplay_enabled,
|
||||
self.crossfade_duration,
|
||||
self.preload_enabled,
|
||||
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
|
||||
Reference in New Issue
Block a user