Skip to content
2 changes: 1 addition & 1 deletion librespot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class Version:
version_name = "0.0.10"
version_name = "0.0.13"

@staticmethod
def platform() -> Platform:
Expand Down
55 changes: 13 additions & 42 deletions librespot/audio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,32 +326,6 @@ def get_url(resp: StorageResolve.StorageResolveResponse) -> str:
selected_url = random.choice(resp.cdnurl)
return selected_url

@staticmethod
def load_track(
session: Session, track: Metadata.Track, file: Metadata.AudioFile,
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse,
str], preload: bool,
halt_listener: HaltListener) -> LoadedStream:
if type(resp_or_url) is str:
url = resp_or_url
else:
url = CdnFeedHelper.get_url(resp_or_url)
start = int(time.time() * 1000)
key = session.audio_key().get_audio_key(track.gid, file.file_id)
audio_key_time = int(time.time() * 1000) - start

streamer = session.cdn().stream_file(file, key, url, halt_listener)
input_stream = streamer.stream()
normalization_data = NormalizationData.read(input_stream)
if input_stream.skip(0xA7) != 0xA7:
raise IOError("Couldn't skip 0xa7 bytes!")
return LoadedStream(
track,
streamer,
normalization_data,
file.file_id, preload, audio_key_time
)

@staticmethod
def load_episode_external(
session: Session, episode: Metadata.Episode,
Expand All @@ -375,9 +349,9 @@ def load_episode_external(
)

@staticmethod
def load_episode(
def load_content(
session: Session,
episode: Metadata.Episode,
track_or_episode: typing.Union[Metadata.Track, Metadata.Episode],
file: Metadata.AudioFile,
resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str],
preload: bool,
Expand All @@ -388,7 +362,7 @@ def load_episode(
else:
url = CdnFeedHelper.get_url(resp_or_url)
start = int(time.time() * 1000)
key = session.audio_key().get_audio_key(episode.gid, file.file_id)
key = session.audio_key().get_audio_key(track_or_episode.gid, file.file_id)
audio_key_time = int(time.time() * 1000) - start

streamer = session.cdn().stream_file(file, key, url, halt_listener)
Expand All @@ -397,7 +371,7 @@ def load_episode(
if input_stream.skip(0xA7) != 0xA7:
raise IOError("Couldn't skip 0xa7 bytes!")
return LoadedStream(
episode,
track_or_episode,
streamer,
normalization_data,
file.file_id, preload, audio_key_time
Expand Down Expand Up @@ -742,22 +716,19 @@ def load(self, playable_id: PlayableId,
preload, halt_listener)
raise TypeError("Unknown content: {}".format(playable_id))

def load_stream(self, file: Metadata.AudioFile, track: Metadata.Track,
episode: Metadata.Episode, preload: bool,
halt_lister: HaltListener):
if track is None and episode is None:
def load_stream(self, file: Metadata.AudioFile,
track_or_episode: typing.Union[Metadata.Track, Metadata.Episode],
preload: bool, halt_lister: HaltListener):
if track_or_episode is None:
raise RuntimeError("No content passed!")
elif file is None:
raise RuntimeError("Content has no audio file!")
response = self.resolve_storage_interactive(file.file_id, preload)
if response.result == StorageResolve.StorageResolveResponse.Result.CDN:
if track is not None:
return CdnFeedHelper.load_track(self.__session, track, file,
response, preload, halt_lister)
return CdnFeedHelper.load_episode(self.__session, episode, file,
response, preload, halt_lister)
return CdnFeedHelper.load_content(self.__session, track_or_episode, file,
response, preload, halt_lister)
if response.result == StorageResolve.StorageResolveResponse.Result.STORAGE:
if track is None:
if track_or_episode is None:
pass
elif response.result == StorageResolve.StorageResolveResponse.Result.RESTRICTED:
raise RuntimeError("Content is restricted!")
Expand All @@ -779,7 +750,7 @@ def load_episode(self, episode_id: EpisodeId,
"Couldn't find any suitable audio file, available: {}".format(
episode.audio))
raise FeederException("Cannot find suitable audio file")
return self.load_stream(file, None, episode, preload, halt_listener)
return self.load_stream(file, episode, preload, halt_listener)

def load_track(self, track_id_or_track: typing.Union[TrackId,
Metadata.Track],
Expand All @@ -799,7 +770,7 @@ def load_track(self, track_id_or_track: typing.Union[TrackId,
"Couldn't find any suitable audio file, available: {}".format(
track.file))
raise FeederException("Cannot find suitable audio file")
return self.load_stream(file, track, None, preload, halt_listener)
return self.load_stream(file, track, preload, halt_listener)

def pick_alternative_if_necessary(
self, track: Metadata.Track) -> typing.Union[Metadata.Track, None]:
Expand Down
112 changes: 68 additions & 44 deletions librespot/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,13 @@ def get_ext_metadata(self, extension_kind: ExtensionKind, uri: str):

body = response.content
if body is None:
raise ConnectionError("Extended Metadata request failed: No response body")
raise ConnectionError("Extended Metadata request for {} failed: No response body".format(uri))

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))
raise ConnectionError("Extended Metadata request for {} failed: Status code {}".format(uri, entityextd.header.status_code))
mdb: bytes = entityextd.extension_data.value
return mdb

Expand Down Expand Up @@ -267,24 +267,52 @@ def get_metadata_4_show(self, show: ShowId) -> Metadata.Show:
md.ParseFromString(mdb)
return md

def get_playlist(self,
_id: PlaylistId) -> Playlist4External.SelectedListContent:
def get_playlist(self, playlist: PlaylistId) -> Playlist4External.SelectedListContent:
"""

:param _id: PlaylistId:
:param playlist: PlaylistId:

"""
response = self.send("GET",
"/playlist/v2/playlist/{}".format(_id.id()), None,
None)
response = self.send("GET", "/playlist/v2/playlist/{}".format(playlist.id()),
None, None)
ApiClient.StatusCodeException.check_status(response)

body = response.content
if body is None:
raise IOError()
raise ConnectionError("Playlist Metadata request for {} failed: No response body".format(playlist.to_spotify_uri()))

proto = Playlist4External.SelectedListContent()
proto.ParseFromString(body)
return proto

def get_user_profile(self, username: str, playlist_limit: int = None, artist_limit: int = None) -> dict[str, typing.Any]:
"""

:param user: str:
:param playlist_limit: int: (Default value = None)
:param artist_limit: int: (Default value = None)

"""

suffix = "/user-profile-view/v3/profile/{}".format(username)
if playlist_limit is not None or artist_limit is not None:
suffix += "?"
if playlist_limit is not None:
suffix += "playlist_limit={}".format(playlist_limit)
if artist_limit is not None:
suffix += "&"
if artist_limit is not None:
suffix += "artist_limit={}".format(artist_limit)

response = self.send("GET", suffix, None, None)
ApiClient.StatusCodeException.check_status(response)

body = response.content
if body is None:
raise ConnectionError("User Profile request for {} failed: No response body".format(username))

return response.json()

def set_client_token(self, client_token):
"""

Expand Down Expand Up @@ -1065,7 +1093,11 @@ def connect(self) -> None:
acc.write_int(2 + 4 + len(client_hello_bytes))
acc.write(client_hello_bytes)
# Read APResponseMessage
ap_response_message_length = self.connection.read_int()
try:
ap_response_message_length = self.connection.read_int()
except struct.error:
time.sleep(1)
ap_response_message_length = self.connection.read_int()
acc.write_int(ap_response_message_length)
ap_response_message_bytes = self.connection.read(
ap_response_message_length - 4)
Expand Down Expand Up @@ -1147,6 +1179,20 @@ def create_client(conf: Configuration) -> requests.Session:
client = requests.Session()
return client

def credentials(self) -> dict:
ap_welcome = self.ap_welcome()
reusable = ap_welcome.reusable_auth_credentials
reusable_type = Authentication.AuthenticationType.Name(
ap_welcome.reusable_auth_credentials_type)
return {
"username":
ap_welcome.canonical_username,
"credentials":
base64.b64encode(reusable).decode(),
"type":
reusable_type,
}

def dealer(self) -> DealerClient:
""" """
self.__wait_auth_lock()
Expand Down Expand Up @@ -1343,28 +1389,15 @@ def __authenticate_partial(self,
self.__auth_lock_bool = False
self.__auth_lock.notify_all()
if self.__inner.conf.store_credentials:
reusable = self.__ap_welcome.reusable_auth_credentials
reusable_type = Authentication.AuthenticationType.Name(
self.__ap_welcome.reusable_auth_credentials_type)
self.__stored_str = base64.b64encode(
json.dumps(self.credentials()).encode()
).decode()
if self.__inner.conf.stored_credentials_file is None:
raise TypeError(
"The file path to be saved is not specified")
self.__stored_str = base64.b64encode(
json.dumps({
"username":
self.__ap_welcome.canonical_username,
"credentials":
base64.b64encode(reusable).decode(),
"type":
reusable_type,
}).encode()).decode()
with open(self.__inner.conf.stored_credentials_file, "w") as f:
json.dump(
{
"username": self.__ap_welcome.canonical_username,
"credentials": base64.b64encode(reusable).decode(),
"type": reusable_type,
},
self.credentials(),
f,
)

Expand Down Expand Up @@ -1583,12 +1616,12 @@ def stored(self, stored_credentials_str: str):
else:
try:
self.login_credentials = Authentication.LoginCredentials(
typ=Authentication.AuthenticationType.Value(
obj["type"]),
typ=obj.get("auth_type") or Authentication.AuthenticationType.Value(obj["type"]),
username=obj["username"],
auth_data=base64.b64decode(obj["credentials"]),
auth_data=base64.b64decode(obj.get("auth_data") or obj["credentials"]),
)
except KeyError:
except KeyError as e:
self.logger.error("The stored credentials must contains these keys: username, auth_type or type, auth_data or credentials")
pass
return self

Expand All @@ -1610,23 +1643,14 @@ def stored_file(self,
pass
else:
try:
# Try Python librespot format first
self.login_credentials = Authentication.LoginCredentials(
typ=Authentication.AuthenticationType.Value(
obj["type"]),
typ=obj.get("auth_type") or Authentication.AuthenticationType.Value(obj["type"]),
username=obj["username"],
auth_data=base64.b64decode(obj["credentials"]),
auth_data=base64.b64decode(obj.get("auth_data") or obj["credentials"]),
)
except KeyError:
# Try Rust librespot format (auth_type as int, auth_data instead of credentials)
try:
self.login_credentials = Authentication.LoginCredentials(
typ=obj["auth_type"],
username=obj["username"],
auth_data=base64.b64decode(obj["auth_data"]),
)
except KeyError:
pass
self.logger.error("The stored credentials must contains these keys: username, auth_type or type, auth_data or credentials")
pass
return self

def oauth(self, oauth_url_callback, success_page_content = None) -> Session.Builder:
Expand Down
Loading
Loading