diff --git a/proto/autoplay_context_request.proto b/proto/autoplay_context_request.proto new file mode 100644 index 0000000..2fc07d1 --- /dev/null +++ b/proto/autoplay_context_request.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; + +package spotify.player; + +option go_package = "go-librespot/proto/spotify/player"; + +message AutoplayContextRequest { + required string context_uri = 1; + repeated string recent_track_uri = 2; +} diff --git a/proto/spotify/player/autoplay_context_request.pb.go b/proto/spotify/player/autoplay_context_request.pb.go new file mode 100644 index 0000000..39bf36b --- /dev/null +++ b/proto/spotify/player/autoplay_context_request.pb.go @@ -0,0 +1,156 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v3.19.6 +// source: autoplay_context_request.proto + +package player + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AutoplayContextRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ContextUri *string `protobuf:"bytes,1,req,name=context_uri,json=contextUri" json:"context_uri,omitempty"` + RecentTrackUri []string `protobuf:"bytes,2,rep,name=recent_track_uri,json=recentTrackUri" json:"recent_track_uri,omitempty"` +} + +func (x *AutoplayContextRequest) Reset() { + *x = AutoplayContextRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_autoplay_context_request_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AutoplayContextRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AutoplayContextRequest) ProtoMessage() {} + +func (x *AutoplayContextRequest) ProtoReflect() protoreflect.Message { + mi := &file_autoplay_context_request_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AutoplayContextRequest.ProtoReflect.Descriptor instead. +func (*AutoplayContextRequest) Descriptor() ([]byte, []int) { + return file_autoplay_context_request_proto_rawDescGZIP(), []int{0} +} + +func (x *AutoplayContextRequest) GetContextUri() string { + if x != nil && x.ContextUri != nil { + return *x.ContextUri + } + return "" +} + +func (x *AutoplayContextRequest) GetRecentTrackUri() []string { + if x != nil { + return x.RecentTrackUri + } + return nil +} + +var File_autoplay_context_request_proto protoreflect.FileDescriptor + +var file_autoplay_context_request_proto_rawDesc = []byte{ + 0x0a, 0x1e, 0x61, 0x75, 0x74, 0x6f, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x78, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x0e, 0x73, 0x70, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x2e, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, + 0x22, 0x63, 0x0a, 0x16, 0x41, 0x75, 0x74, 0x6f, 0x70, 0x6c, 0x61, 0x79, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x78, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x02, 0x28, 0x09, 0x52, + 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x55, 0x72, 0x69, 0x12, 0x28, 0x0a, 0x10, 0x72, + 0x65, 0x63, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x75, 0x72, 0x69, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x61, + 0x63, 0x6b, 0x55, 0x72, 0x69, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x6f, 0x2d, 0x6c, 0x69, 0x62, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x70, 0x6f, 0x74, + 0x69, 0x66, 0x79, 0x2f, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, +} + +var ( + file_autoplay_context_request_proto_rawDescOnce sync.Once + file_autoplay_context_request_proto_rawDescData = file_autoplay_context_request_proto_rawDesc +) + +func file_autoplay_context_request_proto_rawDescGZIP() []byte { + file_autoplay_context_request_proto_rawDescOnce.Do(func() { + file_autoplay_context_request_proto_rawDescData = protoimpl.X.CompressGZIP(file_autoplay_context_request_proto_rawDescData) + }) + return file_autoplay_context_request_proto_rawDescData +} + +var file_autoplay_context_request_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_autoplay_context_request_proto_goTypes = []interface{}{ + (*AutoplayContextRequest)(nil), // 0: spotify.player.AutoplayContextRequest +} +var file_autoplay_context_request_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_autoplay_context_request_proto_init() } +func file_autoplay_context_request_proto_init() { + if File_autoplay_context_request_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_autoplay_context_request_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AutoplayContextRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_autoplay_context_request_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_autoplay_context_request_proto_goTypes, + DependencyIndexes: file_autoplay_context_request_proto_depIdxs, + MessageInfos: file_autoplay_context_request_proto_msgTypes, + }.Build() + File_autoplay_context_request_proto = out.File + file_autoplay_context_request_proto_rawDesc = nil + file_autoplay_context_request_proto_goTypes = nil + file_autoplay_context_request_proto_depIdxs = nil +} diff --git a/spclient/context_resolver.go b/spclient/context_resolver.go index 862750d..85dd49d 100644 --- a/spclient/context_resolver.go +++ b/spclient/context_resolver.go @@ -17,27 +17,34 @@ type ContextResolver struct { ctx *connectpb.Context } -func NewContextResolver(sp *Spclient, ctx *connectpb.Context) (*ContextResolver, error) { +func NewContextResolver(sp *Spclient, ctx *connectpb.Context) (_ *ContextResolver, err error) { typ := librespot.InferSpotifyIdTypeFromContextUri(ctx.Uri) - if len(ctx.Pages) > 0 { - return &ContextResolver{sp, typ, ctx}, nil - } else { - newCtx, err := sp.ContextResolve(ctx.Uri) + if len(ctx.Pages) == 0 { + ctx, err = sp.ContextResolve(ctx.Uri) if err != nil { return nil, fmt.Errorf("failed resolving context %s: %w", ctx.Uri, err) - } else if newCtx.Loading { - return nil, fmt.Errorf("context %s is loading", newCtx.Uri) + } else if ctx.Loading { + return nil, fmt.Errorf("context %s is loading", ctx.Uri) } - if newCtx.Metadata == nil { - newCtx.Metadata = map[string]string{} + if ctx.Metadata == nil { + ctx.Metadata = map[string]string{} } for key, val := range ctx.Metadata { - newCtx.Metadata[key] = val + ctx.Metadata[key] = val } + } - return &ContextResolver{sp, typ, newCtx}, nil + autoplay := strings.HasPrefix(ctx.Uri, "spotify:station:") + for _, page := range ctx.Pages { + for _, track := range page.Tracks { + if autoplay { + track.Metadata["autoplay.is_autoplay"] = "true" + } + } } + + return &ContextResolver{sp, typ, ctx}, nil } func (r *ContextResolver) Type() librespot.SpotifyIdType { diff --git a/spclient/spclient.go b/spclient/spclient.go index 7d1fb27..2ab1578 100644 --- a/spclient/spclient.go +++ b/spclient/spclient.go @@ -11,6 +11,7 @@ import ( connectpb "go-librespot/proto/spotify/connectstate" storagepb "go-librespot/proto/spotify/download" metadatapb "go-librespot/proto/spotify/metadata" + playerpb "go-librespot/proto/spotify/player" "google.golang.org/protobuf/proto" "io" "net/http" @@ -272,3 +273,33 @@ func (c *Spclient) ContextResolve(uri string) (*connectpb.Context, error) { return &context, nil } + +func (c *Spclient) ContextResolveAutoplay(reqProto *playerpb.AutoplayContextRequest) (*connectpb.Context, error) { + reqBody, err := proto.Marshal(reqProto) + if err != nil { + return nil, fmt.Errorf("failed marshalling AutoplayContextRequest: %w", err) + } + + resp, err := c.request("POST", "/context-resolve/v1/autoplay", nil, nil, reqBody) + if err != nil { + return nil, err + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("invalid status code from context resolve autoplay: %d", resp.StatusCode) + } + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed reading response body: %w", err) + } + + var context connectpb.Context + if err := json.Unmarshal(respBytes, &context); err != nil { + return nil, fmt.Errorf("failed json unmarshalling Context: %w", err) + } + + return &context, nil +}