diff --git a/cmd/activity/BUILD.bazel b/cmd/activity/BUILD.bazel index b636bc5..9520e9d 100644 --- a/cmd/activity/BUILD.bazel +++ b/cmd/activity/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_go//go:def.bzl", "go_library", "go_test") +load("@rules_go//go:def.bzl", "go_library") go_library( name = "activity", @@ -18,16 +18,3 @@ go_library( "@com_github_spf13_cobra//:cobra", ], ) - -go_test( - name = "activity_test", - srcs = ["list_test.go"], - embed = [":activity"], - deps = [ - "//pkg/activity", - "@com_github_google_jsonschema_go//jsonschema", - "@com_github_stretchr_testify//assert", - "@org_golang_google_api//option", - "@org_golang_google_api//youtube/v3:youtube", - ], -) diff --git a/cmd/activity/list.go b/cmd/activity/list.go index a2533a6..3c43673 100644 --- a/cmd/activity/list.go +++ b/cmd/activity/list.go @@ -7,7 +7,6 @@ import ( "bytes" "context" "encoding/json" - "io" "log/slog" "time" @@ -23,37 +22,24 @@ const ( listTool = "activity-list" ) -type listIn struct { - ChannelId string `json:"channelId"` - Home *bool `json:"home,omitempty"` - MaxResults int64 `json:"maxResults"` - Mine *bool `json:"mine,omitempty"` - PublishedAfter string `json:"publishedAfter"` - PublishedBefore string `json:"publishedBefore"` - RegionCode string `json:"regionCode"` - Parts []string `json:"parts"` - Output string `json:"output"` - Jsonpath string `json:"jsonpath"` -} - var listInSchema = &jsonschema.Schema{ Type: "object", Required: []string{}, Properties: map[string]*jsonschema.Schema{ - "channelId": {Type: "string", Description: ciUsage}, + "channel_id": {Type: "string", Description: ciUsage}, "home": { Type: "boolean", Enum: []any{true, false}, Description: homeUsage, }, - "maxResults": { + "max_results": { Type: "number", Description: pkg.MRUsage, Default: json.RawMessage("5"), Minimum: jsonschema.Ptr(float64(0)), }, "mine": { Type: "boolean", Enum: []any{true, false}, Description: mineUsage, }, - "publishedAfter": {Type: "string", Description: paUsage}, - "publishedBefore": {Type: "string", Description: pbUsage}, - "regionCode": {Type: "string", Description: rcUsage}, + "published_after": {Type: "string", Description: paUsage}, + "published_before": {Type: "string", Description: pbUsage}, + "region_code": {Type: "string", Description: rcUsage}, "parts": { Type: "array", Description: pkg.PartsUsage, Items: &jsonschema.Schema{Type: "string"}, @@ -104,19 +90,20 @@ var listCmd = &cobra.Command{ Short: short, Long: long, Run: func(cmd *cobra.Command, args []string) { - input := &listIn{ - ChannelId: channelId, - Home: home, - MaxResults: maxResults, - Mine: mine, - PublishedAfter: publishedAfter, - PublishedBefore: publishedAfter, - RegionCode: regionCode, - Parts: parts, - Output: output, - Jsonpath: jsonpath, - } - if err := input.call(cmd.OutOrStdout()); err != nil { + input := activity.NewActivity( + activity.WithChannelId(channelId), + activity.WithHome(home), + activity.WithMaxResults(maxResults), + activity.WithMine(mine), + activity.WithPublishedAfter(publishedAfter), + activity.WithPublishedBefore(publishedBefore), + activity.WithRegionCode(regionCode), + activity.WithParts(parts), + activity.WithOutput(output), + activity.WithJsonpath(jsonpath), + activity.WithService(nil), + ) + if err := input.List(cmd.OutOrStdout()); err != nil { _ = cmd.Help() cmd.PrintErrf("Error: %v\n", err) } @@ -124,7 +111,7 @@ var listCmd = &cobra.Command{ } func listHandler( - ctx context.Context, req *mcp.CallToolRequest, input listIn, + ctx context.Context, req *mcp.CallToolRequest, input activity.Activity, ) (*mcp.CallToolResult, any, error) { logger := slog.New( mcp.NewLoggingHandler( @@ -134,26 +121,9 @@ func listHandler( ) var writer bytes.Buffer - if err := input.call(&writer); err != nil { + if err := input.List(&writer); err != nil { logger.ErrorContext(ctx, err.Error(), "input", input) return nil, nil, err } return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: writer.String()}}}, nil, nil } - -func (l *listIn) call(writer io.Writer, opts ...activity.Option) error { - defaultOpts := []activity.Option{ - activity.WithChannelId(l.ChannelId), - activity.WithHome(l.Home), - activity.WithMaxResults(l.MaxResults), - activity.WithMine(l.Mine), - activity.WithPublishedAfter(l.PublishedAfter), - activity.WithPublishedBefore(l.PublishedBefore), - activity.WithRegionCode(l.RegionCode), - activity.WithService(nil), - } - defaultOpts = append(defaultOpts, opts...) - a := activity.NewActivity(defaultOpts...) - - return a.List(l.Parts, l.Output, l.Jsonpath, writer) -} diff --git a/cmd/activity/list_test.go b/cmd/activity/list_test.go deleted file mode 100644 index 5d37b1d..0000000 --- a/cmd/activity/list_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2026 eat-pray-ai & OpenWaygate -// SPDX-License-Identifier: Apache-2.0 - -package activity - -import ( - "bytes" - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/eat-pray-ai/yutu/pkg/activity" - "github.com/google/jsonschema-go/jsonschema" - "github.com/stretchr/testify/assert" - "google.golang.org/api/option" - "google.golang.org/api/youtube/v3" -) - -func TestList(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{ - "items": [ - { - "id": "test-activity-id", - "snippet": { - "title": "Test Activity", - "type": "upload", - "publishedAt": "2024-01-01T00:00:00Z" - }, - "contentDetails": { - "upload": { - "videoId": "test-video-id" - } - } - } - ], - "nextPageToken": "" - }`)) - })) - defer ts.Close() - - svc, err := youtube.NewService(context.Background(), option.WithEndpoint(ts.URL), option.WithAPIKey("test-key")) - if err != nil { - t.Fatalf("failed to create service: %v", err) - } - - tests := []struct { - name string - input *listIn - wantErr bool - verify func(t *testing.T, output string) - }{ - { - name: "json output", - input: &listIn{ - ChannelId: "test-channel-id", - Parts: []string{"id", "snippet", "contentDetails"}, - Output: "json", - }, - wantErr: false, - verify: func(t *testing.T, output string) { - assert.Contains(t, output, "test-activity-id") - assert.Contains(t, output, "Test Activity") - assert.Contains(t, output, "upload") - }, - }, - { - name: "table output", - input: &listIn{ - Home: jsonschema.Ptr(true), - MaxResults: 10, - RegionCode: "US", - Output: "table", - }, - wantErr: false, - verify: func(t *testing.T, output string) { - assert.Contains(t, output, "ID") - assert.Contains(t, output, "TITLE") - assert.Contains(t, output, "TYPE") - assert.Contains(t, output, "TIME") - assert.Contains(t, output, "test-activity-id") - assert.Contains(t, output, "Test Activity") - }, - }, - { - name: "yaml output", - input: &listIn{ - Mine: jsonschema.Ptr(true), - PublishedAfter: "2024-01-01T00:00:00Z", - PublishedBefore: "2024-12-31T23:59:59Z", - Output: "yaml", - }, - wantErr: false, - verify: func(t *testing.T, output string) { - assert.Contains(t, output, "id: test-activity-id") - assert.Contains(t, output, "title: Test Activity") - assert.Contains(t, output, "type: upload") - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - err = tt.input.call(&buf, activity.WithService(svc)) - if (err != nil) != tt.wantErr { - t.Errorf("listIn.call() error = %v, wantErr %v", err, tt.wantErr) - return - } - tt.verify(t, buf.String()) - }) - } -} diff --git a/cmd/channel/BUILD.bazel b/cmd/channel/BUILD.bazel index 14da353..486bb45 100644 --- a/cmd/channel/BUILD.bazel +++ b/cmd/channel/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "channel", @@ -20,3 +20,19 @@ go_library( "@com_github_spf13_pflag//:pflag", ], ) + +go_test( + name = "channel_test", + srcs = [ + "list_test.go", + "update_test.go", + ], + embed = [":channel"], + deps = [ + "//pkg/channel", + "@com_github_google_jsonschema_go//jsonschema", + "@com_github_stretchr_testify//assert", + "@org_golang_google_api//option", + "@org_golang_google_api//youtube/v3:youtube", + ], +) diff --git a/pkg/activity/activity.go b/pkg/activity/activity.go index 3aedf3d..17d75c9 100644 --- a/pkg/activity/activity.go +++ b/pkg/activity/activity.go @@ -19,7 +19,7 @@ var ( errGetActivity = errors.New("failed to get activity") ) -type activity struct { +type Activity struct { service *youtube.Service ChannelId string `yaml:"channel_id" json:"channel_id"` Home *bool `yaml:"home" json:"home"` @@ -28,34 +28,33 @@ type activity struct { PublishedAfter string `yaml:"published_after" json:"published_after"` PublishedBefore string `yaml:"published_before" json:"published_before"` RegionCode string `yaml:"region_code" json:"region_code"` + + Parts []string `yaml:"parts" json:"parts"` + Output string `yaml:"output" json:"output"` + Jsonpath string `yaml:"jsonpath" json:"jsonpath"` } -type Activity[T any] interface { - List([]string, string, string, io.Writer) error - Get([]string) ([]*T, error) +type IActivity[T any] interface { + List(io.Writer) error + Get() ([]*T, error) + preRun() } -type Option func(*activity) +type Option func(*Activity) -func NewActivity(opts ...Option) Activity[youtube.Activity] { - a := &activity{} +func NewActivity(opts ...Option) IActivity[youtube.Activity] { + a := &Activity{} for _, opt := range opts { opt(a) } - if a.service == nil { - a.service = auth.NewY2BService( - auth.WithCredential("", pkg.Root.FS()), - auth.WithCacheToken("", pkg.Root.FS()), - ).GetService() - } - return a } -func (a *activity) Get(parts []string) ([]*youtube.Activity, error) { - call := a.service.Activities.List(parts) +func (a *Activity) Get() ([]*youtube.Activity, error) { + a.preRun() + call := a.service.Activities.List(a.Parts) if a.ChannelId != "" { call = call.ChannelId(a.ChannelId) } @@ -104,19 +103,17 @@ func (a *activity) Get(parts []string) ([]*youtube.Activity, error) { return items, nil } -func (a *activity) List( - parts []string, output string, jsonpath string, writer io.Writer, -) error { - activities, err := a.Get(parts) +func (a *Activity) List(writer io.Writer) error { + activities, err := a.Get() if err != nil && activities == nil { return err } - switch output { + switch a.Output { case "json": - utils.PrintJSON(activities, jsonpath, writer) + utils.PrintJSON(activities, a.Jsonpath, writer) case "yaml": - utils.PrintYAML(activities, jsonpath, writer) + utils.PrintYAML(activities, a.Jsonpath, writer) case "table": tb := table.NewWriter() defer tb.Render() @@ -135,14 +132,23 @@ func (a *activity) List( return err } +func (a *Activity) preRun() { + if a.service == nil { + a.service = auth.NewY2BService( + auth.WithCredential("", pkg.Root.FS()), + auth.WithCacheToken("", pkg.Root.FS()), + ).GetService() + } +} + func WithChannelId(channelId string) Option { - return func(a *activity) { + return func(a *Activity) { a.ChannelId = channelId } } func WithHome(home *bool) Option { - return func(a *activity) { + return func(a *Activity) { if home != nil { a.Home = home } @@ -150,7 +156,7 @@ func WithHome(home *bool) Option { } func WithMaxResults(maxResults int64) Option { - return func(a *activity) { + return func(a *Activity) { if maxResults < 0 { maxResults = 1 } else if maxResults == 0 { @@ -161,7 +167,7 @@ func WithMaxResults(maxResults int64) Option { } func WithMine(mine *bool) Option { - return func(a *activity) { + return func(a *Activity) { if mine != nil { a.Mine = mine } @@ -169,25 +175,43 @@ func WithMine(mine *bool) Option { } func WithPublishedAfter(publishedAfter string) Option { - return func(a *activity) { + return func(a *Activity) { a.PublishedAfter = publishedAfter } } func WithPublishedBefore(publishedBefore string) Option { - return func(a *activity) { + return func(a *Activity) { a.PublishedBefore = publishedBefore } } func WithRegionCode(regionCode string) Option { - return func(a *activity) { + return func(a *Activity) { a.RegionCode = regionCode } } +func WithParts(parts []string) Option { + return func(a *Activity) { + a.Parts = parts + } +} + +func WithOutput(output string) Option { + return func(a *Activity) { + a.Output = output + } +} + +func WithJsonpath(jsonpath string) Option { + return func(a *Activity) { + a.Jsonpath = jsonpath + } +} + func WithService(svc *youtube.Service) Option { - return func(c *activity) { + return func(c *Activity) { c.service = svc } } diff --git a/pkg/activity/activity_test.go b/pkg/activity/activity_test.go index d8e91b7..c6b1f96 100644 --- a/pkg/activity/activity_test.go +++ b/pkg/activity/activity_test.go @@ -24,7 +24,7 @@ func TestNewActivity(t *testing.T) { tests := []struct { name string args args - want Activity[youtube.Activity] + want IActivity[youtube.Activity] }{ { name: "with all options", @@ -40,7 +40,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{ + want: &Activity{ ChannelId: "test-channel-123", Home: &homeTrue, MaxResults: 50, @@ -57,7 +57,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{}, + want: &Activity{}, }, { name: "with nil boolean options", @@ -68,7 +68,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{}, + want: &Activity{}, }, { name: "with false boolean options", @@ -79,7 +79,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{ + want: &Activity{ Home: &homeFalse, Mine: &mineFalse, }, @@ -92,7 +92,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{ + want: &Activity{ MaxResults: math.MaxInt64, }, }, @@ -104,7 +104,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{ + want: &Activity{ MaxResults: 1, }, }, @@ -119,7 +119,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{ + want: &Activity{ ChannelId: "", PublishedAfter: "", PublishedBefore: "", @@ -136,7 +136,7 @@ func TestNewActivity(t *testing.T) { WithService(&youtube.Service{}), }, }, - want: &activity{ + want: &Activity{ ChannelId: "partial-channel", MaxResults: 25, RegionCode: "UK", @@ -147,13 +147,13 @@ func TestNewActivity(t *testing.T) { for _, tt := range tests { t.Run( tt.name, func(t *testing.T) { - if got := NewActivity(tt.args.opts...); !reflect.DeepEqual(got.(*activity).ChannelId, tt.want.(*activity).ChannelId) || - !reflect.DeepEqual(got.(*activity).Home, tt.want.(*activity).Home) || - !reflect.DeepEqual(got.(*activity).MaxResults, tt.want.(*activity).MaxResults) || - !reflect.DeepEqual(got.(*activity).Mine, tt.want.(*activity).Mine) || - !reflect.DeepEqual(got.(*activity).PublishedAfter, tt.want.(*activity).PublishedAfter) || - !reflect.DeepEqual(got.(*activity).PublishedBefore, tt.want.(*activity).PublishedBefore) || - !reflect.DeepEqual(got.(*activity).RegionCode, tt.want.(*activity).RegionCode) { + if got := NewActivity(tt.args.opts...); !reflect.DeepEqual(got.(*Activity).ChannelId, tt.want.(*Activity).ChannelId) || + !reflect.DeepEqual(got.(*Activity).Home, tt.want.(*Activity).Home) || + !reflect.DeepEqual(got.(*Activity).MaxResults, tt.want.(*Activity).MaxResults) || + !reflect.DeepEqual(got.(*Activity).Mine, tt.want.(*Activity).Mine) || + !reflect.DeepEqual(got.(*Activity).PublishedAfter, tt.want.(*Activity).PublishedAfter) || + !reflect.DeepEqual(got.(*Activity).PublishedBefore, tt.want.(*Activity).PublishedBefore) || + !reflect.DeepEqual(got.(*Activity).RegionCode, tt.want.(*Activity).RegionCode) { t.Errorf("NewActivity() = %v, want %v", got, tt.want) } },