🏗️ Resolve race condition

This commit is contained in:
OpenWaygate committed 2026-01-23 23:06:27 +08:00
1 parent 2ed87e34ee
commit 75f7aa4cd6
4 files changed
+109 -104

No files matched your search

+48 -70
View File
@@ -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)
}
+25 -21
View File
@@ -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)
}
+12 -11
View File
@@ -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
}
}
+24 -2
View File
@@ -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)
}
},