have get_metadata_4_track() call new get_ext_metadata()

TODO: parsing a successful response
always returns a 400 response
This commit is contained in:
Googolplexed0
2025-11-09 03:48:48 -06:00
parent 8760279c64
commit 9481c1c841
7 changed files with 424 additions and 3 deletions

View File

@@ -57,6 +57,8 @@ from librespot.proto import Connectivity_pb2 as Connectivity
from librespot.proto import Keyexchange_pb2 as Keyexchange from librespot.proto import Keyexchange_pb2 as Keyexchange
from librespot.proto import Metadata_pb2 as Metadata from librespot.proto import Metadata_pb2 as Metadata
from librespot.proto import Playlist4External_pb2 as Playlist4External from librespot.proto import Playlist4External_pb2 as Playlist4External
from librespot.proto.ExtendedMetadata_pb2 import EntityRequest, BatchedEntityRequest, ExtensionQuery
from librespot.proto.ExtensionKind_pb2 import ExtensionKind
from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate from librespot.proto.ExplicitContentPubsub_pb2 import UserAttributesUpdate
from librespot.proto.spotify.login5.v3 import Login5_pb2 as Login5 from librespot.proto.spotify.login5.v3 import Login5_pb2 as Login5
from librespot.proto.spotify.login5.v3.credentials import Credentials_pb2 as Login5Credentials from librespot.proto.spotify.login5.v3.credentials import Credentials_pb2 as Login5Credentials
@@ -190,19 +192,27 @@ class ApiClient(Closeable):
self.logger.warning("PUT state returned {}. headers: {}".format( self.logger.warning("PUT state returned {}. headers: {}".format(
response.status_code, response.headers)) response.status_code, response.headers))
def get_ext_metadata(self, extension_kind: ExtensionKind, uri: str):
query = ExtensionQuery(extension_kind=extension_kind)
req = EntityRequest(entity_uri=uri, query=[query,])
batch = BatchedEntityRequest(entity_request=[req,])
headers = CaseInsensitiveDict({"content-type": "application/x-protobuf"})
response = self.send("POST", "/extended-metadata/v0/extended-metadata",
headers, batch.SerializeToString())
return response
def get_metadata_4_track(self, track: TrackId) -> Metadata.Track: def get_metadata_4_track(self, track: TrackId) -> Metadata.Track:
""" """
:param track: TrackId: :param track: TrackId:
""" """
response = self.sendToUrl("GET", "https://spclient.wg.spotify.com", response = self.get_ext_metadata(ExtensionKind.TRACK_V4, track.to_spotify_uri())
"/metadata/4/track/{}".format(track.hex_id()),
None, None)
ApiClient.StatusCodeException.check_status(response) ApiClient.StatusCodeException.check_status(response)
body = response.content body = response.content
if body is None: if body is None:
raise RuntimeError() raise RuntimeError()
# TODO: update parsing of successful response
proto = Metadata.Track() proto = Metadata.Track()
proto.ParseFromString(body) proto.ParseFromString(body)
return proto return proto

View File

@@ -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)

View File

@@ -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)

File diff suppressed because one or more lines are too long

View File

@@ -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;
}

View File

@@ -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;
}

209
proto/extension_kind.proto Normal file
View File

@@ -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;
}