diff --git a/librespot/__init__.py b/librespot/__init__.py index 9503e25..8f688cf 100644 --- a/librespot/__init__.py +++ b/librespot/__init__.py @@ -6,7 +6,7 @@ import platform class Version: - version_name = "0.0.9" + version_name = "0.0.10" @staticmethod def platform() -> Platform: diff --git a/librespot/audio/__init__.py b/librespot/audio/__init__.py index 3d10a6f..6f0f067 100644 --- a/librespot/audio/__init__.py +++ b/librespot/audio/__init__.py @@ -331,7 +331,7 @@ class CdnFeedHelper: session: Session, track: Metadata.Track, file: Metadata.AudioFile, resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str], preload: bool, - halt_listener: HaltListener) -> PlayableContentFeeder.LoadedStream: + halt_listener: HaltListener) -> LoadedStream: if type(resp_or_url) is str: url = resp_or_url else: @@ -345,18 +345,17 @@ class CdnFeedHelper: normalization_data = NormalizationData.read(input_stream) if input_stream.skip(0xA7) != 0xA7: raise IOError("Couldn't skip 0xa7 bytes!") - return PlayableContentFeeder.LoadedStream( + return LoadedStream( track, streamer, normalization_data, - PlayableContentFeeder.Metrics(file.file_id, preload, - -1 if preload else audio_key_time), + file.file_id, preload, audio_key_time ) @staticmethod def load_episode_external( session: Session, episode: Metadata.Episode, - halt_listener: HaltListener) -> PlayableContentFeeder.LoadedStream: + halt_listener: HaltListener) -> LoadedStream: resp = session.client().head(episode.external_url) if resp.status_code != 200: @@ -368,11 +367,11 @@ class CdnFeedHelper: streamer = session.cdn().stream_external_episode( episode, url, halt_listener) - return PlayableContentFeeder.LoadedStream( + return LoadedStream( episode, streamer, None, - PlayableContentFeeder.Metrics(None, False, -1), + None, False, -1 ) @staticmethod @@ -383,7 +382,7 @@ class CdnFeedHelper: resp_or_url: typing.Union[StorageResolve.StorageResolveResponse, str], preload: bool, halt_listener: HaltListener, - ) -> PlayableContentFeeder.LoadedStream: + ) -> LoadedStream: if type(resp_or_url) is str: url = resp_or_url else: @@ -397,12 +396,11 @@ class CdnFeedHelper: normalization_data = NormalizationData.read(input_stream) if input_stream.skip(0xA7) != 0xA7: raise IOError("Couldn't skip 0xa7 bytes!") - return PlayableContentFeeder.LoadedStream( + return LoadedStream( episode, streamer, normalization_data, - PlayableContentFeeder.Metrics(file.file_id, preload, - -1 if preload else audio_key_time), + file.file_id, preload, audio_key_time ) @@ -748,7 +746,9 @@ class PlayableContentFeeder: episode: Metadata.Episode, preload: bool, halt_lister: HaltListener): if track is None and episode is None: - raise RuntimeError() + 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: @@ -778,6 +778,7 @@ class PlayableContentFeeder: self.logger.fatal( "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) def load_track(self, track_id_or_track: typing.Union[TrackId, @@ -797,7 +798,7 @@ class PlayableContentFeeder: self.logger.fatal( "Couldn't find any suitable audio file, available: {}".format( track.file)) - raise FeederException() + raise FeederException("Cannot find suitable audio file") return self.load_stream(file, track, None, preload, halt_listener) def pick_alternative_if_necessary( @@ -848,29 +849,13 @@ class PlayableContentFeeder: storage_resolve_response.ParseFromString(body) return storage_resolve_response - class LoadedStream: - episode: Metadata.Episode - track: Metadata.Track - input_stream: GeneralAudioStream - normalization_data: NormalizationData - metrics: PlayableContentFeeder.Metrics - def __init__(self, track_or_episode: typing.Union[Metadata.Track, - Metadata.Episode], - input_stream: GeneralAudioStream, - normalization_data: typing.Union[NormalizationData, None], - metrics: PlayableContentFeeder.Metrics): - if type(track_or_episode) is Metadata.Track: - self.track = track_or_episode - self.episode = None - elif type(track_or_episode) is Metadata.Episode: - self.track = None - self.episode = track_or_episode - else: - raise TypeError() - self.input_stream = input_stream - self.normalization_data = normalization_data - self.metrics = metrics +class LoadedStream: + episode: Metadata.Episode + track: Metadata.Track + input_stream: GeneralAudioStream + normalization_data: NormalizationData + metrics: Metrics class Metrics: file_id: str @@ -878,13 +863,27 @@ class PlayableContentFeeder: audio_key_time: int def __init__(self, file_id: typing.Union[bytes, None], - preloaded_audio_key: bool, audio_key_time: int): + preloaded_audio_key: bool, audio_key_time: int): self.file_id = None if file_id is None else util.bytes_to_hex( file_id) self.preloaded_audio_key = preloaded_audio_key - self.audio_key_time = audio_key_time - if preloaded_audio_key and audio_key_time != -1: - raise RuntimeError() + self.audio_key_time = -1 if preloaded_audio_key else audio_key_time + + def __init__(self, track_or_episode: typing.Union[Metadata.Track, Metadata.Episode], + input_stream: GeneralAudioStream, + normalization_data: typing.Union[NormalizationData, None], + file_id: str, preloaded_audio_key: bool, audio_key_time: int): + if type(track_or_episode) is Metadata.Track: + self.track = track_or_episode + self.episode = None + elif type(track_or_episode) is Metadata.Episode: + self.track = None + self.episode = track_or_episode + else: + raise TypeError() + self.input_stream = input_stream + self.normalization_data = normalization_data + self.metrics = self.Metrics(file_id, preloaded_audio_key, audio_key_time) class StreamId: diff --git a/librespot/core.py b/librespot/core.py index c0cf55f..46df5f9 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -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: diff --git a/librespot/proto/EntityExtensionData_pb2.py b/librespot/proto/EntityExtensionData_pb2.py new file mode 100644 index 0000000..37d72dc --- /dev/null +++ b/librespot/proto/EntityExtensionData_pb2.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: entity_extension_data.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x65ntity_extension_data.proto\x12\x18spotify.extendedmetadata\x1a\x19google/protobuf/any.proto\"\x8c\x01\n\x19\x45ntityExtensionDataHeader\x12\x13\n\x0bstatus_code\x18\x01 \x01(\x05\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\x12\x0e\n\x06locale\x18\x03 \x01(\t\x12\x1c\n\x14\x63\x61\x63he_ttl_in_seconds\x18\x04 \x01(\x03\x12\x1e\n\x16offline_ttl_in_seconds\x18\x05 \x01(\x03\"\x9c\x01\n\x13\x45ntityExtensionData\x12\x43\n\x06header\x18\x01 \x01(\x0b\x32\x33.spotify.extendedmetadata.EntityExtensionDataHeader\x12\x12\n\nentity_uri\x18\x02 \x01(\t\x12,\n\x0e\x65xtension_data\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\"$\n\x0ePlainListAssoc\x12\x12\n\nentity_uri\x18\x01 \x03(\t\"\r\n\x0b\x41ssocHeader\"|\n\x05\x41ssoc\x12\x35\n\x06header\x18\x01 \x01(\x0b\x32%.spotify.extendedmetadata.AssocHeader\x12<\n\nplain_list\x18\x02 \x01(\x0b\x32(.spotify.extendedmetadata.PlainListAssocB+\n\"com.spotify.extendedmetadata.protoH\x02P\x01\xf8\x01\x01\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'EntityExtensionData_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\"com.spotify.extendedmetadata.protoH\002P\001\370\001\001' + _ENTITYEXTENSIONDATAHEADER._serialized_start=85 + _ENTITYEXTENSIONDATAHEADER._serialized_end=225 + _ENTITYEXTENSIONDATA._serialized_start=228 + _ENTITYEXTENSIONDATA._serialized_end=384 + _PLAINLISTASSOC._serialized_start=386 + _PLAINLISTASSOC._serialized_end=422 + _ASSOCHEADER._serialized_start=424 + _ASSOCHEADER._serialized_end=437 + _ASSOC._serialized_start=439 + _ASSOC._serialized_end=563 +# @@protoc_insertion_point(module_scope) diff --git a/librespot/proto/ExtendedMetadata_pb2.py b/librespot/proto/ExtendedMetadata_pb2.py new file mode 100644 index 0000000..3116a3f --- /dev/null +++ b/librespot/proto/ExtendedMetadata_pb2.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: extended_metadata.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import librespot.proto.ExtensionKind_pb2 as extension__kind__pb2 +import librespot.proto.EntityExtensionData_pb2 as entity__extension__data__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x65xtended_metadata.proto\x12\x18spotify.extendedmetadata\x1a\x14\x65xtension_kind.proto\x1a\x1b\x65ntity_extension_data.proto\"_\n\x0e\x45xtensionQuery\x12?\n\x0e\x65xtension_kind\x18\x01 \x01(\x0e\x32\'.spotify.extendedmetadata.ExtensionKind\x12\x0c\n\x04\x65tag\x18\x02 \x01(\t\"\\\n\rEntityRequest\x12\x12\n\nentity_uri\x18\x01 \x01(\t\x12\x37\n\x05query\x18\x02 \x03(\x0b\x32(.spotify.extendedmetadata.ExtensionQuery\"Q\n\x1a\x42\x61tchedEntityRequestHeader\x12\x0f\n\x07\x63ountry\x18\x01 \x01(\t\x12\x11\n\tcatalogue\x18\x02 \x01(\t\x12\x0f\n\x07task_id\x18\x03 \x01(\x0c\"\x9d\x01\n\x14\x42\x61tchedEntityRequest\x12\x44\n\x06header\x18\x01 \x01(\x0b\x32\x34.spotify.extendedmetadata.BatchedEntityRequestHeader\x12?\n\x0e\x65ntity_request\x18\x02 \x03(\x0b\x32\'.spotify.extendedmetadata.EntityRequest\"\xbe\x01\n\x1e\x45ntityExtensionDataArrayHeader\x12\x1d\n\x15provider_error_status\x18\x01 \x01(\x05\x12\x1c\n\x14\x63\x61\x63he_ttl_in_seconds\x18\x02 \x01(\x03\x12\x1e\n\x16offline_ttl_in_seconds\x18\x03 \x01(\x03\x12?\n\x0e\x65xtension_type\x18\x04 \x01(\x0e\x32\'.spotify.extendedmetadata.ExtensionType\"\xec\x01\n\x18\x45ntityExtensionDataArray\x12H\n\x06header\x18\x01 \x01(\x0b\x32\x38.spotify.extendedmetadata.EntityExtensionDataArrayHeader\x12?\n\x0e\x65xtension_kind\x18\x02 \x01(\x0e\x32\'.spotify.extendedmetadata.ExtensionKind\x12\x45\n\x0e\x65xtension_data\x18\x03 \x03(\x0b\x32-.spotify.extendedmetadata.EntityExtensionData\" \n\x1e\x42\x61tchedExtensionResponseHeader\"\xb3\x01\n\x18\x42\x61tchedExtensionResponse\x12H\n\x06header\x18\x01 \x01(\x0b\x32\x38.spotify.extendedmetadata.BatchedExtensionResponseHeader\x12M\n\x11\x65xtended_metadata\x18\x02 \x03(\x0b\x32\x32.spotify.extendedmetadata.EntityExtensionDataArray*4\n\rExtensionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07GENERIC\x10\x01\x12\t\n\x05\x41SSOC\x10\x02\x42+\n\"com.spotify.extendedmetadata.protoH\x02P\x01\xf8\x01\x01\x62\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ExtendedMetadata_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\"com.spotify.extendedmetadata.protoH\002P\001\370\001\001' + _EXTENSIONTYPE._serialized_start=1186 + _EXTENSIONTYPE._serialized_end=1238 + _EXTENSIONQUERY._serialized_start=104 + _EXTENSIONQUERY._serialized_end=199 + _ENTITYREQUEST._serialized_start=201 + _ENTITYREQUEST._serialized_end=293 + _BATCHEDENTITYREQUESTHEADER._serialized_start=295 + _BATCHEDENTITYREQUESTHEADER._serialized_end=376 + _BATCHEDENTITYREQUEST._serialized_start=379 + _BATCHEDENTITYREQUEST._serialized_end=536 + _ENTITYEXTENSIONDATAARRAYHEADER._serialized_start=539 + _ENTITYEXTENSIONDATAARRAYHEADER._serialized_end=729 + _ENTITYEXTENSIONDATAARRAY._serialized_start=732 + _ENTITYEXTENSIONDATAARRAY._serialized_end=968 + _BATCHEDEXTENSIONRESPONSEHEADER._serialized_start=970 + _BATCHEDEXTENSIONRESPONSEHEADER._serialized_end=1002 + _BATCHEDEXTENSIONRESPONSE._serialized_start=1005 + _BATCHEDEXTENSIONRESPONSE._serialized_end=1184 +# @@protoc_insertion_point(module_scope) diff --git a/librespot/proto/ExtensionKind_pb2.py b/librespot/proto/ExtensionKind_pb2.py new file mode 100644 index 0000000..44d2906 --- /dev/null +++ b/librespot/proto/ExtensionKind_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: extension_kind.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x65xtension_kind.proto\x12\x18spotify.extendedmetadata*\xbb#\n\rExtensionKind\x12\x15\n\x11UNKNOWN_EXTENSION\x10\x00\x12\n\n\x06\x43\x41NVAZ\x10\x01\x12\x0e\n\nSTORYLINES\x10\x02\x12\x12\n\x0ePODCAST_TOPICS\x10\x03\x12\x14\n\x10PODCAST_SEGMENTS\x10\x04\x12\x0f\n\x0b\x41UDIO_FILES\x10\x05\x12\x14\n\x10TRACK_DESCRIPTOR\x10\x06\x12\x13\n\x0fPODCAST_COUNTER\x10\x07\x12\r\n\tARTIST_V4\x10\x08\x12\x0c\n\x08\x41LBUM_V4\x10\t\x12\x0c\n\x08TRACK_V4\x10\n\x12\x0b\n\x07SHOW_V4\x10\x0b\x12\x0e\n\nEPISODE_V4\x10\x0c\x12\x1c\n\x18PODCAST_HTML_DESCRIPTION\x10\r\x12\x12\n\x0ePODCAST_QUOTES\x10\x0e\x12\x10\n\x0cUSER_PROFILE\x10\x0f\x12\r\n\tCANVAS_V1\x10\x10\x12\x10\n\x0cSHOW_V4_BASE\x10\x11\x12\x1a\n\x16SHOW_V4_EPISODES_ASSOC\x10\x12\x12\x1f\n\x1bTRACK_DESCRIPTOR_SIGNATURES\x10\x13\x12\x17\n\x13PODCAST_AD_SEGMENTS\x10\x14\x12\x17\n\x13\x45PISODE_TRANSCRIPTS\x10\x15\x12\x19\n\x15PODCAST_SUBSCRIPTIONS\x10\x16\x12\x13\n\x0f\x45XTRACTED_COLOR\x10\x17\x12\x14\n\x10PODCAST_VIRALITY\x10\x18\x12\x17\n\x13IMAGE_SPARKLES_HACK\x10\x19\x12\x1b\n\x17PODCAST_POPULARITY_HACK\x10\x1a\x12\x10\n\x0c\x41UTOMIX_MODE\x10\x1b\x12\r\n\tCUEPOINTS\x10\x1c\x12\x10\n\x0cPODCAST_POLL\x10\x1d\x12\x12\n\x0e\x45PISODE_ACCESS\x10\x1e\x12\x0f\n\x0bSHOW_ACCESS\x10\x1f\x12\x0f\n\x0bPODCAST_QNA\x10 \x12\t\n\x05\x43LIPS\x10!\x12\x0b\n\x07SHOW_V5\x10\"\x12\x0e\n\nEPISODE_V5\x10#\x12\x15\n\x11PODCAST_CTA_CARDS\x10$\x12\x12\n\x0ePODCAST_RATING\x10%\x12\x14\n\x10\x44ISPLAY_SEGMENTS\x10&\x12\r\n\tGREENROOM\x10\'\x12\x10\n\x0cUSER_CREATED\x10(\x12\x14\n\x10SHOW_DESCRIPTION\x10)\x12\x19\n\x15SHOW_HTML_DESCRIPTION\x10*\x12\x14\n\x10SHOW_PLAYABILITY\x10+\x12\x17\n\x13\x45PISODE_DESCRIPTION\x10,\x12\x1c\n\x18\x45PISODE_HTML_DESCRIPTION\x10-\x12\x17\n\x13\x45PISODE_PLAYABILITY\x10.\x12\x17\n\x13SHOW_EPISODES_ASSOC\x10/\x12\x11\n\rCLIENT_CONFIG\x10\x30\x12\x13\n\x0fPLAYLISTABILITY\x10\x31\x12\x10\n\x0c\x41UDIOBOOK_V5\x10\x32\x12\x0e\n\nCHAPTER_V5\x10\x33\x12\x17\n\x13\x41UDIOBOOK_SPECIFICS\x10\x34\x12\x13\n\x0f\x45PISODE_RANKING\x10\x35\x12\x14\n\x10HTML_DESCRIPTION\x10\x36\x12\x13\n\x0f\x43REATOR_CHANNEL\x10\x37\x12\x17\n\x13\x41UDIOBOOK_PROVIDERS\x10\x38\x12\x0e\n\nPLAY_TRAIT\x10\x39\x12\x13\n\x0f\x43ONTENT_WARNING\x10:\x12\r\n\tIMAGE_CUE\x10;\x12\x10\n\x0cSTREAM_COUNT\x10<\x12\x14\n\x10\x41UDIO_ATTRIBUTES\x10=\x12\x13\n\x0fNAVIGABLE_TRAIT\x10>\x12\x15\n\x11NEXT_BEST_EPISODE\x10?\x12\x13\n\x0f\x41UDIOBOOK_PRICE\x10@\x12\x18\n\x14\x45XPRESSIVE_PLAYLISTS\x10\x41\x12\x18\n\x14\x44YNAMIC_SHOW_EPISODE\x10\x42\x12\x08\n\x04LIVE\x10\x43\x12\x0f\n\x0bSKIP_PLAYED\x10\x44\x12\x1a\n\x16\x41\x44_BREAK_FREE_PODCASTS\x10\x45\x12\x10\n\x0c\x41SSOCIATIONS\x10\x46\x12\x17\n\x13PLAYLIST_EVALUATION\x10G\x12\x17\n\x13\x43\x41\x43HE_INVALIDATIONS\x10H\x12\x15\n\x11LIVESTREAM_ENTITY\x10I\x12\x18\n\x14SINGLE_TAP_REACTIONS\x10J\x12\x11\n\rUSER_COMMENTS\x10K\x12\x17\n\x13\x43LIENT_RESTRICTIONS\x10L\x12\x11\n\rPODCAST_GUEST\x10M\x12\x0f\n\x0bPLAYABILITY\x10N\x12\x0f\n\x0b\x43OVER_IMAGE\x10O\x12\x0f\n\x0bSHARE_TRAIT\x10P\x12\x14\n\x10INSTANCE_SHARING\x10Q\x12\x0f\n\x0b\x41RTIST_TOUR\x10R\x12\x13\n\x0f\x41UDIOBOOK_GENRE\x10S\x12\x0b\n\x07\x43ONCEPT\x10T\x12\x12\n\x0eORIGINAL_VIDEO\x10U\x12\x11\n\rSMART_SHUFFLE\x10V\x12\x0f\n\x0bLIVE_EVENTS\x10W\x12\x17\n\x13\x41UDIOBOOK_RELATIONS\x10X\x12\x15\n\x11HOME_POC_BASECARD\x10Y\x12\x19\n\x15\x41UDIOBOOK_SUPPLEMENTS\x10Z\x12\x17\n\x13PAID_PODCAST_BANNER\x10[\x12\r\n\tFEWER_ADS\x10\\\x12\x1c\n\x18WATCH_FEED_SHOW_EXPLORER\x10]\x12\x1b\n\x17TRACK_EXTRA_DESCRIPTORS\x10^\x12 \n\x1cTRACK_EXTRA_AUDIO_ATTRIBUTES\x10_\x12\x1a\n\x16TRACK_EXTENDED_CREDITS\x10`\x12\x10\n\x0cSIMPLE_TRAIT\x10\x61\x12\x16\n\x12\x41UDIO_ASSOCIATIONS\x10\x62\x12\x16\n\x12VIDEO_ASSOCIATIONS\x10\x63\x12\x12\n\x0ePLAYLIST_TUNER\x10\x64\x12\x1c\n\x18\x41RTIST_VIDEOS_ENTRYPOINT\x10\x65\x12\x14\n\x10\x41LBUM_PRERELEASE\x10\x66\x12\x18\n\x14\x43ONTENT_ALTERNATIVES\x10g\x12\x14\n\x10SNAPSHOT_SHARING\x10i\x12\x1a\n\x16\x44ISPLAY_SEGMENTS_COUNT\x10j\x12\x1c\n\x18PODCAST_FEATURED_EPISODE\x10k\x12\x1d\n\x19PODCAST_SPONSORED_CONTENT\x10l\x12\x1e\n\x1aPODCAST_EPISODE_TOPICS_LLM\x10m\x12\x1d\n\x19PODCAST_EPISODE_TOPICS_KG\x10n\x12\x1e\n\x1a\x45PISODE_RANKING_POPULARITY\x10o\x12\t\n\x05MERCH\x10p\x12\x15\n\x11\x43OMPANION_CONTENT\x10q\x12\x1e\n\x1aWATCH_FEED_ENTITY_EXPLORER\x10r\x12\x15\n\x11\x41NCHOR_CARD_TRAIT\x10s\x12 \n\x1c\x41UDIO_PREVIEW_PLAYBACK_TRAIT\x10t\x12\x1d\n\x19VIDEO_PREVIEW_STILL_TRAIT\x10u\x12\x16\n\x12PREVIEW_CARD_TRAIT\x10v\x12\x18\n\x14SHORTCUTS_CARD_TRAIT\x10w\x12 \n\x1cVIDEO_PREVIEW_PLAYBACK_TRAIT\x10x\x12\x14\n\x10\x43OURSE_SPECIFICS\x10y\x12\x0b\n\x07\x43ONCERT\x10z\x12\x14\n\x10\x43ONCERT_LOCATION\x10{\x12\x15\n\x11\x43ONCERT_MARKETING\x10|\x12\x16\n\x12\x43ONCERT_PERFORMERS\x10}\x12\x19\n\x15TRACK_PAIR_TRANSITION\x10~\x12\x16\n\x12\x43ONTENT_TYPE_TRAIT\x10\x7f\x12\x0f\n\nNAME_TRAIT\x10\x80\x01\x12\x12\n\rARTWORK_TRAIT\x10\x81\x01\x12\x17\n\x12RELEASE_DATE_TRAIT\x10\x82\x01\x12\x12\n\rCREDITS_TRAIT\x10\x83\x01\x12\x16\n\x11RELEASE_URI_TRAIT\x10\x84\x01\x12\x13\n\x0e\x45NTITY_CAPPING\x10\x85\x01\x12\x15\n\x10LESSON_SPECIFICS\x10\x86\x01\x12\x13\n\x0e\x43ONCERT_OFFERS\x10\x87\x01\x12\x14\n\x0fTRANSITION_MAPS\x10\x88\x01\x12\x18\n\x13\x41RTIST_HAS_CONCERTS\x10\x89\x01\x12\x0f\n\nPRERELEASE\x10\x8a\x01\x12\x1b\n\x16PLAYLIST_ATTRIBUTES_V2\x10\x8b\x01\x12\x17\n\x12LIST_ATTRIBUTES_V2\x10\x8c\x01\x12\x12\n\rLIST_METADATA\x10\x8d\x01\x12\x1e\n\x19LIST_TUNER_AUDIO_ANALYSIS\x10\x8e\x01\x12\x19\n\x14LIST_TUNER_CUEPOINTS\x10\x8f\x01\x12\x19\n\x14\x43ONTENT_RATING_TRAIT\x10\x90\x01\x12\x14\n\x0f\x43OPYRIGHT_TRAIT\x10\x91\x01\x12\x15\n\x10SUPPORTED_BADGES\x10\x92\x01\x12\x0b\n\x06\x42\x41\x44GES\x10\x93\x01\x12\x12\n\rPREVIEW_TRAIT\x10\x94\x01\x12\x1a\n\x15ROOTLISTABILITY_TRAIT\x10\x95\x01\x12\x13\n\x0eLOCAL_CONCERTS\x10\x96\x01\x12\x1a\n\x15RECOMMENDED_PLAYLISTS\x10\x97\x01\x12\x15\n\x10POPULAR_RELEASES\x10\x98\x01\x12\x15\n\x10RELATED_RELEASES\x10\x99\x01\x12\x17\n\x12SHARE_RESTRICTIONS\x10\x9a\x01\x12\x12\n\rCONCERT_OFFER\x10\x9b\x01\x12\x1b\n\x16\x43ONCERT_OFFER_PROVIDER\x10\x9c\x01\x12\x15\n\x10\x45NTITY_BOOKMARKS\x10\x9d\x01\x12\x12\n\rPRIVACY_TRAIT\x10\x9e\x01\x12\x1a\n\x15\x44UPLICATE_ITEMS_TRAIT\x10\x9f\x01\x12\x15\n\x10REORDERING_TRAIT\x10\xa0\x01\x12 \n\x1bPODCAST_RESUMPTION_SEGMENTS\x10\xa1\x01\x12\x1c\n\x17\x41RTIST_EXPRESSION_VIDEO\x10\xa2\x01\x12\x15\n\x10PRERELEASE_VIDEO\x10\xa3\x01\x12\x1b\n\x16GATED_ENTITY_RELATIONS\x10\xa4\x01\x12\x1d\n\x18RELATED_CREATORS_SECTION\x10\xa5\x01\x12 \n\x1b\x43REATORS_APPEARS_ON_SECTION\x10\xa6\x01\x12\x13\n\x0ePROMO_V1_TRAIT\x10\xa7\x01\x12\x1a\n\x15SPEECHLESS_SHARE_CARD\x10\xa8\x01\x12\x1a\n\x15TOP_PLAYABLES_SECTION\x10\xa9\x01\x12\x0e\n\tAUTO_LENS\x10\xaa\x01\x12\x13\n\x0ePROMO_V3_TRAIT\x10\xab\x01\x12\x19\n\x14TRACK_CONTENT_FILTER\x10\xac\x01\x12\x15\n\x10HIGHLIGHTABILITY\x10\xad\x01\x12\x1f\n\x1aLINK_CARD_WITH_IMAGE_TRAIT\x10\xae\x01\x12\x18\n\x13TRACK_CLOUD_SECTION\x10\xaf\x01\x12\x13\n\x0e\x45PISODE_TOPICS\x10\xb0\x01\x12\x14\n\x0fVIDEO_THUMBNAIL\x10\xb1\x01\x12\x13\n\x0eIDENTITY_TRAIT\x10\xb2\x01\x12\x1a\n\x15VISUAL_IDENTITY_TRAIT\x10\xb3\x01\x12\x1a\n\x15\x43ONTENT_TYPE_V2_TRAIT\x10\xb4\x01\x12\x1b\n\x16PREVIEW_PLAYBACK_TRAIT\x10\xb5\x01\x12!\n\x1c\x43ONSUMPTION_EXPERIENCE_TRAIT\x10\xb6\x01\x12\x1e\n\x19PUBLISHING_METADATA_TRAIT\x10\xb7\x01\x12\x1e\n\x19\x44\x45TAILED_EVALUATION_TRAIT\x10\xb8\x01\x12!\n\x1cON_PLATFORM_REPUTATION_TRAIT\x10\xb9\x01\x12\x15\n\x10\x43REDITS_V2_TRAIT\x10\xba\x01\x12 \n\x1bHIGHLIGHT_PLAYABILITY_TRAIT\x10\xbb\x01\x12\x16\n\x11SHOW_EPISODE_LIST\x10\xbc\x01\x12\x17\n\x12\x41VAILABLE_RELEASES\x10\xbd\x01\x12\x19\n\x14PLAYLIST_DESCRIPTORS\x10\xbe\x01\x12$\n\x1fLINK_CARD_WITH_ANIMATIONS_TRAIT\x10\xbf\x01\x12\n\n\x05RECAP\x10\xc0\x01\x12 \n\x1b\x41UDIOBOOK_COMPANION_CONTENT\x10\xc1\x01\x12\x1e\n\x19THREE_OH_THREE_PLAY_TRAIT\x10\xc2\x01\x12\x1e\n\x19\x41RTIST_WRAPPED_2024_VIDEO\x10\xc3\x01\x42\x41\n\"com.spotify.extendedmetadata.protoH\x02P\x01\xf8\x01\x01\xa2\x02\x13SPTExtendedMetadatab\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ExtensionKind_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\"com.spotify.extendedmetadata.protoH\002P\001\370\001\001\242\002\023SPTExtendedMetadata' + _EXTENSIONKIND._serialized_start=51 + _EXTENSIONKIND._serialized_end=4590 +# @@protoc_insertion_point(module_scope) diff --git a/proto/entity_extension_data.proto b/proto/entity_extension_data.proto new file mode 100644 index 0000000..589ee68 --- /dev/null +++ b/proto/entity_extension_data.proto @@ -0,0 +1,38 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + +syntax = "proto3"; + +package spotify.extendedmetadata; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.extendedmetadata.proto"; + +message EntityExtensionDataHeader { + int32 status_code = 1; + string etag = 2; + string locale = 3; + int64 cache_ttl_in_seconds = 4; + int64 offline_ttl_in_seconds = 5; +} + +message EntityExtensionData { + EntityExtensionDataHeader header = 1; + string entity_uri = 2; + google.protobuf.Any extension_data = 3; +} + +message PlainListAssoc { + repeated string entity_uri = 1; +} + +message AssocHeader { +} + +message Assoc { + AssocHeader header = 1; + PlainListAssoc plain_list = 2; +} diff --git a/proto/extended_metadata.proto b/proto/extended_metadata.proto new file mode 100644 index 0000000..c4cefa5 --- /dev/null +++ b/proto/extended_metadata.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package spotify.extendedmetadata; + +import "extension_kind.proto"; +import "entity_extension_data.proto"; + +option cc_enable_arenas = true; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.extendedmetadata.proto"; + +message ExtensionQuery { + ExtensionKind extension_kind = 1; + string etag = 2; +} + +message EntityRequest { + string entity_uri = 1; + repeated ExtensionQuery query = 2; +} + +message BatchedEntityRequestHeader { + string country = 1; + string catalogue = 2; + bytes task_id = 3; +} + +message BatchedEntityRequest { + BatchedEntityRequestHeader header = 1; + repeated EntityRequest entity_request = 2; +} + +message EntityExtensionDataArrayHeader { + int32 provider_error_status = 1; + int64 cache_ttl_in_seconds = 2; + int64 offline_ttl_in_seconds = 3; + ExtensionType extension_type = 4; +} + +message EntityExtensionDataArray { + EntityExtensionDataArrayHeader header = 1; + ExtensionKind extension_kind = 2; + repeated EntityExtensionData extension_data = 3; +} + +message BatchedExtensionResponseHeader { +} + +message BatchedExtensionResponse { + BatchedExtensionResponseHeader header = 1; + repeated EntityExtensionDataArray extended_metadata = 2; +} + +enum ExtensionType { + UNKNOWN = 0; + GENERIC = 1; + ASSOC = 2; +} \ No newline at end of file diff --git a/proto/extension_kind.proto b/proto/extension_kind.proto new file mode 100644 index 0000000..bb66295 --- /dev/null +++ b/proto/extension_kind.proto @@ -0,0 +1,209 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + +syntax = "proto3"; + +package spotify.extendedmetadata; + +option objc_class_prefix = "SPTExtendedMetadata"; +option cc_enable_arenas = true; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.extendedmetadata.proto"; + +enum ExtensionKind { + UNKNOWN_EXTENSION = 0; + CANVAZ = 1; + STORYLINES = 2; + PODCAST_TOPICS = 3; + PODCAST_SEGMENTS = 4; + AUDIO_FILES = 5; + TRACK_DESCRIPTOR = 6; + PODCAST_COUNTER = 7; + ARTIST_V4 = 8; + ALBUM_V4 = 9; + TRACK_V4 = 10; + SHOW_V4 = 11; + EPISODE_V4 = 12; + PODCAST_HTML_DESCRIPTION = 13; + PODCAST_QUOTES = 14; + USER_PROFILE = 15; + CANVAS_V1 = 16; + SHOW_V4_BASE = 17; + SHOW_V4_EPISODES_ASSOC = 18; + TRACK_DESCRIPTOR_SIGNATURES = 19; + PODCAST_AD_SEGMENTS = 20; + EPISODE_TRANSCRIPTS = 21; + PODCAST_SUBSCRIPTIONS = 22; + EXTRACTED_COLOR = 23; + PODCAST_VIRALITY = 24; + IMAGE_SPARKLES_HACK = 25; + PODCAST_POPULARITY_HACK = 26; + AUTOMIX_MODE = 27; + CUEPOINTS = 28; + PODCAST_POLL = 29; + EPISODE_ACCESS = 30; + SHOW_ACCESS = 31; + PODCAST_QNA = 32; + CLIPS = 33; + SHOW_V5 = 34; + EPISODE_V5 = 35; + PODCAST_CTA_CARDS = 36; + PODCAST_RATING = 37; + DISPLAY_SEGMENTS = 38; + GREENROOM = 39; + USER_CREATED = 40; + SHOW_DESCRIPTION = 41; + SHOW_HTML_DESCRIPTION = 42; + SHOW_PLAYABILITY = 43; + EPISODE_DESCRIPTION = 44; + EPISODE_HTML_DESCRIPTION = 45; + EPISODE_PLAYABILITY = 46; + SHOW_EPISODES_ASSOC = 47; + CLIENT_CONFIG = 48; + PLAYLISTABILITY = 49; + AUDIOBOOK_V5 = 50; + CHAPTER_V5 = 51; + AUDIOBOOK_SPECIFICS = 52; + EPISODE_RANKING = 53; + HTML_DESCRIPTION = 54; + CREATOR_CHANNEL = 55; + AUDIOBOOK_PROVIDERS = 56; + PLAY_TRAIT = 57; + CONTENT_WARNING = 58; + IMAGE_CUE = 59; + STREAM_COUNT = 60; + AUDIO_ATTRIBUTES = 61; + NAVIGABLE_TRAIT = 62; + NEXT_BEST_EPISODE = 63; + AUDIOBOOK_PRICE = 64; + EXPRESSIVE_PLAYLISTS = 65; + DYNAMIC_SHOW_EPISODE = 66; + LIVE = 67; + SKIP_PLAYED = 68; + AD_BREAK_FREE_PODCASTS = 69; + ASSOCIATIONS = 70; + PLAYLIST_EVALUATION = 71; + CACHE_INVALIDATIONS = 72; + LIVESTREAM_ENTITY = 73; + SINGLE_TAP_REACTIONS = 74; + USER_COMMENTS = 75; + CLIENT_RESTRICTIONS = 76; + PODCAST_GUEST = 77; + PLAYABILITY = 78; + COVER_IMAGE = 79; + SHARE_TRAIT = 80; + INSTANCE_SHARING = 81; + ARTIST_TOUR = 82; + AUDIOBOOK_GENRE = 83; + CONCEPT = 84; + ORIGINAL_VIDEO = 85; + SMART_SHUFFLE = 86; + LIVE_EVENTS = 87; + AUDIOBOOK_RELATIONS = 88; + HOME_POC_BASECARD = 89; + AUDIOBOOK_SUPPLEMENTS = 90; + PAID_PODCAST_BANNER = 91; + FEWER_ADS = 92; + WATCH_FEED_SHOW_EXPLORER = 93; + TRACK_EXTRA_DESCRIPTORS = 94; + TRACK_EXTRA_AUDIO_ATTRIBUTES = 95; + TRACK_EXTENDED_CREDITS = 96; + SIMPLE_TRAIT = 97; + AUDIO_ASSOCIATIONS = 98; + VIDEO_ASSOCIATIONS = 99; + PLAYLIST_TUNER = 100; + ARTIST_VIDEOS_ENTRYPOINT = 101; + ALBUM_PRERELEASE = 102; + CONTENT_ALTERNATIVES = 103; + SNAPSHOT_SHARING = 105; + DISPLAY_SEGMENTS_COUNT = 106; + PODCAST_FEATURED_EPISODE = 107; + PODCAST_SPONSORED_CONTENT = 108; + PODCAST_EPISODE_TOPICS_LLM = 109; + PODCAST_EPISODE_TOPICS_KG = 110; + EPISODE_RANKING_POPULARITY = 111; + MERCH = 112; + COMPANION_CONTENT = 113; + WATCH_FEED_ENTITY_EXPLORER = 114; + ANCHOR_CARD_TRAIT = 115; + AUDIO_PREVIEW_PLAYBACK_TRAIT = 116; + VIDEO_PREVIEW_STILL_TRAIT = 117; + PREVIEW_CARD_TRAIT = 118; + SHORTCUTS_CARD_TRAIT = 119; + VIDEO_PREVIEW_PLAYBACK_TRAIT = 120; + COURSE_SPECIFICS = 121; + CONCERT = 122; + CONCERT_LOCATION = 123; + CONCERT_MARKETING = 124; + CONCERT_PERFORMERS = 125; + TRACK_PAIR_TRANSITION = 126; + CONTENT_TYPE_TRAIT = 127; + NAME_TRAIT = 128; + ARTWORK_TRAIT = 129; + RELEASE_DATE_TRAIT = 130; + CREDITS_TRAIT = 131; + RELEASE_URI_TRAIT = 132; + ENTITY_CAPPING = 133; + LESSON_SPECIFICS = 134; + CONCERT_OFFERS = 135; + TRANSITION_MAPS = 136; + ARTIST_HAS_CONCERTS = 137; + PRERELEASE = 138; + PLAYLIST_ATTRIBUTES_V2 = 139; + LIST_ATTRIBUTES_V2 = 140; + LIST_METADATA = 141; + LIST_TUNER_AUDIO_ANALYSIS = 142; + LIST_TUNER_CUEPOINTS = 143; + CONTENT_RATING_TRAIT = 144; + COPYRIGHT_TRAIT = 145; + SUPPORTED_BADGES = 146; + BADGES = 147; + PREVIEW_TRAIT = 148; + ROOTLISTABILITY_TRAIT = 149; + LOCAL_CONCERTS = 150; + RECOMMENDED_PLAYLISTS = 151; + POPULAR_RELEASES = 152; + RELATED_RELEASES = 153; + SHARE_RESTRICTIONS = 154; + CONCERT_OFFER = 155; + CONCERT_OFFER_PROVIDER = 156; + ENTITY_BOOKMARKS = 157; + PRIVACY_TRAIT = 158; + DUPLICATE_ITEMS_TRAIT = 159; + REORDERING_TRAIT = 160; + PODCAST_RESUMPTION_SEGMENTS = 161; + ARTIST_EXPRESSION_VIDEO = 162; + PRERELEASE_VIDEO = 163; + GATED_ENTITY_RELATIONS = 164; + RELATED_CREATORS_SECTION = 165; + CREATORS_APPEARS_ON_SECTION = 166; + PROMO_V1_TRAIT = 167; + SPEECHLESS_SHARE_CARD = 168; + TOP_PLAYABLES_SECTION = 169; + AUTO_LENS = 170; + PROMO_V3_TRAIT = 171; + TRACK_CONTENT_FILTER = 172; + HIGHLIGHTABILITY = 173; + LINK_CARD_WITH_IMAGE_TRAIT = 174; + TRACK_CLOUD_SECTION = 175; + EPISODE_TOPICS = 176; + VIDEO_THUMBNAIL = 177; + IDENTITY_TRAIT = 178; + VISUAL_IDENTITY_TRAIT = 179; + CONTENT_TYPE_V2_TRAIT = 180; + PREVIEW_PLAYBACK_TRAIT = 181; + CONSUMPTION_EXPERIENCE_TRAIT = 182; + PUBLISHING_METADATA_TRAIT = 183; + DETAILED_EVALUATION_TRAIT = 184; + ON_PLATFORM_REPUTATION_TRAIT = 185; + CREDITS_V2_TRAIT = 186; + HIGHLIGHT_PLAYABILITY_TRAIT = 187; + SHOW_EPISODE_LIST = 188; + AVAILABLE_RELEASES = 189; + PLAYLIST_DESCRIPTORS = 190; + LINK_CARD_WITH_ANIMATIONS_TRAIT = 191; + RECAP = 192; + AUDIOBOOK_COMPANION_CONTENT = 193; + THREE_OH_THREE_PLAY_TRAIT = 194; + ARTIST_WRAPPED_2024_VIDEO = 195; +} diff --git a/setup.py b/setup.py index a361b4f..1f3c5b5 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import setuptools setuptools.setup(name="librespot", - version="0.0.9", + version="0.0.10", description="Open Source Spotify Client", long_description=open("README.md").read(), long_description_content_type="text/markdown",