2021-05-22 03:26:35 +02:00
|
|
|
import logging
|
|
|
|
import typing
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
2021-02-24 00:46:59 +01:00
|
|
|
from librespot.core.ApResolver import ApResolver
|
2021-05-22 03:26:41 +02:00
|
|
|
from librespot.metadata import AlbumId
|
|
|
|
from librespot.metadata import ArtistId
|
|
|
|
from librespot.metadata import EpisodeId
|
|
|
|
from librespot.metadata import ShowId
|
|
|
|
from librespot.metadata import TrackId
|
|
|
|
from librespot.proto import Connect
|
|
|
|
from librespot.proto import Metadata
|
2021-02-24 00:46:59 +01:00
|
|
|
from librespot.standard import Closeable
|
|
|
|
|
|
|
|
|
|
|
|
class ApiClient(Closeable):
|
|
|
|
_LOGGER: logging = logging.getLogger(__name__)
|
|
|
|
_session = None
|
|
|
|
_baseUrl: str = None
|
|
|
|
|
|
|
|
def __init__(self, session):
|
|
|
|
self._session = session
|
|
|
|
self._baseUrl = "https://{}".format(ApResolver.get_random_spclient())
|
|
|
|
|
2021-04-24 14:25:52 +02:00
|
|
|
def build_request(
|
2021-05-22 03:26:33 +02:00
|
|
|
self,
|
|
|
|
method: str,
|
|
|
|
suffix: str,
|
|
|
|
headers: typing.Union[None, typing.Dict[str, str]],
|
|
|
|
body: typing.Union[None, bytes],
|
|
|
|
) -> requests.PreparedRequest:
|
2021-02-24 00:46:59 +01:00
|
|
|
request = requests.PreparedRequest()
|
|
|
|
request.method = method
|
|
|
|
request.data = body
|
|
|
|
request.headers = {}
|
|
|
|
if headers is not None:
|
|
|
|
request.headers = headers
|
|
|
|
request.headers["Authorization"] = "Bearer {}".format(
|
2021-05-22 03:26:33 +02:00
|
|
|
self._session.tokens().get("playlist-read")
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
request.url = self._baseUrl + suffix
|
|
|
|
return request
|
|
|
|
|
2021-05-22 03:26:33 +02:00
|
|
|
def send(
|
|
|
|
self,
|
|
|
|
method: str,
|
|
|
|
suffix: str,
|
|
|
|
headers: typing.Union[None, typing.Dict[str, str]],
|
|
|
|
body: typing.Union[None, bytes],
|
|
|
|
) -> requests.Response:
|
2021-02-24 00:46:59 +01:00
|
|
|
resp = self._session.client().send(
|
2021-05-22 03:26:33 +02:00
|
|
|
self.build_request(method, suffix, headers, body)
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
return resp
|
|
|
|
|
2021-05-22 03:26:33 +02:00
|
|
|
def put_connect_state(
|
|
|
|
self, connection_id: str, proto: Connect.PutStateRequest
|
|
|
|
) -> None:
|
2021-02-24 00:46:59 +01:00
|
|
|
resp = self.send(
|
|
|
|
"PUT",
|
2021-05-22 03:26:33 +02:00
|
|
|
"/connect-state/v1/devices/{}".format(self._session.device_id()),
|
|
|
|
{
|
2021-02-24 00:46:59 +01:00
|
|
|
"Content-Type": "application/protobuf",
|
2021-05-22 03:26:33 +02:00
|
|
|
"X-Spotify-Connection-Id": connection_id,
|
|
|
|
},
|
|
|
|
proto.SerializeToString(),
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
|
|
|
|
if resp.status_code == 413:
|
|
|
|
self._LOGGER.warning(
|
2021-05-22 03:26:33 +02:00
|
|
|
"PUT state payload is too large: {} bytes uncompressed.".format(
|
|
|
|
len(proto.SerializeToString())
|
|
|
|
)
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
elif resp.status_code != 200:
|
2021-05-22 03:26:33 +02:00
|
|
|
self._LOGGER.warning(
|
|
|
|
"PUT state returned {}. headers: {}".format(
|
|
|
|
resp.status_code, resp.headers
|
|
|
|
)
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
|
|
|
|
def get_metadata_4_track(self, track: TrackId) -> Metadata.Track:
|
2021-05-22 03:26:33 +02:00
|
|
|
resp = self.send(
|
|
|
|
"GET", "/metadata/4/track/{}".format(track.hex_id()), None, None
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
ApiClient.StatusCodeException.check_status(resp)
|
|
|
|
|
|
|
|
body = resp.content
|
|
|
|
if body is None:
|
|
|
|
raise RuntimeError()
|
|
|
|
proto = Metadata.Track()
|
|
|
|
proto.ParseFromString(body)
|
|
|
|
return proto
|
|
|
|
|
|
|
|
def get_metadata_4_episode(self, episode: EpisodeId) -> Metadata.Episode:
|
2021-05-22 03:26:33 +02:00
|
|
|
resp = self.send(
|
|
|
|
"GET", "/metadata/4/episode/{}".format(episode.hex_id()), None, None
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
ApiClient.StatusCodeException.check_status(resp)
|
|
|
|
|
|
|
|
body = resp.content
|
|
|
|
if body is None:
|
|
|
|
raise IOError()
|
|
|
|
proto = Metadata.Episode()
|
|
|
|
proto.ParseFromString(body)
|
|
|
|
return proto
|
|
|
|
|
|
|
|
def get_metadata_4_album(self, album: AlbumId) -> Metadata.Album:
|
2021-05-22 03:26:33 +02:00
|
|
|
resp = self.send(
|
|
|
|
"GET", "/metadata/4/album/{}".format(album.hex_id()), None, None
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
ApiClient.StatusCodeException.check_status(resp)
|
|
|
|
|
|
|
|
body = resp.content
|
|
|
|
if body is None:
|
|
|
|
raise IOError()
|
|
|
|
proto = Metadata.Album()
|
|
|
|
proto.ParseFromString(body)
|
|
|
|
return proto
|
|
|
|
|
|
|
|
def get_metadata_4_artist(self, artist: ArtistId) -> Metadata.Artist:
|
2021-05-22 03:26:33 +02:00
|
|
|
resp = self.send(
|
|
|
|
"GET", "/metadata/4/artist/{}".format(artist.hex_id()), None, None
|
|
|
|
)
|
2021-02-24 00:46:59 +01:00
|
|
|
ApiClient.StatusCodeException.check_status(resp)
|
|
|
|
|
|
|
|
body = resp.content
|
|
|
|
if body is None:
|
|
|
|
raise IOError()
|
|
|
|
proto = Metadata.Artist()
|
|
|
|
proto.ParseFromString(body)
|
|
|
|
return proto
|
|
|
|
|
|
|
|
def get_metadata_4_show(self, show: ShowId) -> Metadata.Show:
|
2021-05-22 03:26:33 +02:00
|
|
|
resp = self.send("GET", "/metadata/4/show/{}".format(show.hex_id()), None, None)
|
2021-02-24 00:46:59 +01:00
|
|
|
ApiClient.StatusCodeException.check_status(resp)
|
|
|
|
|
|
|
|
body = resp.content
|
|
|
|
if body is None:
|
|
|
|
raise IOError()
|
|
|
|
proto = Metadata.Show()
|
|
|
|
proto.ParseFromString(body)
|
|
|
|
return proto
|
|
|
|
|
|
|
|
class StatusCodeException(IOError):
|
|
|
|
code: int
|
|
|
|
|
|
|
|
def __init__(self, resp: requests.Response):
|
|
|
|
super().__init__(resp.status_code)
|
|
|
|
self.code = resp.status_code
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def check_status(resp: requests.Response) -> None:
|
|
|
|
if resp.status_code != 200:
|
|
|
|
raise ApiClient.StatusCodeException(resp)
|