diff --git a/librespot/__init__.py b/librespot/__init__.py index 8f688cf..adb415a 100644 --- a/librespot/__init__.py +++ b/librespot/__init__.py @@ -6,7 +6,7 @@ class Version: - version_name = "0.0.10" + version_name = "0.0.11" @staticmethod def platform() -> Platform: diff --git a/librespot/audio/__init__.py b/librespot/audio/__init__.py index 6f0f067..5bb15ef 100644 --- a/librespot/audio/__init__.py +++ b/librespot/audio/__init__.py @@ -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, @@ -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, @@ -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) @@ -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 @@ -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!") @@ -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], @@ -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]: diff --git a/librespot/core.py b/librespot/core.py index 08c30fe..6ec5c70 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -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 @@ -267,20 +267,20 @@ 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 @@ -1065,7 +1065,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) diff --git a/librespot/metadata.py b/librespot/metadata.py index a5e01e9..7b5cda5 100644 --- a/librespot/metadata.py +++ b/librespot/metadata.py @@ -5,279 +5,121 @@ import re -class SpotifyId: - STATIC_FROM_URI = "fromUri" - STATIC_FROM_BASE62 = "fromBase62" - STATIC_FROM_HEX = "fromHex" +class Id: + b62 = Base62.create_instance_with_inverted_character_set() + uri_pattern: str = None + mercury_pattern: str = None - @staticmethod - def from_base62(base62: str): - raise NotImplementedError - - @staticmethod - def from_hex(hex_str: str): - raise NotImplementedError - - @staticmethod - def from_uri(uri: str): - raise NotImplementedError - - def to_spotify_uri(self) -> str: - raise NotImplementedError - - class SpotifyIdParsingException(Exception): - pass - - -class PlayableId: - base62 = Base62.create_instance_with_inverted_character_set() - - @staticmethod - def from_uri(uri: str) -> PlayableId: - if not PlayableId.is_supported(uri): - return UnsupportedId(uri) - if TrackId.pattern.search(uri) is not None: - return TrackId.from_uri(uri) - if EpisodeId.pattern.search(uri) is not None: - return EpisodeId.from_uri(uri) - raise TypeError("Unknown uri: {}".format(uri)) - - @staticmethod - def is_supported(uri: str): - return (not uri.startswith("spotify:local:") - and not uri == "spotify:delimiter" - and not uri == "spotify:meta:delimiter") - - @staticmethod - def should_play(track: ContextTrack): - return track.metadata_or_default - - def get_gid(self) -> bytes: - raise NotImplementedError - - def hex_id(self) -> str: - raise NotImplementedError - - def to_spotify_uri(self) -> str: - raise NotImplementedError + def __init__(self, _id: str): + self.__id = _id + @classmethod + def from_base62(cls, base62: str) -> Id: + return cls(base62) -class PlaylistId(SpotifyId): - base62 = Base62.create_instance_with_inverted_character_set() - pattern = re.compile(r"spotify:playlist:(.{22})") - __id: str + @classmethod + def from_hex(cls, hex_str: str) -> Id: + return cls(cls.b62.encode(util.hex_to_bytes(hex_str)).decode()) - def __init__(self, _id: str): - self.__id = _id + @classmethod + def match_uri(cls, uri: str): + if not cls.uri_pattern: + raise NotImplementedError + return re.search(cls.uri_pattern + r":(.{22})", uri) - @staticmethod - def from_uri(uri: str) -> PlaylistId: - matcher = PlaylistId.pattern.search(uri) + @classmethod + def from_uri(cls, uri: str) -> Id: + matcher = cls.match_uri(uri) if matcher is not None: - playlist_id = matcher.group(1) - return PlaylistId(playlist_id) - raise TypeError("Not a Spotify playlist ID: {}.".format(uri)) + return cls(matcher.group(1)) + raise TypeError("Not a Spotify ID: {}.".format(uri)) def id(self) -> str: return self.__id - def to_spotify_uri(self) -> str: - return "spotify:playlist:" + self.__id - - -class UnsupportedId(PlayableId): - uri: str - - def __init__(self, uri: str): - self.uri = uri - def get_gid(self) -> bytes: - raise TypeError() + return self.b62.decode(self.__id.encode(), 16) def hex_id(self) -> str: - raise TypeError() + return util.bytes_to_hex(self.get_gid()).lower() def to_spotify_uri(self) -> str: - return self.uri - - -class AlbumId(SpotifyId): - base62 = Base62.create_instance_with_inverted_character_set() - pattern = re.compile(r"spotify:album:(.{22})") - __hex_id: str - - def __init__(self, hex_id: str): - self.__hex_id = hex_id.lower() - - @staticmethod - def from_uri(uri: str) -> AlbumId: - matcher = AlbumId.pattern.search(uri) - if matcher is not None: - album_id = matcher.group(1) - return AlbumId(util.bytes_to_hex(AlbumId.base62.decode(album_id.encode(), 16))) - raise TypeError("Not a Spotify album ID: {}.".format(uri)) - - @staticmethod - def from_base62(base62: str) -> AlbumId: - return AlbumId(util.bytes_to_hex(AlbumId.base62.decode(base62.encode(), 16))) - - @staticmethod - def from_hex(hex_str: str) -> AlbumId: - return AlbumId(hex_str) + if not self.uri_pattern: + raise NotImplementedError + return self.uri_pattern + ":" + self.__id def to_mercury_uri(self) -> str: - return "hm://metadata/4/album/{}".format(self.__hex_id) - - def hex_id(self) -> str: - return self.__hex_id - - def to_spotify_uri(self) -> str: - return "spotify:album:{}".format( - AlbumId.base62.encode(util.hex_to_bytes(self.__hex_id)).decode()) - - -class ArtistId(SpotifyId): - base62 = Base62.create_instance_with_inverted_character_set() - pattern = re.compile("spotify:artist:(.{22})") - __hex_id: str - - def __init__(self, hex_id: str): - self.__hex_id = hex_id.lower() + if not self.mercury_pattern: + raise NotImplementedError + return self.mercury_pattern + "/" + self.hex_id() - @staticmethod - def from_uri(uri: str) -> ArtistId: - matcher = ArtistId.pattern.search(uri) - if matcher is not None: - artist_id = matcher.group(1) - return ArtistId( - util.bytes_to_hex(ArtistId.base62.decode(artist_id.encode(), 16))) - raise TypeError("Not a Spotify artist ID: {}".format(uri)) - @staticmethod - def from_base62(base62: str) -> ArtistId: - return ArtistId(util.bytes_to_hex(ArtistId.base62.decode(base62.encode(), 16))) +class PlaylistId(Id): + uri_pattern = r"spotify:playlist" - @staticmethod - def from_hex(hex_str: str) -> ArtistId: - return ArtistId(hex_str) - def to_mercury_uri(self) -> str: - return "hm://metadata/4/artist/{}".format(self.__hex_id) +class AlbumId(Id): + uri_pattern = r"spotify:album" + mercury_pattern = "hm://metadata/4/album" - def to_spotify_uri(self) -> str: - return "spotify:artist:{}".format( - ArtistId.base62.encode(util.hex_to_bytes(self.__hex_id)).decode()) - def hex_id(self) -> str: - return self.__hex_id +class ArtistId(Id): + uri_pattern = r"spotify:artist" + mercury_pattern = "hm://metadata/4/artist" -class EpisodeId(SpotifyId, PlayableId): - pattern = re.compile(r"spotify:episode:(.{22})") - __hex_id: str +class ShowId(Id): + uri_pattern = r"spotify:show" + mercury_pattern = "hm://metadata/4/show" - def __init__(self, hex_id: str): - self.__hex_id = hex_id.lower() +class PlayableId: @staticmethod - def from_uri(uri: str) -> EpisodeId: - matcher = EpisodeId.pattern.search(uri) - if matcher is not None: - episode_id = matcher.group(1) - return EpisodeId( - util.bytes_to_hex(PlayableId.base62.decode(episode_id.encode(), 16))) - raise TypeError("Not a Spotify episode ID: {}".format(uri)) + def from_uri(uri: str) -> PlayableId: + if not PlayableId.is_supported(uri): + return UnsupportedId(uri) + if TrackId.match_uri(uri) is not None: + return TrackId.from_uri(uri) + if EpisodeId.match_uri(uri) is not None: + return EpisodeId.from_uri(uri) + raise TypeError("Unknown uri: {}".format(uri)) @staticmethod - def from_base62(base62: str) -> EpisodeId: - return EpisodeId( - util.bytes_to_hex(PlayableId.base62.decode(base62.encode(), 16))) + def is_supported(uri: str) -> bool: + return (not uri.startswith("spotify:local:") + and not uri == "spotify:delimiter" + and not uri == "spotify:meta:delimiter") @staticmethod - def from_hex(hex_str: str) -> EpisodeId: - return EpisodeId(hex_str) + def should_play(track: ContextTrack): + return track.metadata_or_default - def to_mercury_uri(self) -> str: - return "hm://metadata/4/episode/{}".format(self.__hex_id) - def to_spotify_uri(self) -> str: - return "Spotify:episode:{}".format( - PlayableId.base62.encode(util.hex_to_bytes(self.__hex_id)).decode()) +class UnsupportedId(PlayableId): + def __init__(self, uri: str): + self.uri = uri - def hex_id(self) -> str: - return self.__hex_id + def id(self) -> str: + raise TypeError() def get_gid(self) -> bytes: - return util.hex_to_bytes(self.__hex_id) - - -class ShowId(SpotifyId): - base62 = Base62.create_instance_with_inverted_character_set() - pattern = re.compile("spotify:show:(.{22})") - __hex_id: str - - def __init__(self, hex_id: str): - self.__hex_id = hex_id - - @staticmethod - def from_uri(uri: str) -> ShowId: - matcher = ShowId.pattern.search(uri) - if matcher is not None: - show_id = matcher.group(1) - return ShowId(util.bytes_to_hex(ShowId.base62.decode(show_id.encode(), 16))) - raise TypeError("Not a Spotify show ID: {}".format(uri)) - - @staticmethod - def from_base62(base62: str) -> ShowId: - return ShowId(util.bytes_to_hex(ShowId.base62.decode(base62.encode(), 16))) - - @staticmethod - def from_hex(hex_str: str) -> ShowId: - return ShowId(hex_str) - - def to_mercury_uri(self) -> str: - return "hm://metadata/4/show/{}".format(self.__hex_id) - - def to_spotify_uri(self) -> str: - return "spotify:show:{}".format( - ShowId.base62.encode(util.hex_to_bytes(self.__hex_id)).decode()) + raise TypeError() def hex_id(self) -> str: - return self.__hex_id - - -class TrackId(PlayableId, SpotifyId): - pattern = re.compile("spotify:track:(.{22})") - __hex_id: str - - def __init__(self, hex_id: str): - self.__hex_id = hex_id.lower() - - @staticmethod - def from_uri(uri: str) -> TrackId: - search = TrackId.pattern.search(uri) - if search is not None: - track_id = search.group(1) - return TrackId( - util.bytes_to_hex(PlayableId.base62.decode(track_id.encode(), 16))) - raise RuntimeError("Not a Spotify track ID: {}".format(uri)) - - @staticmethod - def from_base62(base62: str) -> TrackId: - return TrackId(util.bytes_to_hex(PlayableId.base62.decode(base62.encode(), 16))) + raise TypeError() - @staticmethod - def from_hex(hex_str: str) -> TrackId: - return TrackId(hex_str) + def to_spotify_uri(self) -> str: + return self.uri def to_mercury_uri(self) -> str: - return "hm://metadata/4/track/{}".format(self.__hex_id) + raise TypeError() - def to_spotify_uri(self) -> str: - return "spotify:track:{}".format(TrackId.base62.encode(util.hex_to_bytes(self.__hex_id)).decode()) - def hex_id(self) -> str: - return self.__hex_id +class TrackId(Id, PlayableId): + uri_pattern = r"spotify:track" + mercury_pattern = "hm://metadata/4/track" - def get_gid(self) -> bytes: - return util.hex_to_bytes(self.__hex_id) + +class EpisodeId(Id, PlayableId): + uri_pattern = r"spotify:episode" + mercury_pattern = "hm://metadata/4/episode" diff --git a/setup.py b/setup.py index 1f3c5b5..5499c51 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import setuptools setuptools.setup(name="librespot", - version="0.0.10", + version="0.0.11", description="Open Source Spotify Client", long_description=open("README.md").read(), long_description_content_type="text/markdown",