Skip to content

Commit acd633d

Browse files
authored
Merge pull request #324 from Googolplexed0/proto-ext-metadata
Implement ExtendedMetadata Method for Content Loading
2 parents 5182578 + 8736aca commit acd633d

File tree

10 files changed

+506
-105
lines changed

10 files changed

+506
-105
lines changed

librespot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
class Version:
9-
version_name = "0.0.9"
9+
version_name = "0.0.10"
1010

1111
@staticmethod
1212
def platform() -> Platform:

librespot/audio/__init__.py

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def load_track(
331331
session: Session, track: Metadata.Track, file: Metadata.AudioFile,
332332
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse,
333333
str], preload: bool,
334-
halt_listener: HaltListener) -> PlayableContentFeeder.LoadedStream:
334+
halt_listener: HaltListener) -> LoadedStream:
335335
if type(resp_or_url) is str:
336336
url = resp_or_url
337337
else:
@@ -345,18 +345,17 @@ def load_track(
345345
normalization_data = NormalizationData.read(input_stream)
346346
if input_stream.skip(0xA7) != 0xA7:
347347
raise IOError("Couldn't skip 0xa7 bytes!")
348-
return PlayableContentFeeder.LoadedStream(
348+
return LoadedStream(
349349
track,
350350
streamer,
351351
normalization_data,
352-
PlayableContentFeeder.Metrics(file.file_id, preload,
353-
-1 if preload else audio_key_time),
352+
file.file_id, preload, audio_key_time
354353
)
355354

356355
@staticmethod
357356
def load_episode_external(
358357
session: Session, episode: Metadata.Episode,
359-
halt_listener: HaltListener) -> PlayableContentFeeder.LoadedStream:
358+
halt_listener: HaltListener) -> LoadedStream:
360359
resp = session.client().head(episode.external_url)
361360

362361
if resp.status_code != 200:
@@ -368,11 +367,11 @@ def load_episode_external(
368367

369368
streamer = session.cdn().stream_external_episode(
370369
episode, url, halt_listener)
371-
return PlayableContentFeeder.LoadedStream(
370+
return LoadedStream(
372371
episode,
373372
streamer,
374373
None,
375-
PlayableContentFeeder.Metrics(None, False, -1),
374+
None, False, -1
376375
)
377376

378377
@staticmethod
@@ -383,7 +382,7 @@ def load_episode(
383382
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str],
384383
preload: bool,
385384
halt_listener: HaltListener,
386-
) -> PlayableContentFeeder.LoadedStream:
385+
) -> LoadedStream:
387386
if type(resp_or_url) is str:
388387
url = resp_or_url
389388
else:
@@ -397,12 +396,11 @@ def load_episode(
397396
normalization_data = NormalizationData.read(input_stream)
398397
if input_stream.skip(0xA7) != 0xA7:
399398
raise IOError("Couldn't skip 0xa7 bytes!")
400-
return PlayableContentFeeder.LoadedStream(
399+
return LoadedStream(
401400
episode,
402401
streamer,
403402
normalization_data,
404-
PlayableContentFeeder.Metrics(file.file_id, preload,
405-
-1 if preload else audio_key_time),
403+
file.file_id, preload, audio_key_time
406404
)
407405

408406

@@ -748,7 +746,9 @@ def load_stream(self, file: Metadata.AudioFile, track: Metadata.Track,
748746
episode: Metadata.Episode, preload: bool,
749747
halt_lister: HaltListener):
750748
if track is None and episode is None:
751-
raise RuntimeError()
749+
raise RuntimeError("No content passed!")
750+
elif file is None:
751+
raise RuntimeError("Content has no audio file!")
752752
response = self.resolve_storage_interactive(file.file_id, preload)
753753
if response.result == StorageResolve.StorageResolveResponse.Result.CDN:
754754
if track is not None:
@@ -778,6 +778,7 @@ def load_episode(self, episode_id: EpisodeId,
778778
self.logger.fatal(
779779
"Couldn't find any suitable audio file, available: {}".format(
780780
episode.audio))
781+
raise FeederException("Cannot find suitable audio file")
781782
return self.load_stream(file, None, episode, preload, halt_listener)
782783

783784
def load_track(self, track_id_or_track: typing.Union[TrackId,
@@ -797,7 +798,7 @@ def load_track(self, track_id_or_track: typing.Union[TrackId,
797798
self.logger.fatal(
798799
"Couldn't find any suitable audio file, available: {}".format(
799800
track.file))
800-
raise FeederException()
801+
raise FeederException("Cannot find suitable audio file")
801802
return self.load_stream(file, track, None, preload, halt_listener)
802803

803804
def pick_alternative_if_necessary(
@@ -848,43 +849,41 @@ def resolve_storage_interactive(
848849
storage_resolve_response.ParseFromString(body)
849850
return storage_resolve_response
850851

851-
class LoadedStream:
852-
episode: Metadata.Episode
853-
track: Metadata.Track
854-
input_stream: GeneralAudioStream
855-
normalization_data: NormalizationData
856-
metrics: PlayableContentFeeder.Metrics
857-
858-
def __init__(self, track_or_episode: typing.Union[Metadata.Track,
859-
Metadata.Episode],
860-
input_stream: GeneralAudioStream,
861-
normalization_data: typing.Union[NormalizationData, None],
862-
metrics: PlayableContentFeeder.Metrics):
863-
if type(track_or_episode) is Metadata.Track:
864-
self.track = track_or_episode
865-
self.episode = None
866-
elif type(track_or_episode) is Metadata.Episode:
867-
self.track = None
868-
self.episode = track_or_episode
869-
else:
870-
raise TypeError()
871-
self.input_stream = input_stream
872-
self.normalization_data = normalization_data
873-
self.metrics = metrics
852+
853+
class LoadedStream:
854+
episode: Metadata.Episode
855+
track: Metadata.Track
856+
input_stream: GeneralAudioStream
857+
normalization_data: NormalizationData
858+
metrics: Metrics
874859

875860
class Metrics:
876861
file_id: str
877862
preloaded_audio_key: bool
878863
audio_key_time: int
879864

880865
def __init__(self, file_id: typing.Union[bytes, None],
881-
preloaded_audio_key: bool, audio_key_time: int):
866+
preloaded_audio_key: bool, audio_key_time: int):
882867
self.file_id = None if file_id is None else util.bytes_to_hex(
883868
file_id)
884869
self.preloaded_audio_key = preloaded_audio_key
885-
self.audio_key_time = audio_key_time
886-
if preloaded_audio_key and audio_key_time != -1:
887-
raise RuntimeError()
870+
self.audio_key_time = -1 if preloaded_audio_key else audio_key_time
871+
872+
def __init__(self, track_or_episode: typing.Union[Metadata.Track, Metadata.Episode],
873+
input_stream: GeneralAudioStream,
874+
normalization_data: typing.Union[NormalizationData, None],
875+
file_id: str, preloaded_audio_key: bool, audio_key_time: int):
876+
if type(track_or_episode) is Metadata.Track:
877+
self.track = track_or_episode
878+
self.episode = None
879+
elif type(track_or_episode) is Metadata.Episode:
880+
self.track = None
881+
self.episode = track_or_episode
882+
else:
883+
raise TypeError()
884+
self.input_stream = input_stream
885+
self.normalization_data = normalization_data
886+
self.metrics = self.Metrics(file_id, preloaded_audio_key, audio_key_time)
888887

889888

890889
class StreamId:

librespot/core.py

Lines changed: 54 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
from librespot.proto import Keyexchange_pb2 as Keyexchange
5858
from librespot.proto import Metadata_pb2 as Metadata
5959
from librespot.proto import Playlist4External_pb2 as Playlist4External
60+
from librespot.proto.ExtendedMetadata_pb2 import EntityRequest, BatchedEntityRequest, ExtensionQuery, BatchedExtensionResponse
61+
from librespot.proto.ExtensionKind_pb2 import ExtensionKind
6062
from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate
6163
from librespot.proto.spotify.login5.v3 import Login5_pb2 as Login5
6264
from librespot.proto.spotify.login5.v3.credentials import Credentials_pb2 as Login5Credentials
@@ -104,20 +106,20 @@ def build_request(
104106
self.logger.debug("Updated client token: {}".format(
105107
self.__client_token_str))
106108

107-
request = requests.PreparedRequest()
108-
request.method = method
109-
request.data = body
110-
request.headers = CaseInsensitiveDict()
111-
if headers is not None:
112-
request.headers = headers
113-
request.headers["Authorization"] = "Bearer {}".format(
114-
self.__session.tokens().get("playlist-read"))
115-
request.headers["client-token"] = self.__client_token_str
116109
if url is None:
117-
request.url = self.__base_url + suffix
110+
url = self.__base_url + suffix
118111
else:
119-
request.url = url + suffix
120-
return request
112+
url = url + suffix
113+
114+
if headers is None:
115+
headers = CaseInsensitiveDict()
116+
headers["Authorization"] = "Bearer {}".format(
117+
self.__session.tokens().get("playlist-read"))
118+
headers["client-token"] = self.__client_token_str
119+
120+
request = requests.Request(method, url, headers=headers, data=body)
121+
122+
return request.prepare()
121123

122124
def send(
123125
self,
@@ -190,91 +192,80 @@ def put_connect_state(self, connection_id: str,
190192
self.logger.warning("PUT state returned {}. headers: {}".format(
191193
response.status_code, response.headers))
192194

195+
def get_ext_metadata(self, extension_kind: ExtensionKind, uri: str):
196+
headers = CaseInsensitiveDict({"content-type": "application/x-protobuf"})
197+
req = EntityRequest(entity_uri=uri, query=[ExtensionQuery(extension_kind=extension_kind),])
198+
199+
response = self.send("POST", "/extended-metadata/v0/extended-metadata",
200+
headers, BatchedEntityRequest(entity_request=[req,]).SerializeToString())
201+
ApiClient.StatusCodeException.check_status(response)
202+
203+
body = response.content
204+
if body is None:
205+
raise ConnectionError("Extended Metadata request failed: No response body")
206+
207+
proto = BatchedExtensionResponse()
208+
proto.ParseFromString(body)
209+
entityextd = proto.extended_metadata.pop().extension_data.pop()
210+
if entityextd.header.status_code != 200:
211+
raise ConnectionError("Extended Metadata request failed: Status code {}".format(entityextd.header.status_code))
212+
mdb: bytes = entityextd.extension_data.value
213+
return mdb
214+
193215
def get_metadata_4_track(self, track: TrackId) -> Metadata.Track:
194216
"""
195217
196218
:param track: TrackId:
197219
198220
"""
199-
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
200-
"/metadata/4/track/{}".format(track.hex_id()),
201-
None, None)
202-
ApiClient.StatusCodeException.check_status(response)
203-
body = response.content
204-
if body is None:
205-
raise RuntimeError()
206-
proto = Metadata.Track()
207-
proto.ParseFromString(body)
208-
return proto
221+
mdb = self.get_ext_metadata(ExtensionKind.TRACK_V4, track.to_spotify_uri())
222+
md = Metadata.Track()
223+
md.ParseFromString(mdb)
224+
return md
209225

210226
def get_metadata_4_episode(self, episode: EpisodeId) -> Metadata.Episode:
211227
"""
212228
213229
:param episode: EpisodeId:
214230
215231
"""
216-
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
217-
"/metadata/4/episode/{}".format(episode.hex_id()),
218-
None, None)
219-
ApiClient.StatusCodeException.check_status(response)
220-
body = response.content
221-
if body is None:
222-
raise IOError()
223-
proto = Metadata.Episode()
224-
proto.ParseFromString(body)
225-
return proto
232+
mdb = self.get_ext_metadata(ExtensionKind.EPISODE_V4, episode.to_spotify_uri())
233+
md = Metadata.Episode()
234+
md.ParseFromString(mdb)
235+
return md
226236

227237
def get_metadata_4_album(self, album: AlbumId) -> Metadata.Album:
228238
"""
229239
230240
:param album: AlbumId:
231241
232242
"""
233-
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
234-
"/metadata/4/album/{}".format(album.hex_id()),
235-
None, None)
236-
ApiClient.StatusCodeException.check_status(response)
237-
238-
body = response.content
239-
if body is None:
240-
raise IOError()
241-
proto = Metadata.Album()
242-
proto.ParseFromString(body)
243-
return proto
243+
mdb = self.get_ext_metadata(ExtensionKind.ALBUM_V4, album.to_spotify_uri())
244+
md = Metadata.Album()
245+
md.ParseFromString(mdb)
246+
return md
244247

245248
def get_metadata_4_artist(self, artist: ArtistId) -> Metadata.Artist:
246249
"""
247250
248251
:param artist: ArtistId:
249252
250253
"""
251-
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
252-
"/metadata/4/artist/{}".format(artist.hex_id()),
253-
None, None)
254-
ApiClient.StatusCodeException.check_status(response)
255-
body = response.content
256-
if body is None:
257-
raise IOError()
258-
proto = Metadata.Artist()
259-
proto.ParseFromString(body)
260-
return proto
254+
mdb = self.get_ext_metadata(ExtensionKind.ARTIST_V4, artist.to_spotify_uri())
255+
md = Metadata.Artist()
256+
md.ParseFromString(mdb)
257+
return md
261258

262259
def get_metadata_4_show(self, show: ShowId) -> Metadata.Show:
263260
"""
264261
265262
:param show: ShowId:
266263
267264
"""
268-
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com",
269-
"/metadata/4/show/{}".format(show.hex_id()), None,
270-
None)
271-
ApiClient.StatusCodeException.check_status(response)
272-
body = response.content
273-
if body is None:
274-
raise IOError()
275-
proto = Metadata.Show()
276-
proto.ParseFromString(body)
277-
return proto
265+
mdb = self.get_ext_metadata(ExtensionKind.SHOW_V4, show.to_spotify_uri())
266+
md = Metadata.Show()
267+
md.ParseFromString(mdb)
268+
return md
278269

279270
def get_playlist(self,
280271
_id: PlaylistId) -> Playlist4External.SelectedListContent:

librespot/proto/EntityExtensionData_pb2.py

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)