Merge pull request #324 from Googolplexed0/proto-ext-metadata

Implement ExtendedMetadata Method for Content Loading
This commit is contained in:
碧舞すみほ
2025-11-13 09:00:45 +09:00
committed by GitHub
10 changed files with 505 additions and 104 deletions

View File

@@ -57,6 +57,8 @@ from librespot.proto import Connectivity_pb2 as Connectivity
from librespot.proto import Keyexchange_pb2 as Keyexchange
from librespot.proto import Metadata_pb2 as Metadata
from librespot.proto import Playlist4External_pb2 as Playlist4External
from librespot.proto.ExtendedMetadata_pb2 import EntityRequest, BatchedEntityRequest, ExtensionQuery, BatchedExtensionResponse
from librespot.proto.ExtensionKind_pb2 import ExtensionKind
from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate
from librespot.proto.spotify.login5.v3 import Login5_pb2 as Login5
from librespot.proto.spotify.login5.v3.credentials import Credentials_pb2 as Login5Credentials
@@ -104,20 +106,20 @@ class ApiClient(Closeable):
self.logger.debug("Updated client token: {}".format(
self.__client_token_str))
request = requests.PreparedRequest()
request.method = method
request.data = body
request.headers = CaseInsensitiveDict()
if headers is not None:
request.headers = headers
request.headers["Authorization"] = "Bearer {}".format(
self.__session.tokens().get("playlist-read"))
request.headers["client-token"] = self.__client_token_str
if url is None:
request.url = self.__base_url + suffix
url = self.__base_url + suffix
else:
request.url = url + suffix
return request
url = url + suffix
if headers is None:
headers = CaseInsensitiveDict()
headers["Authorization"] = "Bearer {}".format(
self.__session.tokens().get("playlist-read"))
headers["client-token"] = self.__client_token_str
request = requests.Request(method, url, headers=headers, data=body)
return request.prepare()
def send(
self,
@@ -190,22 +192,36 @@ class ApiClient(Closeable):
self.logger.warning("PUT state returned {}. headers: {}".format(
response.status_code, response.headers))
def get_ext_metadata(self, extension_kind: ExtensionKind, uri: str):
headers = CaseInsensitiveDict({"content-type": "application/x-protobuf"})
req = EntityRequest(entity_uri=uri, query=[ExtensionQuery(extension_kind=extension_kind),])
response = self.send("POST", "/extended-metadata/v0/extended-metadata",
headers, BatchedEntityRequest(entity_request=[req,]).SerializeToString())
ApiClient.StatusCodeException.check_status(response)
body = response.content
if body is None:
raise ConnectionError("Extended Metadata request failed: No response body")
proto = BatchedExtensionResponse()
proto.ParseFromString(body)
entityextd = proto.extended_metadata.pop().extension_data.pop()
if entityextd.header.status_code != 200:
raise ConnectionError("Extended Metadata request failed: Status code {}".format(entityextd.header.status_code))
mdb: bytes = entityextd.extension_data.value
return mdb
def get_metadata_4_track(self, track: TrackId) -> Metadata.Track:
"""
:param track: TrackId:
"""
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
"/metadata/4/track/{}".format(track.hex_id()),
None, None)
ApiClient.StatusCodeException.check_status(response)
body = response.content
if body is None:
raise RuntimeError()
proto = Metadata.Track()
proto.ParseFromString(body)
return proto
mdb = self.get_ext_metadata(ExtensionKind.TRACK_V4, track.to_spotify_uri())
md = Metadata.Track()
md.ParseFromString(mdb)
return md
def get_metadata_4_episode(self, episode: EpisodeId) -> Metadata.Episode:
"""
@@ -213,16 +229,10 @@ class ApiClient(Closeable):
:param episode: EpisodeId:
"""
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
"/metadata/4/episode/{}".format(episode.hex_id()),
None, None)
ApiClient.StatusCodeException.check_status(response)
body = response.content
if body is None:
raise IOError()
proto = Metadata.Episode()
proto.ParseFromString(body)
return proto
mdb = self.get_ext_metadata(ExtensionKind.EPISODE_V4, episode.to_spotify_uri())
md = Metadata.Episode()
md.ParseFromString(mdb)
return md
def get_metadata_4_album(self, album: AlbumId) -> Metadata.Album:
"""
@@ -230,17 +240,10 @@ class ApiClient(Closeable):
:param album: AlbumId:
"""
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
"/metadata/4/album/{}".format(album.hex_id()),
None, None)
ApiClient.StatusCodeException.check_status(response)
body = response.content
if body is None:
raise IOError()
proto = Metadata.Album()
proto.ParseFromString(body)
return proto
mdb = self.get_ext_metadata(ExtensionKind.ALBUM_V4, album.to_spotify_uri())
md = Metadata.Album()
md.ParseFromString(mdb)
return md
def get_metadata_4_artist(self, artist: ArtistId) -> Metadata.Artist:
"""
@@ -248,16 +251,10 @@ class ApiClient(Closeable):
:param artist: ArtistId:
"""
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
"/metadata/4/artist/{}".format(artist.hex_id()),
None, None)
ApiClient.StatusCodeException.check_status(response)
body = response.content
if body is None:
raise IOError()
proto = Metadata.Artist()
proto.ParseFromString(body)
return proto
mdb = self.get_ext_metadata(ExtensionKind.ARTIST_V4, artist.to_spotify_uri())
md = Metadata.Artist()
md.ParseFromString(mdb)
return md
def get_metadata_4_show(self, show: ShowId) -> Metadata.Show:
"""
@@ -265,16 +262,10 @@ class ApiClient(Closeable):
:param show: ShowId:
"""
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
"/metadata/4/show/{}".format(show.hex_id()), None,
None)
ApiClient.StatusCodeException.check_status(response)
body = response.content
if body is None:
raise IOError()
proto = Metadata.Show()
proto.ParseFromString(body)
return proto
mdb = self.get_ext_metadata(ExtensionKind.SHOW_V4, show.to_spotify_uri())
md = Metadata.Show()
md.ParseFromString(mdb)
return md
def get_playlist(self,
_id: PlaylistId) -> Playlist4External.SelectedListContent: