From 2d9ea8eb880fe631a2b0bdd72b957f28c2cd00cf Mon Sep 17 00:00:00 2001 From: Siddhartha Varma <39856034+BRO3886@users.noreply.github.com> Date: Thu, 28 May 2026 12:37:30 +0530 Subject: [PATCH 1/5] feat(youtube): add search command Implements `gog youtube search ls ` backed by the YouTube Data API v3 search.list endpoint. Supports --type (video, channel, playlist), --order (relevance, date, rating, title, viewCount), --channel-id filtering, pagination, and both JSON and table output. Uses OAuth when --account is provided, falls back to API key otherwise. Validates --type values individually (comma-separated) and --order via kong enum tag. --- internal/cmd/youtube.go | 100 +++++++++++++++++++++++++++++++++++ internal/cmd/youtube_test.go | 96 +++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/internal/cmd/youtube.go b/internal/cmd/youtube.go index 1b4447758..4be650f5c 100644 --- a/internal/cmd/youtube.go +++ b/internal/cmd/youtube.go @@ -18,6 +18,7 @@ type YouTubeCmd struct { Playlists YouTubePlaylistsCmd `cmd:"" name:"playlists" aliases:"playlist" help:"List playlists"` Comments YouTubeCommentsCmd `cmd:"" name:"comments" aliases:"comment" help:"List comment threads"` Channels YouTubeChannelsCmd `cmd:"" name:"channels" aliases:"channel" help:"List channels"` + Search YouTubeSearchCmd `cmd:"" name:"search" aliases:"find" help:"Search YouTube for videos, channels, or playlists"` } type YouTubeActivitiesCmd struct { @@ -428,6 +429,105 @@ func (c *YouTubeChannelsListCmd) Run(ctx context.Context, flags *RootFlags) erro return nil } +type YouTubeSearchCmd struct { + List YouTubeSearchListCmd `cmd:"" name:"list" aliases:"ls" help:"Search for videos, channels, or playlists"` +} + +type YouTubeSearchListCmd struct { + Query string `arg:"" help:"Search query"` + Type string `name:"type" help:"Resource type: video, channel, playlist (comma-separated)" default:"video"` + Order string `name:"order" help:"Sort order: relevance, date, rating, title, viewCount" default:"relevance" enum:"relevance,date,rating,title,viewCount"` + ChannelID string `name:"channel-id" help:"Restrict results to a specific channel"` + Max int64 `name:"max" aliases:"limit" help:"Max results" default:"25"` + Page string `name:"page" help:"Page token"` +} + +func (c *YouTubeSearchListCmd) Run(ctx context.Context, flags *RootFlags) error { + u := ui.FromContext(ctx) + if err := validateYouTubeMax(c.Max); err != nil { + return err + } + if c.Query == "" { + return usage("search query is required") + } + + types := splitCSV(c.Type) + for _, t := range types { + switch t { + case "video", "channel", "playlist": + default: + return usage("--type must be video, channel, or playlist (comma-separated)") + } + } + + var svc *youtube.Service + var err error + if flags.Account != "" { + svc, err = getYouTubeServiceForAccount(ctx, flags.Account) + } else { + svc, err = getYouTubeServiceWithAPIKey(ctx) + } + if err != nil { + return err + } + + call := svc.Search.List([]string{"snippet"}). + Q(c.Query). + Type(types...). + Order(c.Order). + MaxResults(c.Max). + PageToken(c.Page) + if c.ChannelID != "" { + call = call.ChannelId(c.ChannelID) + } + resp, err := call.Do() + if err != nil { + return err + } + + if outfmt.IsJSON(ctx) { + return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{ + "items": resp.Items, + "nextPageToken": resp.NextPageToken, + }) + } + if len(resp.Items) == 0 { + u.Err().Println("No results") + return nil + } + w, flush := tableWriter(ctx) + defer flush() + fmt.Fprintln(w, "KIND\tID\tTITLE\tCHANNEL\tPUBLISHED_AT") + for _, item := range resp.Items { + id := "" + kind := "" + if item.Id != nil { + switch { + case item.Id.VideoId != "": + id = item.Id.VideoId + kind = "video" + case item.Id.ChannelId != "": + id = item.Id.ChannelId + kind = "channel" + case item.Id.PlaylistId != "": + id = item.Id.PlaylistId + kind = "playlist" + } + } + title := "" + ch := "" + pubAt := "" + if item.Snippet != nil { + title = item.Snippet.Title + ch = item.Snippet.ChannelTitle + pubAt = item.Snippet.PublishedAt + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", kind, id, sanitizeTab(title), sanitizeTab(ch), sanitizeTab(pubAt)) + } + printNextPageHint(u, resp.NextPageToken) + return nil +} + func validateYouTubeMax(limit int64) error { if limit < 1 || limit > 50 { return usage("--max must be between 1 and 50") diff --git a/internal/cmd/youtube_test.go b/internal/cmd/youtube_test.go index b4865204a..9814c5113 100644 --- a/internal/cmd/youtube_test.go +++ b/internal/cmd/youtube_test.go @@ -102,6 +102,102 @@ func TestYouTubeMineUsesOAuthService(t *testing.T) { } } +func TestYouTubeSearchWithAPIKey(t *testing.T) { + t.Setenv("GOG_YOUTUBE_API_KEY", "test-key") + origNew := newYouTubeWithAPIKey + t.Cleanup(func() { newYouTubeWithAPIKey = origNew }) + + var gotQuery string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotQuery = r.URL.RawQuery + if r.URL.Path != "/youtube/v3/search" { + t.Fatalf("path = %s", r.URL.Path) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "items": []map[string]any{ + { + "id": map[string]any{ + "kind": "youtube#video", + "videoId": "abc123", + }, + "snippet": map[string]any{ + "title": "Test Video", + "channelTitle": "Test Channel", + "publishedAt": "2026-01-02T03:04:05Z", + }, + }, + }, + }) + })) + defer srv.Close() + + svc := newGoogleTestServiceWithEndpoint(t, srv.Client(), srv.URL+"/", youtube.NewService) + newYouTubeWithAPIKey = func(_ context.Context, key string) (*youtube.Service, error) { + return svc, nil + } + + var err error + out := captureStdout(t, func() { + ctx := newCmdOutputContext(t, &bytes.Buffer{}, &bytes.Buffer{}) + err = runKong(t, &YouTubeSearchListCmd{}, []string{"golang tutorials", "--max", "5"}, ctx, &RootFlags{}) + }) + if err != nil { + t.Fatalf("runKong: %v", err) + } + if !strings.Contains(gotQuery, "q=golang+tutorials") { + t.Fatalf("query = %s", gotQuery) + } + if !strings.Contains(out, "abc123") || !strings.Contains(out, "Test Video") { + t.Fatalf("stdout = %q", out) + } +} + +func TestYouTubeSearchWithOAuth(t *testing.T) { + origNew := newYouTubeForAccount + t.Cleanup(func() { newYouTubeForAccount = origNew }) + + var gotAccount string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/youtube/v3/search" { + t.Fatalf("path = %s", r.URL.Path) + } + if got := r.URL.Query().Get("q"); got != "test query" { + t.Fatalf("q = %q", got) + } + _ = json.NewEncoder(w).Encode(map[string]any{"items": []map[string]any{}}) + })) + defer srv.Close() + + svc := newGoogleTestServiceWithEndpoint(t, srv.Client(), srv.URL+"/", youtube.NewService) + newYouTubeForAccount = func(_ context.Context, account string) (*youtube.Service, error) { + gotAccount = account + return svc, nil + } + + err := runKong(t, &YouTubeSearchListCmd{}, []string{"test query", "--max", "1"}, newQuietUIContext(t), &RootFlags{Account: "me@example.com"}) + if err != nil { + t.Fatalf("runKong: %v", err) + } + if gotAccount != "me@example.com" { + t.Fatalf("account = %q", gotAccount) + } +} + +func TestYouTubeSearchTypeValidation(t *testing.T) { + t.Setenv("GOG_YOUTUBE_API_KEY", "test-key") + origNew := newYouTubeWithAPIKey + t.Cleanup(func() { newYouTubeWithAPIKey = origNew }) + newYouTubeWithAPIKey = func(_ context.Context, key string) (*youtube.Service, error) { + t.Fatal("should not reach API call with invalid type") + return nil, nil + } + + err := runKong(t, &YouTubeSearchListCmd{}, []string{"query", "--type", "invalid"}, newQuietUIContext(t), &RootFlags{}) + if err == nil || !strings.Contains(err.Error(), "--type must be video, channel, or playlist") { + t.Fatalf("expected type validation, got %v", err) + } +} + func TestYouTubeValidation(t *testing.T) { err := runKong(t, &YouTubeChannelsListCmd{}, []string{"--id", "UC123", "--max", "51"}, newQuietUIContext(t), &RootFlags{}) if err == nil || !strings.Contains(err.Error(), "--max must be between 1 and 50") { From a3e30fb1b21be4c1e088e30b1382cfe868e286d5 Mon Sep 17 00:00:00 2001 From: Siddhartha Varma <39856034+BRO3886@users.noreply.github.com> Date: Thu, 28 May 2026 12:59:24 +0530 Subject: [PATCH 2/5] docs: regenerate command reference for youtube search --- docs/commands.generated.md | 2 + docs/commands/README.md | 4 +- docs/commands/gog-youtube-search-list.md | 49 ++++++++++++++++++++++++ docs/commands/gog-youtube-search.md | 48 +++++++++++++++++++++++ docs/commands/gog-youtube.md | 1 + 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 docs/commands/gog-youtube-search-list.md create mode 100644 docs/commands/gog-youtube-search.md diff --git a/docs/commands.generated.md b/docs/commands.generated.md index 96b67a333..1329e3ea7 100644 --- a/docs/commands.generated.md +++ b/docs/commands.generated.md @@ -568,6 +568,8 @@ Generated from `gog schema --json`. - [`gog youtube (yt) comments (comment) list (ls) [flags]`](commands/gog-youtube-comments-list.md) - List comment threads for a video or channel - [`gog youtube (yt) playlists (playlist) `](commands/gog-youtube-playlists.md) - List playlists - [`gog youtube (yt) playlists (playlist) list (ls) [flags]`](commands/gog-youtube-playlists-list.md) - List playlists by channel or authenticated user + - [`gog youtube (yt) search (find) `](commands/gog-youtube-search.md) - Search YouTube for videos, channels, or playlists + - [`gog youtube (yt) search (find) list (ls) [flags]`](commands/gog-youtube-search-list.md) - Search for videos, channels, or playlists - [`gog youtube (yt) videos (video) `](commands/gog-youtube-videos.md) - List or get videos - [`gog youtube (yt) videos (video) list (ls) [flags]`](commands/gog-youtube-videos-list.md) - List videos by ID or chart - [`gog zoom [flags]`](commands/gog-zoom.md) - Zoom diff --git a/docs/commands/README.md b/docs/commands/README.md index 9b8a3abbb..77266fc16 100644 --- a/docs/commands/README.md +++ b/docs/commands/README.md @@ -2,7 +2,7 @@ Every `gog` command has a generated docs page. The source of truth is the live CLI schema; run `make docs-commands` after changing command names, flags, help text, aliases, or arguments. -Generated pages: 572. +Generated pages: 574. ## Top-level Commands @@ -619,6 +619,8 @@ Generated pages: 572. - [gog youtube comments list](gog-youtube-comments-list.md) - List comment threads for a video or channel - [gog youtube playlists](gog-youtube-playlists.md) - List playlists - [gog youtube playlists list](gog-youtube-playlists-list.md) - List playlists by channel or authenticated user + - [gog youtube search](gog-youtube-search.md) - Search YouTube for videos, channels, or playlists + - [gog youtube search list](gog-youtube-search-list.md) - Search for videos, channels, or playlists - [gog youtube videos](gog-youtube-videos.md) - List or get videos - [gog youtube videos list](gog-youtube-videos-list.md) - List videos by ID or chart - [gog zoom](gog-zoom.md) - Zoom diff --git a/docs/commands/gog-youtube-search-list.md b/docs/commands/gog-youtube-search-list.md new file mode 100644 index 000000000..365879fb2 --- /dev/null +++ b/docs/commands/gog-youtube-search-list.md @@ -0,0 +1,49 @@ +# `gog youtube search list` + +> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`. + +Search for videos, channels, or playlists + +## Usage + +```bash +gog youtube (yt) search (find) list (ls) [flags] +``` + +## Parent + +- [gog youtube search](gog-youtube-search.md) + +## Flags + +| Flag | Type | Default | Help | +| --- | --- | --- | --- | +| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) | +| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/drivelabels/docs/slides/contacts/tasks/people/sheets/forms/sites/appscript/analytics/searchconsole/ads/photos) | +| `--channel-id` | `string` | | Restrict results to a specific channel | +| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) | +| `--color` | `string` | auto | Color output: auto\|always\|never | +| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed | +| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully | +| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) | +| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands | +| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) | +| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. | +| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) | +| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) | +| `--max`
`--limit` | `int64` | 25 | Max results | +| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) | +| `--order` | `string` | relevance | Sort order: relevance, date, rating, title, viewCount | +| `--page` | `string` | | Page token | +| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) | +| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) | +| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. | +| `--type` | `string` | video | Resource type: video, channel, playlist (comma-separated) | +| `-v`
`--verbose` | `bool` | | Enable verbose logging | +| `--version` | `kong.VersionFlag` | | Print version and exit | +| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers | + +## See Also + +- [gog youtube search](gog-youtube-search.md) +- [Command index](README.md) diff --git a/docs/commands/gog-youtube-search.md b/docs/commands/gog-youtube-search.md new file mode 100644 index 000000000..3b18c79df --- /dev/null +++ b/docs/commands/gog-youtube-search.md @@ -0,0 +1,48 @@ +# `gog youtube search` + +> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`. + +Search YouTube for videos, channels, or playlists + +## Usage + +```bash +gog youtube (yt) search (find) +``` + +## Parent + +- [gog youtube](gog-youtube.md) + +## Subcommands + +- [gog youtube search list](gog-youtube-search-list.md) - Search for videos, channels, or playlists + +## Flags + +| Flag | Type | Default | Help | +| --- | --- | --- | --- | +| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) | +| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/drivelabels/docs/slides/contacts/tasks/people/sheets/forms/sites/appscript/analytics/searchconsole/ads/photos) | +| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) | +| `--color` | `string` | auto | Color output: auto\|always\|never | +| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed | +| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully | +| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) | +| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands | +| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) | +| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. | +| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) | +| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) | +| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) | +| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) | +| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) | +| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. | +| `-v`
`--verbose` | `bool` | | Enable verbose logging | +| `--version` | `kong.VersionFlag` | | Print version and exit | +| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers | + +## See Also + +- [gog youtube](gog-youtube.md) +- [Command index](README.md) diff --git a/docs/commands/gog-youtube.md b/docs/commands/gog-youtube.md index d76f8e115..291f09b3c 100644 --- a/docs/commands/gog-youtube.md +++ b/docs/commands/gog-youtube.md @@ -20,6 +20,7 @@ gog youtube (yt) [flags] - [gog youtube channels](gog-youtube-channels.md) - List channels - [gog youtube comments](gog-youtube-comments.md) - List comment threads - [gog youtube playlists](gog-youtube-playlists.md) - List playlists +- [gog youtube search](gog-youtube-search.md) - Search YouTube for videos, channels, or playlists - [gog youtube videos](gog-youtube-videos.md) - List or get videos ## Flags From 678b3701c13dd560513c231f70e58ec14d76d456 Mon Sep 17 00:00:00 2001 From: Siddhartha Varma <39856034+BRO3886@users.noreply.github.com> Date: Thu, 28 May 2026 13:36:15 +0530 Subject: [PATCH 3/5] docs: update youtube help string to include search --- docs/commands.generated.md | 2 +- docs/commands/README.md | 4 ++-- docs/commands/gog-youtube.md | 2 +- docs/commands/gog.md | 2 +- internal/cmd/root.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/commands.generated.md b/docs/commands.generated.md index 1329e3ea7..f7a2dd268 100644 --- a/docs/commands.generated.md +++ b/docs/commands.generated.md @@ -559,7 +559,7 @@ Generated from `gog schema --json`. - [`gog upload (up,put) [flags]`](commands/gog-upload.md) - Upload a file to Drive (alias for 'drive upload') - [`gog version [flags]`](commands/gog-version.md) - Print version - [`gog whoami (who-am-i) [flags]`](commands/gog-whoami.md) - Show your profile (alias for 'people me') - - [`gog youtube (yt) [flags]`](commands/gog-youtube.md) - YouTube Data API (activities, videos, playlists, comments, channels) + - [`gog youtube (yt) [flags]`](commands/gog-youtube.md) - YouTube Data API (search, activities, videos, playlists, comments, channels) - [`gog youtube (yt) activities (activity) `](commands/gog-youtube-activities.md) - List channel activities - [`gog youtube (yt) activities (activity) list (ls) [flags]`](commands/gog-youtube-activities-list.md) - List activities for a channel (or authenticated user) - [`gog youtube (yt) channels (channel) `](commands/gog-youtube-channels.md) - List channels diff --git a/docs/commands/README.md b/docs/commands/README.md index 77266fc16..7e7e56fce 100644 --- a/docs/commands/README.md +++ b/docs/commands/README.md @@ -48,7 +48,7 @@ Generated pages: 574. - [gog upload](gog-upload.md) - Upload a file to Drive (alias for 'drive upload') - [gog version](gog-version.md) - Print version - [gog whoami](gog-whoami.md) - Show your profile (alias for 'people me') -- [gog youtube](gog-youtube.md) - YouTube Data API (activities, videos, playlists, comments, channels) +- [gog youtube](gog-youtube.md) - YouTube Data API (search, activities, videos, playlists, comments, channels) - [gog zoom](gog-zoom.md) - Zoom ## All Commands @@ -610,7 +610,7 @@ Generated pages: 574. - [gog upload](gog-upload.md) - Upload a file to Drive (alias for 'drive upload') - [gog version](gog-version.md) - Print version - [gog whoami](gog-whoami.md) - Show your profile (alias for 'people me') - - [gog youtube](gog-youtube.md) - YouTube Data API (activities, videos, playlists, comments, channels) + - [gog youtube](gog-youtube.md) - YouTube Data API (search, activities, videos, playlists, comments, channels) - [gog youtube activities](gog-youtube-activities.md) - List channel activities - [gog youtube activities list](gog-youtube-activities-list.md) - List activities for a channel (or authenticated user) - [gog youtube channels](gog-youtube-channels.md) - List channels diff --git a/docs/commands/gog-youtube.md b/docs/commands/gog-youtube.md index 291f09b3c..96ad3660e 100644 --- a/docs/commands/gog-youtube.md +++ b/docs/commands/gog-youtube.md @@ -2,7 +2,7 @@ > Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`. -YouTube Data API (activities, videos, playlists, comments, channels) +YouTube Data API (search, activities, videos, playlists, comments, channels) ## Usage diff --git a/docs/commands/gog.md b/docs/commands/gog.md index 83a486759..8227176d5 100644 --- a/docs/commands/gog.md +++ b/docs/commands/gog.md @@ -58,7 +58,7 @@ gog [flags] - [gog upload](gog-upload.md) - Upload a file to Drive (alias for 'drive upload') - [gog version](gog-version.md) - Print version - [gog whoami](gog-whoami.md) - Show your profile (alias for 'people me') -- [gog youtube](gog-youtube.md) - YouTube Data API (activities, videos, playlists, comments, channels) +- [gog youtube](gog-youtube.md) - YouTube Data API (search, activities, videos, playlists, comments, channels) - [gog zoom](gog-zoom.md) - Zoom ## Flags diff --git a/internal/cmd/root.go b/internal/cmd/root.go index bcaafdbe7..4ef49dc03 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -90,7 +90,7 @@ type CLI struct { AppScript AppScriptCmd `cmd:"" name:"appscript" aliases:"script,apps-script" help:"Google Apps Script"` Analytics AnalyticsCmd `cmd:"" aliases:"ga" help:"Google Analytics"` SearchConsole SearchConsoleCmd `cmd:"" name:"searchconsole" aliases:"gsc,search-console,webmasters" help:"Google Search Console"` - YouTube YouTubeCmd `cmd:"" name:"youtube" aliases:"yt" help:"YouTube Data API (activities, videos, playlists, comments, channels)"` + YouTube YouTubeCmd `cmd:"" name:"youtube" aliases:"yt" help:"YouTube Data API (search, activities, videos, playlists, comments, channels)"` Photos PhotosCmd `cmd:"" name:"photos" aliases:"photo" help:"Google Photos Library API (app-created media)"` Config ConfigCmd `cmd:"" help:"Manage configuration"` ExitCodes AgentExitCodesCmd `cmd:"" name:"exit-codes" aliases:"exitcodes" help:"Print stable exit codes (alias for 'agent exit-codes')"` From 0ce7b5d5cb987aac1787c2b534405387c9155049 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 21:50:31 +0100 Subject: [PATCH 4/5] fix(youtube): allow videoCount search ordering --- docs/commands/gog-youtube-search-list.md | 2 +- internal/cmd/youtube.go | 2 +- internal/cmd/youtube_test.go | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/commands/gog-youtube-search-list.md b/docs/commands/gog-youtube-search-list.md index 109a2d274..10128f3d9 100644 --- a/docs/commands/gog-youtube-search-list.md +++ b/docs/commands/gog-youtube-search-list.md @@ -34,7 +34,7 @@ gog youtube (yt) search (find) list (ls) [flags] | `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) | | `--max`
`--limit` | `int64` | 25 | Max results | | `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) | -| `--order` | `string` | relevance | Sort order: relevance, date, rating, title, viewCount | +| `--order` | `string` | relevance | Sort order: relevance, date, rating, title, videoCount, viewCount | | `--page` | `string` | | Page token | | `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) | | `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) | diff --git a/internal/cmd/youtube.go b/internal/cmd/youtube.go index c85e6abf7..db6c41fd0 100644 --- a/internal/cmd/youtube.go +++ b/internal/cmd/youtube.go @@ -439,7 +439,7 @@ type YouTubeSearchCmd struct { type YouTubeSearchListCmd struct { Query string `arg:"" help:"Search query"` Type string `name:"type" help:"Resource type: video, channel, playlist (comma-separated)" default:"video"` - Order string `name:"order" help:"Sort order: relevance, date, rating, title, viewCount" default:"relevance" enum:"relevance,date,rating,title,viewCount"` + Order string `name:"order" help:"Sort order: relevance, date, rating, title, videoCount, viewCount" default:"relevance" enum:"relevance,date,rating,title,videoCount,viewCount"` ChannelID string `name:"channel-id" help:"Restrict results to a specific channel"` Max int64 `name:"max" aliases:"limit" help:"Max results" default:"25"` Page string `name:"page" help:"Page token"` diff --git a/internal/cmd/youtube_test.go b/internal/cmd/youtube_test.go index 3f0d34a74..3e5e001f8 100644 --- a/internal/cmd/youtube_test.go +++ b/internal/cmd/youtube_test.go @@ -122,11 +122,11 @@ func TestYouTubeSearchWithAPIKey(t *testing.T) { "items": []map[string]any{ { "id": map[string]any{ - "kind": "youtube#video", - "videoId": "abc123", + "kind": "youtube#channel", + "channelId": "UC123", }, "snippet": map[string]any{ - "title": "Test Video", + "title": "Test Channel", "channelTitle": "Test Channel", "publishedAt": "2026-01-02T03:04:05Z", }, @@ -145,7 +145,7 @@ func TestYouTubeSearchWithAPIKey(t *testing.T) { var err error out := captureStdout(t, func() { ctx := newCmdOutputContext(t, &bytes.Buffer{}, &bytes.Buffer{}) - err = runKong(t, &YouTubeSearchListCmd{}, []string{"golang tutorials", "--max", "5"}, ctx, &RootFlags{}) + err = runKong(t, &YouTubeSearchListCmd{}, []string{"golang tutorials", "--type", "channel", "--order", "videoCount", "--max", "5"}, ctx, &RootFlags{}) }) if err != nil { t.Fatalf("runKong: %v", err) @@ -156,7 +156,10 @@ func TestYouTubeSearchWithAPIKey(t *testing.T) { if !strings.Contains(gotQuery, "q=golang+tutorials") { t.Fatalf("query = %s", gotQuery) } - if !strings.Contains(out, "abc123") || !strings.Contains(out, "Test Video") { + if !strings.Contains(gotQuery, "type=channel") || !strings.Contains(gotQuery, "order=videoCount") { + t.Fatalf("query = %s", gotQuery) + } + if !strings.Contains(out, "UC123") || !strings.Contains(out, "Test Channel") { t.Fatalf("stdout = %q", out) } } From 98b93ba61bc32cf65fb4a0c52bd5d2f971401de4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 21:54:14 +0100 Subject: [PATCH 5/5] test(youtube): avoid nil service sentinel --- internal/cmd/youtube_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/youtube_test.go b/internal/cmd/youtube_test.go index 3e5e001f8..00fc755cf 100644 --- a/internal/cmd/youtube_test.go +++ b/internal/cmd/youtube_test.go @@ -253,7 +253,7 @@ func TestYouTubeSearchTypeValidation(t *testing.T) { t.Cleanup(func() { newYouTubeWithAPIKey = origNew }) newYouTubeWithAPIKey = func(_ context.Context, key string) (*youtube.Service, error) { t.Fatal("should not reach API call with invalid type") - return nil, nil + return nil, errors.New("unexpected API key service") } err := runKong(t, &YouTubeSearchListCmd{}, []string{"query", "--type", "invalid"}, newQuietUIContext(t), &RootFlags{})