From 75f7aa4cd6316005924177414a7fbf3576ea5dda Mon Sep 17 00:00:00 2001 From: OpenWaygate <27287694+OpenWaygate@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:06:27 +0800 Subject: [PATCH] :building_construction: Resolve race condition --- cmd/channel/list.go | 118 +++++++++++++++--------------------- cmd/channel/update.go | 46 +++++++------- pkg/channel/channel.go | 23 +++---- pkg/channel/channel_test.go | 26 +++++++- 4 files changed, 109 insertions(+), 104 deletions(-) diff --git a/cmd/channel/list.go b/cmd/channel/list.go index c5371a0..39beae1 100644 --- a/cmd/channel/list.go +++ b/cmd/channel/list.go @@ -14,7 +14,6 @@ import ( "github.com/eat-pray-ai/yutu/cmd" "github.com/eat-pray-ai/yutu/pkg" "github.com/eat-pray-ai/yutu/pkg/channel" - "github.com/eat-pray-ai/yutu/pkg/utils" "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" @@ -33,10 +32,10 @@ type listIn struct { ForUsername string `json:"forUsername"` Hl string `json:"hl"` Ids []string `json:"ids"` - ManagedByMe *string `json:"managedByMe,omitempty"` + ManagedByMe *bool `json:"managedByMe,omitempty"` MaxResults int64 `json:"maxResults"` - Mine *string `json:"mine,omitempty"` - MySubscribers *string `json:"mySubscribers,omitempty"` + Mine *bool `json:"mine,omitempty"` + MySubscribers *bool `json:"mySubscribers,omitempty"` OnBehalfOfContentOwner string `json:"onBehalfOfContentOwner"` Parts []string `json:"parts"` Output string `json:"output"` @@ -47,32 +46,17 @@ var listInSchema = &jsonschema.Schema{ Type: "object", Required: []string{}, Properties: map[string]*jsonschema.Schema{ - "categoryId": { - Type: "string", Description: cidUsage, - Default: json.RawMessage(`""`), - }, - "forHandle": { - Type: "string", Description: fhUsage, - Default: json.RawMessage(`""`), - }, - "forUsername": { - Type: "string", Description: fuUsage, - Default: json.RawMessage(`""`), - }, - "hl": { - Type: "string", Description: hlUsage, - Default: json.RawMessage(`""`), - }, + "categoryId": {Type: "string", Description: cidUsage}, + "forHandle": {Type: "string", Description: fhUsage}, + "forUsername": {Type: "string", Description: fuUsage}, + "hl": {Type: "string", Description: hlUsage}, "ids": { - Type: "array", Items: &jsonschema.Schema{ - Type: "string", - }, + Type: "array", Items: &jsonschema.Schema{Type: "string"}, Description: listIdsUsage, Default: json.RawMessage(`[]`), }, "managedByMe": { - Type: "string", Enum: []any{"true", "false", ""}, - Description: mbmUsage, Default: json.RawMessage(`""`), + Type: "boolean", Enum: []any{true, false}, Description: mbmUsage, }, "maxResults": { Type: "number", Description: pkg.MRUsage, @@ -80,21 +64,14 @@ var listInSchema = &jsonschema.Schema{ Minimum: jsonschema.Ptr(float64(0)), }, "mine": { - Type: "string", Enum: []any{"true", "false", ""}, - Description: mineUsage, Default: json.RawMessage(`""`), + Type: "boolean", Enum: []any{true, false}, Description: mineUsage, }, "mySubscribers": { - Type: "string", Enum: []any{"true", "false", ""}, - Description: msUsage, Default: json.RawMessage(`""`), - }, - "onBehalfOfContentOwner": { - Type: "string", Description: "", - Default: json.RawMessage(`""`), + Type: "boolean", Enum: []any{true, false}, Description: msUsage, }, + "onBehalfOfContentOwner": {Type: "string", Description: ""}, "parts": { - Type: "array", Items: &jsonschema.Schema{ - Type: "string", - }, + Type: "array", Items: &jsonschema.Schema{Type: "string"}, Description: pkg.PartsUsage, Default: json.RawMessage(`["id","snippet","status"]`), }, @@ -102,10 +79,7 @@ var listInSchema = &jsonschema.Schema{ Type: "string", Enum: []any{"json", "yaml", "table"}, Description: pkg.TableUsage, Default: json.RawMessage(`"yaml"`), }, - "jsonpath": { - Type: "string", Description: pkg.JPUsage, - Default: json.RawMessage(`""`), - }, + "jsonpath": {Type: "string", Description: pkg.JPUsage}, }, } @@ -159,7 +133,22 @@ var listCmd = &cobra.Command{ Short: listShort, Long: listLong, Run: func(cmd *cobra.Command, args []string) { - err := list(cmd.OutOrStdout()) + input := &listIn{ + CategoryId: categoryId, + ForHandle: forHandle, + ForUsername: forUsername, + Hl: hl, + Ids: ids, + ManagedByMe: managedByMe, + MaxResults: maxResults, + Mine: mine, + MySubscribers: mySubscribers, + OnBehalfOfContentOwner: onBehalfOfContentOwner, + Parts: parts, + Output: output, + Jsonpath: jsonpath, + } + err := input.call(cmd.OutOrStdout()) if err != nil { _ = cmd.Help() cmd.PrintErrf("Error: %v\n", err) @@ -177,22 +166,8 @@ func listHandler( ), ) - categoryId = input.CategoryId - forHandle = input.ForHandle - forUsername = input.ForUsername - hl = input.Hl - ids = input.Ids - managedByMe = utils.StrToBoolPtr(input.ManagedByMe) - maxResults = input.MaxResults - mine = utils.StrToBoolPtr(input.Mine) - mySubscribers = utils.StrToBoolPtr(input.MySubscribers) - onBehalfOfContentOwner = input.OnBehalfOfContentOwner - parts = input.Parts - output = input.Output - jsonpath = input.Jsonpath - var writer bytes.Buffer - err := list(&writer) + err := input.call(&writer) if err != nil { logger.ErrorContext(ctx, err.Error(), "input", input) return nil, nil, err @@ -200,20 +175,23 @@ func listHandler( return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } -func list(writer io.Writer) error { - c := channel.NewChannel( - channel.WithCategoryId(categoryId), - channel.WithForHandle(forHandle), - channel.WithForUsername(forUsername), - channel.WithHl(hl), - channel.WithIDs(ids), - channel.WithChannelManagedByMe(managedByMe), - channel.WithMaxResults(maxResults), - channel.WithMine(mine), - channel.WithMySubscribers(mySubscribers), - channel.WithOnBehalfOfContentOwner(onBehalfOfContentOwner), +func (l *listIn) call(writer io.Writer, opts ...channel.Option) error { + defaultOpts := []channel.Option{ + channel.WithCategoryId(l.CategoryId), + channel.WithForHandle(l.ForHandle), + channel.WithForUsername(l.ForUsername), + channel.WithHl(l.Hl), + channel.WithIDs(l.Ids), + channel.WithChannelManagedByMe(l.ManagedByMe), + channel.WithMaxResults(l.MaxResults), + channel.WithMine(l.Mine), + channel.WithMySubscribers(l.MySubscribers), + channel.WithOnBehalfOfContentOwner(l.OnBehalfOfContentOwner), channel.WithService(nil), - ) + } + defaultOpts = append(defaultOpts, opts...) - return c.List(parts, output, jsonpath, writer) + c := channel.NewChannel(defaultOpts...) + + return c.List(l.Parts, l.Output, l.Jsonpath, writer) } diff --git a/cmd/channel/update.go b/cmd/channel/update.go index 613997f..6504b91 100644 --- a/cmd/channel/update.go +++ b/cmd/channel/update.go @@ -114,7 +114,17 @@ var updateCmd = &cobra.Command{ Short: updateShort, Long: updateLong, Run: func(cmd *cobra.Command, args []string) { - err := update(cmd.OutOrStdout()) + input := &updateIn{ + Ids: ids, + Country: country, + CustomUrl: customUrl, + DefaultLanguage: defaultLanguage, + Description: description, + Title: title, + Output: output, + Jsonpath: jsonpath, + } + err := input.call(cmd.OutOrStdout()) if err != nil { _ = cmd.Help() cmd.PrintErrf("Error: %v\n", err) @@ -134,17 +144,8 @@ func updateHandler( ), ) - ids = input.Ids - country = input.Country - customUrl = input.CustomUrl - defaultLanguage = input.DefaultLanguage - description = input.Description - title = input.Title - output = input.Output - jsonpath = input.Jsonpath - var writer bytes.Buffer - err := update(&writer) + err := input.call(&writer) if err != nil { logger.ErrorContext(ctx, err.Error(), "input", input) return nil, nil, err @@ -152,17 +153,20 @@ func updateHandler( return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } -func update(writer io.Writer) error { - c := channel.NewChannel( - channel.WithIDs(ids), - channel.WithCountry(country), - channel.WithCustomUrl(customUrl), - channel.WithDefaultLanguage(defaultLanguage), - channel.WithDescription(description), - channel.WithTitle(title), +func (u *updateIn) call(writer io.Writer, opts ...channel.Option) error { + defaultOpts := []channel.Option{ + channel.WithIDs(u.Ids), + channel.WithCountry(u.Country), + channel.WithCustomUrl(u.CustomUrl), + channel.WithDefaultLanguage(u.DefaultLanguage), + channel.WithDescription(u.Description), + channel.WithTitle(u.Title), channel.WithMaxResults(1), channel.WithService(nil), - ) + } + defaultOpts = append(defaultOpts, opts...) - return c.Update(output, jsonpath, writer) + c := channel.NewChannel(defaultOpts...) + + return c.Update(u.Output, u.Jsonpath, writer) } diff --git a/pkg/channel/channel.go b/pkg/channel/channel.go index 16e8f6a..16e4a8f 100644 --- a/pkg/channel/channel.go +++ b/pkg/channel/channel.go @@ -18,12 +18,12 @@ import ( ) var ( - service *youtube.Service errGetChannel = errors.New("failed to get channel") errUpdateChannel = errors.New("failed to update channel") ) type channel struct { + service *youtube.Service CategoryId string `yaml:"category_id" json:"category_id"` ForHandle string `yaml:"for_handle" json:"for_handle"` ForUsername string `yaml:"for_username" json:"for_username"` @@ -57,11 +57,18 @@ func NewChannel(opts ...Option) Channel[youtube.Channel] { opt(c) } + if c.service == nil { + c.service = auth.NewY2BService( + auth.WithCredential("", pkg.Root.FS()), + auth.WithCacheToken("", pkg.Root.FS()), + ).GetService() + } + return c } func (c *channel) Get(parts []string) ([]*youtube.Channel, error) { - call := service.Channels.List(parts) + call := c.service.Channels.List(parts) if c.CategoryId != "" { call = call.CategoryId(c.CategoryId) } @@ -168,7 +175,7 @@ func (c *channel) Update(output string, jsonpath string, writer io.Writer) error cha.Snippet.Title = c.Title } - call := service.Channels.Update(parts, cha) + call := c.service.Channels.Update(parts, cha) res, err := call.Do() if err != nil { return errors.Join(errUpdateChannel, err) @@ -288,13 +295,7 @@ func WithTitle(title string) Option { } func WithService(svc *youtube.Service) Option { - return func(_ *channel) { - if svc == nil { - svc = auth.NewY2BService( - auth.WithCredential("", pkg.Root.FS()), - auth.WithCacheToken("", pkg.Root.FS()), - ).GetService() - } - service = svc + return func(c *channel) { + c.service = svc } } diff --git a/pkg/channel/channel_test.go b/pkg/channel/channel_test.go index 241c1cc..9ecf0da 100644 --- a/pkg/channel/channel_test.go +++ b/pkg/channel/channel_test.go @@ -71,7 +71,9 @@ func TestNewChannel(t *testing.T) { { name: "with no options", args: args{ - opts: []Option{}, + opts: []Option{ + WithService(&youtube.Service{}), + }, }, want: &channel{}, }, @@ -82,6 +84,7 @@ func TestNewChannel(t *testing.T) { WithChannelManagedByMe(nil), WithMine(nil), WithMySubscribers(nil), + WithService(&youtube.Service{}), }, }, want: &channel{}, @@ -93,6 +96,7 @@ func TestNewChannel(t *testing.T) { WithChannelManagedByMe(&managedByMeFalse), WithMine(&mineFalse), WithMySubscribers(&mySubscribersFalse), + WithService(&youtube.Service{}), }, }, want: &channel{ @@ -106,6 +110,7 @@ func TestNewChannel(t *testing.T) { args: args{ opts: []Option{ WithMaxResults(0), + WithService(&youtube.Service{}), }, }, want: &channel{ @@ -117,6 +122,7 @@ func TestNewChannel(t *testing.T) { args: args{ opts: []Option{ WithMaxResults(-5), + WithService(&youtube.Service{}), }, }, want: &channel{ @@ -137,6 +143,7 @@ func TestNewChannel(t *testing.T) { WithDefaultLanguage(""), WithDescription(""), WithTitle(""), + WithService(&youtube.Service{}), }, }, want: &channel{ @@ -160,6 +167,7 @@ func TestNewChannel(t *testing.T) { WithTitle("My Channel"), WithCountry("UK"), WithMaxResults(50), + WithService(&youtube.Service{}), }, }, want: &channel{ @@ -174,7 +182,21 @@ func TestNewChannel(t *testing.T) { for _, tt := range tests { t.Run( tt.name, func(t *testing.T) { - if got := NewChannel(tt.args.opts...); !reflect.DeepEqual(got, tt.want) { + if got := NewChannel(tt.args.opts...); !reflect.DeepEqual(got.(*channel).CategoryId, tt.want.(*channel).CategoryId) || + !reflect.DeepEqual(got.(*channel).ForHandle, tt.want.(*channel).ForHandle) || + !reflect.DeepEqual(got.(*channel).ForUsername, tt.want.(*channel).ForUsername) || + !reflect.DeepEqual(got.(*channel).Hl, tt.want.(*channel).Hl) || + !reflect.DeepEqual(got.(*channel).IDs, tt.want.(*channel).IDs) || + !reflect.DeepEqual(got.(*channel).ManagedByMe, tt.want.(*channel).ManagedByMe) || + !reflect.DeepEqual(got.(*channel).MaxResults, tt.want.(*channel).MaxResults) || + !reflect.DeepEqual(got.(*channel).Mine, tt.want.(*channel).Mine) || + !reflect.DeepEqual(got.(*channel).MySubscribers, tt.want.(*channel).MySubscribers) || + !reflect.DeepEqual(got.(*channel).OnBehalfOfContentOwner, tt.want.(*channel).OnBehalfOfContentOwner) || + !reflect.DeepEqual(got.(*channel).Country, tt.want.(*channel).Country) || + !reflect.DeepEqual(got.(*channel).CustomUrl, tt.want.(*channel).CustomUrl) || + !reflect.DeepEqual(got.(*channel).DefaultLanguage, tt.want.(*channel).DefaultLanguage) || + !reflect.DeepEqual(got.(*channel).Description, tt.want.(*channel).Description) || + !reflect.DeepEqual(got.(*channel).Title, tt.want.(*channel).Title) { t.Errorf("NewChannel() = %v, want %v", got, tt.want) } },