Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,13 @@ func (r *Root) newCommand(runCtx *runContext) *cobra.Command {
r.newDouyinCommand(runCtx),
r.newGoogleAdsCommand(runCtx),
r.newGoogleTrendsCommand(runCtx),
r.newInstagramCommand(runCtx),
r.newMetaAdsCommand(runCtx),
r.newRedditCommand(runCtx),
r.newTiktokCommand(runCtx),
r.newTrendCloudCommand(runCtx),
r.newXiaohongshuCommand(runCtx),
r.newYouTubeCommand(runCtx),
)

return rootCmd
Expand Down
36 changes: 36 additions & 0 deletions internal/command/root_sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,25 @@ func (r *Root) newMetaAdsCommand(runCtx *runContext) *cobra.Command {
)
}

func (r *Root) newInstagramCommand(runCtx *runContext) *cobra.Command {
return newStaticSourceCommand(
"instagram",
"Instagram profile, media, search, and comments",
"Instagram profile, media, search, and comments.\n\nUse this command to search Instagram users/reels/hashtags, inspect public profiles and posts/reels, and collect comments and replies.",
" apimux instagram search_users --query pimax\n apimux instagram get_user_posts --username pimaxofficial\n apimux instagram get_post_comments --url https://www.instagram.com/p/CODE/",
"search_users, search_reels, search_hashtags, get_user_profile, get_user_posts, get_user_reels, get_post_detail, get_post_comments, get_comment_replies",
newSchemaBoundCapabilityCommand(runCtx, "instagram.search_users", "search_users", "Search Instagram users", "instagram search_users"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.search_reels", "search_reels", "Search Instagram reels", "instagram search_reels"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.search_hashtags", "search_hashtags", "Search Instagram hashtags", "instagram search_hashtags"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.get_user_profile", "get_user_profile", "Fetch one Instagram public profile", "instagram get_user_profile"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.get_user_posts", "get_user_posts", "List Instagram user posts", "instagram get_user_posts"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.get_user_reels", "get_user_reels", "List Instagram user reels", "instagram get_user_reels"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.get_post_detail", "get_post_detail", "Fetch one Instagram post or reel detail", "instagram get_post_detail"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.get_post_comments", "get_post_comments", "List Instagram post comments", "instagram get_post_comments"),
newSchemaBoundCapabilityCommand(runCtx, "instagram.get_comment_replies", "get_comment_replies", "List Instagram comment replies", "instagram get_comment_replies"),
)
}

func (r *Root) newRedditCommand(runCtx *runContext) *cobra.Command {
return newStaticSourceCommand(
"reddit",
Expand Down Expand Up @@ -182,6 +201,23 @@ func (r *Root) newTiktokCommand(runCtx *runContext) *cobra.Command {
)
}

func (r *Root) newYouTubeCommand(runCtx *runContext) *cobra.Command {
return newStaticSourceCommand(
"youtube",
"YouTube video, channel, comment, and transcript data",
"YouTube video, channel, comment, and transcript data.\n\nUse this command to search YouTube videos, inspect videos/channels, list comments and replies, and fetch captions or transcript text.",
" apimux youtube search_videos --query 'Pimax VR'\n apimux youtube get_video_comments --url https://www.youtube.com/watch?v=VIDEO_ID\n apimux youtube get_channel_videos --handle @PimaxOfficial",
"search_videos, get_video_detail, get_video_comments, get_comment_replies, get_channel_detail, get_channel_videos, get_video_transcript",
newSchemaBoundCapabilityCommand(runCtx, "youtube.search_videos", "search_videos", "Search YouTube videos", "youtube search_videos"),
newSchemaBoundCapabilityCommand(runCtx, "youtube.get_video_detail", "get_video_detail", "Fetch one YouTube video detail", "youtube get_video_detail"),
newSchemaBoundCapabilityCommand(runCtx, "youtube.get_video_comments", "get_video_comments", "List YouTube video comments", "youtube get_video_comments"),
newSchemaBoundCapabilityCommand(runCtx, "youtube.get_comment_replies", "get_comment_replies", "List YouTube comment replies", "youtube get_comment_replies"),
newSchemaBoundCapabilityCommand(runCtx, "youtube.get_channel_detail", "get_channel_detail", "Fetch one YouTube channel detail", "youtube get_channel_detail"),
newSchemaBoundCapabilityCommand(runCtx, "youtube.get_channel_videos", "get_channel_videos", "List YouTube channel videos or Shorts", "youtube get_channel_videos"),
newSchemaBoundCapabilityCommand(runCtx, "youtube.get_video_transcript", "get_video_transcript", "Fetch YouTube captions or transcript text", "youtube get_video_transcript"),
)
}

func (r *Root) newXiaohongshuCommand(runCtx *runContext) *cobra.Command {
return newStaticSourceCommand(
"xiaohongshu",
Expand Down
110 changes: 110 additions & 0 deletions internal/command/root_sources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1219,3 +1219,113 @@ func TestXiaohongshuGetNoteDetailCallsService(t *testing.T) {
}
assertDataOnlyOutputContains(t, stdout.String(), `"note_id":"67c1f4f1000000001a01b6d3"`)
}

func TestYouTubeSearchVideosCallsService(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if maybeServeSchema(w, r) {
return
}
if r.URL.Path != "/v1/capabilities/youtube.search_videos" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
var req struct {
Params map[string]any `json:"params"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode request body: %v", err)
}
if req.Params["query"] != "Pimax VR" || req.Params["count"] != float64(10) {
t.Fatalf("unexpected params: %#v", req.Params)
}
_, _ = w.Write([]byte(`{"ok":true,"data":[],"meta":{"capability":"youtube.search_videos","cursor":"next"}}`))
}))
defer server.Close()

var stdout bytes.Buffer
var stderr bytes.Buffer

root := NewRoot(&stdout, &stderr)
exitCode, err := root.Execute(context.Background(), []string{
"--base-url", server.URL,
"youtube", "search_videos",
"--query", "Pimax VR",
"--count", "10",
})
if err != nil {
t.Fatalf("execute root: %v", err)
}
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String())
}
assertDataOnlyOutputContains(t, stdout.String(), `"data":[]`)
}

func TestInstagramPostCommentsCallsService(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if maybeServeSchema(w, r) {
return
}
if r.URL.Path != "/v1/capabilities/instagram.get_post_comments" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
var req struct {
Params map[string]any `json:"params"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode request body: %v", err)
}
if req.Params["url"] != "https://www.instagram.com/p/CODE/" || req.Params["count"] != float64(20) {
t.Fatalf("unexpected params: %#v", req.Params)
}
_, _ = w.Write([]byte(`{"ok":true,"data":[{"comment_id":"c1","text":"nice"}],"meta":{"capability":"instagram.get_post_comments"}}`))
}))
defer server.Close()

var stdout bytes.Buffer
var stderr bytes.Buffer

root := NewRoot(&stdout, &stderr)
exitCode, err := root.Execute(context.Background(), []string{
"--base-url", server.URL,
"instagram", "get_post_comments",
"--url", "https://www.instagram.com/p/CODE/",
"--count", "20",
})
if err != nil {
t.Fatalf("execute root: %v", err)
}
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String())
}
assertDataOnlyOutputContains(t, stdout.String(), `"comment_id":"c1"`)
}

func TestInstagramSearchUsersHelpPrintsSchemaFlags(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if maybeServeSchema(w, r) {
return
}
t.Fatalf("unexpected request path: %s", r.URL.Path)
}))
defer server.Close()

var stdout bytes.Buffer
var stderr bytes.Buffer

root := NewRoot(&stdout, &stderr)
exitCode, err := root.Execute(context.Background(), []string{
"--base-url", server.URL,
"instagram", "search_users",
"--help",
})
if err != nil {
t.Fatalf("execute root: %v", err)
}
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d, stderr=%s, stdout=%s", exitCode, stderr.String(), stdout.String())
}
output := stdout.String()
if !strings.Contains(output, "Search Instagram users") || !strings.Contains(output, "--query string") || !strings.Contains(output, "--count int") {
t.Fatalf("expected instagram search_users help with schema flags, got %s", output)
}
}
16 changes: 16 additions & 0 deletions internal/command/schema_test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ func testSchemas() map[string]schema.CapabilitySchema {
"tiktok.shop_product_info": {Name: "tiktok.shop_product_info", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string"}}},
"tiktok.search_products": {Name: "tiktok.search_products", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "cursor", Type: "string"}, {Name: "offset", Type: "integer"}, {Name: "sort_type", Type: "integer", FlagName: "sort-type"}, {Name: "count", Type: "integer"}}},
"tiktok.product_reviews": {Name: "tiktok.product_reviews", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "page", Type: "integer"}, {Name: "sort", Type: "string", Enum: []string{"default", "latest"}}, {Name: "media_filter", Type: "string", Enum: []string{"all", "media", "verified"}, FlagName: "media-filter"}, {Name: "star", Type: "string", Enum: []string{"all", "1", "2", "3", "4", "5"}}, {Name: "count", Type: "integer"}}},
"youtube.search_videos": {Name: "youtube.search_videos", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"youtube.get_video_detail": {Name: "youtube.get_video_detail", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "url", Type: "string"}}},
"youtube.get_video_comments": {Name: "youtube.get_video_comments", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "url", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"youtube.get_comment_replies": {Name: "youtube.get_comment_replies", Parameters: []schema.CapabilityParam{{Name: "comment_id", Type: "string", Required: true, FlagName: "comment-id"}, {Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"youtube.get_channel_detail": {Name: "youtube.get_channel_detail", Parameters: []schema.CapabilityParam{{Name: "channel_id", Type: "string", FlagName: "channel-id"}, {Name: "handle", Type: "string"}, {Name: "url", Type: "string"}}},
"youtube.get_channel_videos": {Name: "youtube.get_channel_videos", Parameters: []schema.CapabilityParam{{Name: "channel_id", Type: "string", FlagName: "channel-id"}, {Name: "handle", Type: "string"}, {Name: "url", Type: "string"}, {Name: "content_type", Type: "string", Enum: []string{"video", "shorts", "all"}, FlagName: "content-type"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"youtube.get_video_transcript": {Name: "youtube.get_video_transcript", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", Required: true, FlagName: "video-id"}, {Name: "language", Type: "string"}, {Name: "format", Type: "string", Enum: []string{"segments", "plain"}}}},
"instagram.search_users": {Name: "instagram.search_users", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"instagram.search_reels": {Name: "instagram.search_reels", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"instagram.search_hashtags": {Name: "instagram.search_hashtags", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"instagram.get_user_profile": {Name: "instagram.get_user_profile", Parameters: []schema.CapabilityParam{{Name: "username", Type: "string"}, {Name: "user_id", Type: "string", FlagName: "user-id"}}},
"instagram.get_user_posts": {Name: "instagram.get_user_posts", Parameters: []schema.CapabilityParam{{Name: "username", Type: "string"}, {Name: "user_id", Type: "string", FlagName: "user-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"instagram.get_user_reels": {Name: "instagram.get_user_reels", Parameters: []schema.CapabilityParam{{Name: "username", Type: "string"}, {Name: "user_id", Type: "string", FlagName: "user-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"instagram.get_post_detail": {Name: "instagram.get_post_detail", Parameters: []schema.CapabilityParam{{Name: "shortcode", Type: "string"}, {Name: "media_id", Type: "string", FlagName: "media-id"}, {Name: "url", Type: "string"}}},
"instagram.get_post_comments": {Name: "instagram.get_post_comments", Parameters: []schema.CapabilityParam{{Name: "shortcode", Type: "string"}, {Name: "media_id", Type: "string", FlagName: "media-id"}, {Name: "url", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"instagram.get_comment_replies": {Name: "instagram.get_comment_replies", Parameters: []schema.CapabilityParam{{Name: "comment_id", Type: "string", Required: true, FlagName: "comment-id"}, {Name: "media_id", Type: "string", FlagName: "media-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}},
"meta_ads.search_ads": {Name: "meta_ads.search_ads", Parameters: []schema.CapabilityParam{{Name: "q", Type: "string", Required: true}, {Name: "country", Type: "string"}, {Name: "ad_type", Type: "string", Enum: []string{"all", "political_and_issue_ads", "housing_ads", "employment_ads", "credit_ads"}, FlagName: "ad-type"}, {Name: "active_status", Type: "string", Enum: []string{"active", "inactive", "all"}, FlagName: "active-status"}, {Name: "media_type", Type: "string", Enum: []string{"all", "video", "image", "meme", "image_and_meme", "none"}, FlagName: "media-type"}, {Name: "platforms", Type: "string"}, {Name: "start_date", Type: "string", FlagName: "start-date"}, {Name: "end_date", Type: "string", FlagName: "end-date"}, {Name: "next_page_token", Type: "string", FlagName: "next-page-token"}}},
"meta_ads.get_ad_detail": {Name: "meta_ads.get_ad_detail", Parameters: []schema.CapabilityParam{{Name: "ad_id", Type: "string", Required: true, FlagName: "ad-id"}}},
"douyin.search_videos": {Name: "douyin.search_videos", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "sort_type", Type: "string", Enum: []string{"comprehensive", "likes", "latest"}, FlagName: "sort-type"}, {Name: "publish_time", Type: "string", Enum: []string{"all", "1d", "1w", "6m"}, FlagName: "publish-time"}, {Name: "filter_duration", Type: "string", Enum: []string{"all", "under_1m", "1m_5m", "over_5m"}, FlagName: "filter-duration"}, {Name: "content_type", Type: "string", Enum: []string{"all", "video", "image", "article"}, FlagName: "content-type"}, {Name: "cursor", Type: "integer"}}},
Expand Down
Loading