def __init__(self, session: Session, stream_id: StreamId, audio_format: SuperAudioFormat, cdn_url, cache: CacheManager, audio_decrypt: AudioDecrypt, halt_listener: HaltListener): self._session = session self._streamId = stream_id self._audioFormat = audio_format self._audioDecrypt = audio_decrypt self._cdnUrl = cdn_url self._haltListener = halt_listener resp = self.request(range_start=0, range_end=ChannelManager.CHUNK_SIZE - 1) content_range = resp._headers.get("Content-Range") if content_range is None: raise IOError("Missing Content-Range header!") split = Utils.split(content_range, "/") self._size = int(split[1]) self._chunks = int( math.ceil(self._size / ChannelManager.CHUNK_SIZE)) first_chunk = resp._buffer self._available = [False for _ in range(self._chunks)] self._requested = [False for _ in range(self._chunks)] self._buffer = [bytearray() for _ in range(self._chunks)] self._internalStream = CdnManager.Streamer.InternalStream( self, False) self._requested[0] = True self.write_chunk(first_chunk, 0, False)
def _put_connect_state(self, req: Connect.PutStateRequest): self._session.api().put_connect_state(self._connectionId, req) self._LOGGER.info("Put state. ts: {}, connId: {}, reason: {}".format( req.client_side_timestamp, Utils.truncate_middle(self._connectionId, 10), req.put_state_reason, ))
def from_uri(uri: str) -> ArtistId: matcher = ArtistId._PATTERN.search(uri) if matcher is not None: artist_id = matcher.group(1) return ArtistId( Utils.bytes_to_hex(ArtistId._BASE62.decode(artist_id, 16))) raise TypeError("Not a Spotify artist ID: {}".format(uri))
def from_uri(uri: str) -> ShowId: matcher = ShowId._PATTERN.search(uri) if matcher is not None: show_id = matcher.group(1) return ShowId( Utils.bytes_to_hex(ShowId._BASE62.decode(show_id, 16))) raise TypeError("Not a Spotify show ID: {}".format(uri))
def from_uri(uri: str) -> TrackId: search = TrackId._PATTERN.search(uri) if search is not None: track_id = search.group(1) return TrackId( Utils.bytes_to_hex(PlayableId.BASE62.decode(track_id, 16))) raise RuntimeError("Not a Spotify track ID: {}".format(uri))
def get_audio_url(self, file_id: bytes): resp = self._session.api().send( "GET", "/storage-resolve/files/audio/interactive/{}".format( Utils.bytes_to_hex(file_id)), None, None) if resp.status_code != 200: raise IOError(resp.status_code) body = resp.content if body is None: raise IOError("Response body is empty!") proto = StorageResolve.StorageResolveResponse() proto.ParseFromString(body) if proto.result == StorageResolve.StorageResolveResponse.Result.CDN: url = random.choice(proto.cdnurl) self._LOGGER.debug("Fetched CDN url for {}: {}".format( Utils.bytes_to_hex(file_id), url)) return url raise CdnManager.CdnException( "Could not retrieve CDN url! result: {}".format(proto.result))
def get_head(self, file_id: bytes): resp = self._session.client() \ .get(self._session.get_user_attribute("head-files-url", "https://heads-fa.spotify.com/head/{file_id}") .replace("{file_id}", Utils.bytes_to_hex(file_id))) if resp.status_code != 200: raise IOError("{}".format(resp.status_code)) body = resp.content if body is None: raise IOError("Response body is empty!") return body
def to_spotify_uri(self) -> str: return "spotify:show:{}".format( ShowId._BASE62.encode(Utils.hex_to_bytes(self._hexId)))
def from_base62(base62: str) -> ShowId: return ShowId(Utils.bytes_to_hex(ShowId._BASE62.decode(base62, 16)))
def to_spotify_uri(self) -> str: return "spotify:artist:{}".format( ArtistId._BASE62.encode(Utils.hex_to_bytes(self._hexId)))
def from_base62(base62: str) -> ArtistId: return ArtistId(Utils.bytes_to_hex(ArtistId._BASE62.decode(base62, 16)))
def dispatch(self, packet: Packet) -> None: payload = BytesInputStream(packet.payload) seq_length = payload.read_short() if seq_length == 2: seq = payload.read_short() elif seq_length == 4: seq = payload.read_int() elif seq_length == 8: seq = payload.read_long() else: raise RuntimeError("Unknown seq length: {}".format(seq_length)) flags = payload.read_byte() parts = payload.read_short() partial = self._partials.get(seq) if partial is None or flags == 0: partial = [] self._partials[seq] = partial self._LOGGER.debug( "Handling packet, cmd: 0x{}, seq: {}, flags: {}, parts: {}".format( Utils.bytes_to_hex(packet.cmd), seq, flags, parts)) for i in range(parts): size = payload.read_short() buffer = payload.read(size) partial.append(buffer) self._partials[seq] = partial if flags != b"\x01": return self._partials.pop(seq) header = Mercury.Header() header.ParseFromString(partial[0]) resp = MercuryClient.Response(header, partial) if packet.is_cmd(Packet.Type.mercury_event): dispatched = False with self._subscriptionsLock: for sub in self._subscriptions: if sub.matches(header.uri): sub.dispatch(resp) dispatched = True if not dispatched: self._LOGGER.debug( "Couldn't dispatch Mercury event seq: {}, uri: {}, code: {}, payload: {}" .format(seq, header.uri, header.status_code, resp.payload)) elif packet.is_cmd(Packet.Type.mercury_req) or \ packet.is_cmd(Packet.Type.mercury_sub) or \ packet.is_cmd(Packet.Type.mercury_sub): callback = self._callbacks.get(seq) self._callbacks.pop(seq) if callback is not None: callback.response(resp) else: self._LOGGER.warning( "Skipped Mercury response, seq: {}, uri: {}, code: {}". format(seq, resp.uri, resp.status_code)) with self._removeCallbackLock: self._removeCallbackLock.notify_all() else: self._LOGGER.warning( "Couldn't handle packet, seq: {}, uri: {}, code: {}".format( seq, header.uri, header.status_code))
def get_gid(self) -> bytes: return Utils.hex_to_bytes(self._hexId)
def from_base62(base62: str) -> TrackId: return TrackId(Utils.bytes_to_hex(PlayableId.BASE62.decode(base62, 16)))
def to_mercury_uri(self) -> str: return "spotify:album:{}".format( AlbumId._BASE62.encode(Utils.hex_to_bytes(self._hexId)))