From 1469e9c66cbf673333be554ee4c266870830c872 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 28 Jan 2026 19:08:33 -0800 Subject: [PATCH] feat(youtube): add captions, trending, and video categories tools with enhanced API coverage (#3060) * feat(youtube): add captions, trending, and video categories tools with enhanced API coverage * fix(youtube): remove captions tool (requires OAuth), fix tinybird defaults, encode pageToken --- apps/docs/content/docs/en/tools/youtube.mdx | 321 +++++++++++++------- apps/sim/blocks/blocks/youtube.ts | 133 +++++++- apps/sim/tools/registry.ts | 14 +- apps/sim/tools/youtube/channel_info.ts | 57 +++- apps/sim/tools/youtube/channel_playlists.ts | 38 ++- apps/sim/tools/youtube/channel_videos.ts | 42 ++- apps/sim/tools/youtube/comments.ts | 67 ++-- apps/sim/tools/youtube/index.ts | 4 + apps/sim/tools/youtube/playlist_items.ts | 56 +++- apps/sim/tools/youtube/search.ts | 63 +++- apps/sim/tools/youtube/trending.ts | 139 +++++++++ apps/sim/tools/youtube/types.ts | 93 +++++- apps/sim/tools/youtube/video_categories.ts | 108 +++++++ apps/sim/tools/youtube/video_details.ts | 133 +++++++- 14 files changed, 1039 insertions(+), 229 deletions(-) create mode 100644 apps/sim/tools/youtube/trending.ts create mode 100644 apps/sim/tools/youtube/video_categories.ts diff --git a/apps/docs/content/docs/en/tools/youtube.mdx b/apps/docs/content/docs/en/tools/youtube.mdx index ff580ca43..bf2f53ee6 100644 --- a/apps/docs/content/docs/en/tools/youtube.mdx +++ b/apps/docs/content/docs/en/tools/youtube.mdx @@ -26,78 +26,41 @@ In Sim, the YouTube integration enables your agents to programmatically search a ## Usage Instructions -Integrate YouTube into the workflow. Can search for videos, get video details, get channel information, get all videos from a channel, get channel playlists, get playlist items, find related videos, and get video comments. +Integrate YouTube into the workflow. Can search for videos, get trending videos, get video details, get video captions, get video categories, get channel information, get all videos from a channel, get channel playlists, get playlist items, and get video comments. ## Tools -### `youtube_search` +### `youtube_captions` -Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, and more. +List available caption tracks (subtitles/transcripts) for a YouTube video. Returns information about each caption including language, type, and whether it is auto-generated. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `query` | string | Yes | Search query for YouTube videos | -| `maxResults` | number | No | Maximum number of videos to return \(1-50\) | -| `apiKey` | string | Yes | YouTube API Key | -| `channelId` | string | No | Filter results to a specific YouTube channel ID | -| `publishedAfter` | string | No | Only return videos published after this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) | -| `publishedBefore` | string | No | Only return videos published before this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) | -| `videoDuration` | string | No | Filter by video length: "short" \(<4 min\), "medium" \(4-20 min\), "long" \(>20 min\), "any" | -| `order` | string | No | Sort results by: "date", "rating", "relevance" \(default\), "title", "videoCount", "viewCount" | -| `videoCategoryId` | string | No | Filter by YouTube category ID \(e.g., "10" for Music, "20" for Gaming\) | -| `videoDefinition` | string | No | Filter by video quality: "high" \(HD\), "standard", "any" | -| `videoCaption` | string | No | Filter by caption availability: "closedCaption" \(has captions\), "none" \(no captions\), "any" | -| `regionCode` | string | No | Return results relevant to a specific region \(ISO 3166-1 alpha-2 country code, e.g., "US", "GB"\) | -| `relevanceLanguage` | string | No | Return results most relevant to a language \(ISO 639-1 code, e.g., "en", "es"\) | -| `safeSearch` | string | No | Content filtering level: "moderate" \(default\), "none", "strict" | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `items` | array | Array of YouTube videos matching the search query | -| ↳ `videoId` | string | YouTube video ID | -| ↳ `title` | string | Video title | -| ↳ `description` | string | Video description | -| ↳ `thumbnail` | string | Video thumbnail URL | -| `totalResults` | number | Total number of search results available | -| `nextPageToken` | string | Token for accessing the next page of results | - -### `youtube_video_details` - -Get detailed information about a specific YouTube video. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `videoId` | string | Yes | YouTube video ID | +| `videoId` | string | Yes | YouTube video ID to get captions for | | `apiKey` | string | Yes | YouTube API Key | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `videoId` | string | YouTube video ID | -| `title` | string | Video title | -| `description` | string | Video description | -| `channelId` | string | Channel ID | -| `channelTitle` | string | Channel name | -| `publishedAt` | string | Published date and time | -| `duration` | string | Video duration in ISO 8601 format | -| `viewCount` | number | Number of views | -| `likeCount` | number | Number of likes | -| `commentCount` | number | Number of comments | -| `thumbnail` | string | Video thumbnail URL | -| `tags` | array | Video tags | +| `items` | array | Array of available caption tracks for the video | +| ↳ `captionId` | string | Caption track ID | +| ↳ `language` | string | Language code of the caption \(e.g., | +| ↳ `name` | string | Name/label of the caption track | +| ↳ `trackKind` | string | Type of caption track: | +| ↳ `lastUpdated` | string | When the caption was last updated | +| ↳ `isCC` | boolean | Whether this is a closed caption track | +| ↳ `isAutoSynced` | boolean | Whether the caption timing was automatically synced | +| ↳ `audioTrackType` | string | Type of audio track this caption is for | +| `totalResults` | number | Total number of caption tracks available | ### `youtube_channel_info` -Get detailed information about a YouTube channel. +Get detailed information about a YouTube channel including statistics, branding, and content details. #### Input @@ -114,43 +77,20 @@ Get detailed information about a YouTube channel. | `channelId` | string | YouTube channel ID | | `title` | string | Channel name | | `description` | string | Channel description | -| `subscriberCount` | number | Number of subscribers | -| `videoCount` | number | Number of videos | +| `subscriberCount` | number | Number of subscribers \(0 if hidden\) | +| `videoCount` | number | Number of public videos | | `viewCount` | number | Total channel views | | `publishedAt` | string | Channel creation date | -| `thumbnail` | string | Channel thumbnail URL | -| `customUrl` | string | Channel custom URL | - -### `youtube_channel_videos` - -Get all videos from a specific YouTube channel, with sorting options. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `channelId` | string | Yes | YouTube channel ID to get videos from | -| `maxResults` | number | No | Maximum number of videos to return \(1-50\) | -| `order` | string | No | Sort order: "date" \(newest first\), "rating", "relevance", "title", "viewCount" | -| `pageToken` | string | No | Page token for pagination | -| `apiKey` | string | Yes | YouTube API Key | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `items` | array | Array of videos from the channel | -| ↳ `videoId` | string | YouTube video ID | -| ↳ `title` | string | Video title | -| ↳ `description` | string | Video description | -| ↳ `thumbnail` | string | Video thumbnail URL | -| ↳ `publishedAt` | string | Video publish date | -| `totalResults` | number | Total number of videos in the channel | -| `nextPageToken` | string | Token for accessing the next page of results | +| `thumbnail` | string | Channel thumbnail/avatar URL | +| `customUrl` | string | Channel custom URL \(handle\) | +| `country` | string | Country the channel is associated with | +| `uploadsPlaylistId` | string | Playlist ID containing all channel uploads \(use with playlist_items\) | +| `bannerImageUrl` | string | Channel banner image URL | +| `hiddenSubscriberCount` | boolean | Whether the subscriber count is hidden | ### `youtube_channel_playlists` -Get all playlists from a specific YouTube channel. +Get all public playlists from a specific YouTube channel. #### Input @@ -172,19 +112,80 @@ Get all playlists from a specific YouTube channel. | ↳ `thumbnail` | string | Playlist thumbnail URL | | ↳ `itemCount` | number | Number of videos in playlist | | ↳ `publishedAt` | string | Playlist creation date | +| ↳ `channelTitle` | string | Channel name | | `totalResults` | number | Total number of playlists in the channel | | `nextPageToken` | string | Token for accessing the next page of results | -### `youtube_playlist_items` +### `youtube_channel_videos` -Get videos from a YouTube playlist. +Search for videos from a specific YouTube channel with sorting options. For complete channel video list, use channel_info to get uploadsPlaylistId, then use playlist_items. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `playlistId` | string | Yes | YouTube playlist ID | -| `maxResults` | number | No | Maximum number of videos to return | +| `channelId` | string | Yes | YouTube channel ID to get videos from | +| `maxResults` | number | No | Maximum number of videos to return \(1-50\) | +| `order` | string | No | Sort order: "date" \(newest first, default\), "rating", "relevance", "title", "viewCount" | +| `pageToken` | string | No | Page token for pagination | +| `apiKey` | string | Yes | YouTube API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of videos from the channel | +| ↳ `videoId` | string | YouTube video ID | +| ↳ `title` | string | Video title | +| ↳ `description` | string | Video description | +| ↳ `thumbnail` | string | Video thumbnail URL | +| ↳ `publishedAt` | string | Video publish date | +| ↳ `channelTitle` | string | Channel name | +| `totalResults` | number | Total number of videos in the channel | +| `nextPageToken` | string | Token for accessing the next page of results | + +### `youtube_comments` + +Get top-level comments from a YouTube video with author details and engagement. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `videoId` | string | Yes | YouTube video ID | +| `maxResults` | number | No | Maximum number of comments to return \(1-100\) | +| `order` | string | No | Order of comments: "time" \(newest first\) or "relevance" \(most relevant first\) | +| `pageToken` | string | No | Page token for pagination | +| `apiKey` | string | Yes | YouTube API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of top-level comments from the video | +| ↳ `commentId` | string | Comment ID | +| ↳ `authorDisplayName` | string | Comment author display name | +| ↳ `authorChannelUrl` | string | Comment author channel URL | +| ↳ `authorProfileImageUrl` | string | Comment author profile image URL | +| ↳ `textDisplay` | string | Comment text \(HTML formatted\) | +| ↳ `textOriginal` | string | Comment text \(plain text\) | +| ↳ `likeCount` | number | Number of likes on the comment | +| ↳ `publishedAt` | string | When the comment was posted | +| ↳ `updatedAt` | string | When the comment was last edited | +| ↳ `replyCount` | number | Number of replies to this comment | +| `totalResults` | number | Total number of comment threads available | +| `nextPageToken` | string | Token for accessing the next page of results | + +### `youtube_playlist_items` + +Get videos from a YouTube playlist. Can be used with a channel uploads playlist to get all channel videos. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `playlistId` | string | Yes | YouTube playlist ID. Use uploadsPlaylistId from channel_info to get all channel videos. | +| `maxResults` | number | No | Maximum number of videos to return \(1-50\) | | `pageToken` | string | No | Page token for pagination | | `apiKey` | string | Yes | YouTube API Key | @@ -198,22 +199,65 @@ Get videos from a YouTube playlist. | ↳ `description` | string | Video description | | ↳ `thumbnail` | string | Video thumbnail URL | | ↳ `publishedAt` | string | Date added to playlist | -| ↳ `channelTitle` | string | Channel name | -| ↳ `position` | number | Position in playlist | +| ↳ `channelTitle` | string | Playlist owner channel name | +| ↳ `position` | number | Position in playlist \(0-indexed\) | +| ↳ `videoOwnerChannelId` | string | Channel ID of the video owner | +| ↳ `videoOwnerChannelTitle` | string | Channel name of the video owner | | `totalResults` | number | Total number of items in playlist | | `nextPageToken` | string | Token for accessing the next page of results | -### `youtube_comments` +### `youtube_search` -Get comments from a YouTube video. +Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, live streams, and more. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `videoId` | string | Yes | YouTube video ID | -| `maxResults` | number | No | Maximum number of comments to return | -| `order` | string | No | Order of comments: time or relevance | +| `query` | string | Yes | Search query for YouTube videos | +| `maxResults` | number | No | Maximum number of videos to return \(1-50\) | +| `pageToken` | string | No | Page token for pagination \(use nextPageToken from previous response\) | +| `apiKey` | string | Yes | YouTube API Key | +| `channelId` | string | No | Filter results to a specific YouTube channel ID | +| `publishedAfter` | string | No | Only return videos published after this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) | +| `publishedBefore` | string | No | Only return videos published before this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) | +| `videoDuration` | string | No | Filter by video length: "short" \(<4 min\), "medium" \(4-20 min\), "long" \(>20 min\), "any" | +| `order` | string | No | Sort results by: "date", "rating", "relevance" \(default\), "title", "videoCount", "viewCount" | +| `videoCategoryId` | string | No | Filter by YouTube category ID \(e.g., "10" for Music, "20" for Gaming\). Use video_categories to list IDs. | +| `videoDefinition` | string | No | Filter by video quality: "high" \(HD\), "standard", "any" | +| `videoCaption` | string | No | Filter by caption availability: "closedCaption" \(has captions\), "none" \(no captions\), "any" | +| `eventType` | string | No | Filter by live broadcast status: "live" \(currently live\), "upcoming" \(scheduled\), "completed" \(past streams\) | +| `regionCode` | string | No | Return results relevant to a specific region \(ISO 3166-1 alpha-2 country code, e.g., "US", "GB"\) | +| `relevanceLanguage` | string | No | Return results most relevant to a language \(ISO 639-1 code, e.g., "en", "es"\) | +| `safeSearch` | string | No | Content filtering level: "moderate" \(default\), "none", "strict" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of YouTube videos matching the search query | +| ↳ `videoId` | string | YouTube video ID | +| ↳ `title` | string | Video title | +| ↳ `description` | string | Video description | +| ↳ `thumbnail` | string | Video thumbnail URL | +| ↳ `channelId` | string | Channel ID that uploaded the video | +| ↳ `channelTitle` | string | Channel name | +| ↳ `publishedAt` | string | Video publish date | +| ↳ `liveBroadcastContent` | string | Live broadcast status: | +| `totalResults` | number | Total number of search results available | +| `nextPageToken` | string | Token for accessing the next page of results | + +### `youtube_trending` + +Get the most popular/trending videos on YouTube. Can filter by region and video category. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `regionCode` | string | No | ISO 3166-1 alpha-2 country code to get trending videos for \(e.g., "US", "GB", "JP"\). Defaults to US. | +| `videoCategoryId` | string | No | Filter by video category ID \(e.g., "10" for Music, "20" for Gaming, "17" for Sports\) | +| `maxResults` | number | No | Maximum number of trending videos to return \(1-50\) | | `pageToken` | string | No | Page token for pagination | | `apiKey` | string | Yes | YouTube API Key | @@ -221,17 +265,84 @@ Get comments from a YouTube video. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `items` | array | Array of comments from the video | -| ↳ `commentId` | string | Comment ID | -| ↳ `authorDisplayName` | string | Comment author name | -| ↳ `authorChannelUrl` | string | Comment author channel URL | -| ↳ `textDisplay` | string | Comment text \(HTML formatted\) | -| ↳ `textOriginal` | string | Comment text \(plain text\) | +| `items` | array | Array of trending videos | +| ↳ `videoId` | string | YouTube video ID | +| ↳ `title` | string | Video title | +| ↳ `description` | string | Video description | +| ↳ `thumbnail` | string | Video thumbnail URL | +| ↳ `channelId` | string | Channel ID | +| ↳ `channelTitle` | string | Channel name | +| ↳ `publishedAt` | string | Video publish date | +| ↳ `viewCount` | number | Number of views | | ↳ `likeCount` | number | Number of likes | -| ↳ `publishedAt` | string | Comment publish date | -| ↳ `updatedAt` | string | Comment last updated date | -| ↳ `replyCount` | number | Number of replies | -| `totalResults` | number | Total number of comments | +| ↳ `commentCount` | number | Number of comments | +| ↳ `duration` | string | Video duration in ISO 8601 format | +| `totalResults` | number | Total number of trending videos available | | `nextPageToken` | string | Token for accessing the next page of results | +### `youtube_video_categories` + +Get a list of video categories available on YouTube. Use this to discover valid category IDs for filtering search and trending results. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `regionCode` | string | No | ISO 3166-1 alpha-2 country code to get categories for \(e.g., "US", "GB", "JP"\). Defaults to US. | +| `hl` | string | No | Language for category titles \(e.g., "en", "es", "fr"\). Defaults to English. | +| `apiKey` | string | Yes | YouTube API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of video categories available in the specified region | +| ↳ `categoryId` | string | Category ID to use in search/trending filters \(e.g., | +| ↳ `title` | string | Human-readable category name | +| ↳ `assignable` | boolean | Whether videos can be tagged with this category | +| `totalResults` | number | Total number of categories available | + +### `youtube_video_details` + +Get detailed information about a specific YouTube video including statistics, content details, live streaming info, and metadata. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `videoId` | string | Yes | YouTube video ID | +| `apiKey` | string | Yes | YouTube API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `videoId` | string | YouTube video ID | +| `title` | string | Video title | +| `description` | string | Video description | +| `channelId` | string | Channel ID | +| `channelTitle` | string | Channel name | +| `publishedAt` | string | Published date and time | +| `duration` | string | Video duration in ISO 8601 format \(e.g., | +| `viewCount` | number | Number of views | +| `likeCount` | number | Number of likes | +| `commentCount` | number | Number of comments | +| `favoriteCount` | number | Number of times added to favorites | +| `thumbnail` | string | Video thumbnail URL | +| `tags` | array | Video tags | +| `categoryId` | string | YouTube video category ID | +| `definition` | string | Video definition: | +| `caption` | string | Whether captions are available: | +| `licensedContent` | boolean | Whether the video is licensed content | +| `privacyStatus` | string | Video privacy status: | +| `liveBroadcastContent` | string | Live broadcast status: | +| `defaultLanguage` | string | Default language of the video metadata | +| `defaultAudioLanguage` | string | Default audio language of the video | +| `isLiveContent` | boolean | Whether this video is or was a live stream | +| `scheduledStartTime` | string | Scheduled start time for upcoming live streams \(ISO 8601\) | +| `actualStartTime` | string | When the live stream actually started \(ISO 8601\) | +| `actualEndTime` | string | When the live stream ended \(ISO 8601\) | +| `concurrentViewers` | number | Current number of viewers \(only for active live streams\) | +| `activeLiveChatId` | string | Live chat ID for the stream \(only for active live streams\) | + diff --git a/apps/sim/blocks/blocks/youtube.ts b/apps/sim/blocks/blocks/youtube.ts index e8f9be426..b86995445 100644 --- a/apps/sim/blocks/blocks/youtube.ts +++ b/apps/sim/blocks/blocks/youtube.ts @@ -9,7 +9,7 @@ export const YouTubeBlock: BlockConfig = { description: 'Interact with YouTube videos, channels, and playlists', authMode: AuthMode.ApiKey, longDescription: - 'Integrate YouTube into the workflow. Can search for videos, get video details, get channel information, get all videos from a channel, get channel playlists, get playlist items, find related videos, and get video comments.', + 'Integrate YouTube into the workflow. Can search for videos, get trending videos, get video details, get video categories, get channel information, get all videos from a channel, get channel playlists, get playlist items, and get video comments.', docsLink: 'https://docs.sim.ai/tools/youtube', category: 'tools', bgColor: '#FF0000', @@ -21,7 +21,9 @@ export const YouTubeBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Search Videos', id: 'youtube_search' }, + { label: 'Get Trending Videos', id: 'youtube_trending' }, { label: 'Get Video Details', id: 'youtube_video_details' }, + { label: 'Get Video Categories', id: 'youtube_video_categories' }, { label: 'Get Channel Info', id: 'youtube_channel_info' }, { label: 'Get Channel Videos', id: 'youtube_channel_videos' }, { label: 'Get Channel Playlists', id: 'youtube_channel_playlists' }, @@ -49,6 +51,13 @@ export const YouTubeBlock: BlockConfig = { integer: true, condition: { field: 'operation', value: 'youtube_search' }, }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for pagination (from nextPageToken)', + condition: { field: 'operation', value: 'youtube_search' }, + }, { id: 'channelId', title: 'Filter by Channel ID', @@ -56,6 +65,19 @@ export const YouTubeBlock: BlockConfig = { placeholder: 'Filter results to a specific channel', condition: { field: 'operation', value: 'youtube_search' }, }, + { + id: 'eventType', + title: 'Live Stream Filter', + type: 'dropdown', + options: [ + { label: 'All Videos', id: '' }, + { label: 'Currently Live', id: 'live' }, + { label: 'Upcoming Streams', id: 'upcoming' }, + { label: 'Past Streams', id: 'completed' }, + ], + value: () => '', + condition: { field: 'operation', value: 'youtube_search' }, + }, { id: 'publishedAfter', title: 'Published After', @@ -131,7 +153,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, id: 'videoCategoryId', title: 'Category ID', type: 'short-input', - placeholder: '10 for Music, 20 for Gaming', + placeholder: 'Use Get Video Categories to find IDs', condition: { field: 'operation', value: 'youtube_search' }, }, { @@ -163,7 +185,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Region Code', type: 'short-input', placeholder: 'US, GB, JP', - condition: { field: 'operation', value: 'youtube_search' }, + condition: { + field: 'operation', + value: ['youtube_search', 'youtube_trending', 'youtube_video_categories'], + }, }, { id: 'relevanceLanguage', @@ -184,6 +209,31 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, value: () => 'moderate', condition: { field: 'operation', value: 'youtube_search' }, }, + // Get Trending Videos operation inputs + { + id: 'maxResults', + title: 'Max Results', + type: 'slider', + min: 1, + max: 50, + step: 1, + integer: true, + condition: { field: 'operation', value: 'youtube_trending' }, + }, + { + id: 'videoCategoryId', + title: 'Category ID', + type: 'short-input', + placeholder: 'Use Get Video Categories to find IDs', + condition: { field: 'operation', value: 'youtube_trending' }, + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for pagination (from nextPageToken)', + condition: { field: 'operation', value: 'youtube_trending' }, + }, // Get Video Details operation inputs { id: 'videoId', @@ -193,6 +243,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, required: true, condition: { field: 'operation', value: 'youtube_video_details' }, }, + // Get Video Categories operation inputs + { + id: 'hl', + title: 'Language', + type: 'short-input', + placeholder: 'en, es, fr (for category names)', + condition: { field: 'operation', value: 'youtube_video_categories' }, + }, // Get Channel Info operation inputs { id: 'channelId', @@ -241,6 +299,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, value: () => 'date', condition: { field: 'operation', value: 'youtube_channel_videos' }, }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for pagination (from nextPageToken)', + condition: { field: 'operation', value: 'youtube_channel_videos' }, + }, // Get Channel Playlists operation inputs { id: 'channelId', @@ -260,6 +325,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, integer: true, condition: { field: 'operation', value: 'youtube_channel_playlists' }, }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for pagination (from nextPageToken)', + condition: { field: 'operation', value: 'youtube_channel_playlists' }, + }, // Get Playlist Items operation inputs { id: 'playlistId', @@ -279,6 +351,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, integer: true, condition: { field: 'operation', value: 'youtube_playlist_items' }, }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for pagination (from nextPageToken)', + condition: { field: 'operation', value: 'youtube_playlist_items' }, + }, // Get Video Comments operation inputs { id: 'videoId', @@ -309,6 +388,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, value: () => 'relevance', condition: { field: 'operation', value: 'youtube_comments' }, }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for pagination (from nextPageToken)', + condition: { field: 'operation', value: 'youtube_comments' }, + }, // API Key (common to all operations) { id: 'apiKey', @@ -321,13 +407,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, ], tools: { access: [ - 'youtube_search', - 'youtube_video_details', 'youtube_channel_info', - 'youtube_channel_videos', 'youtube_channel_playlists', - 'youtube_playlist_items', + 'youtube_channel_videos', 'youtube_comments', + 'youtube_playlist_items', + 'youtube_search', + 'youtube_trending', + 'youtube_video_categories', + 'youtube_video_details', ], config: { tool: (params) => { @@ -339,8 +427,12 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, switch (params.operation) { case 'youtube_search': return 'youtube_search' + case 'youtube_trending': + return 'youtube_trending' case 'youtube_video_details': return 'youtube_video_details' + case 'youtube_video_categories': + return 'youtube_video_categories' case 'youtube_channel_info': return 'youtube_channel_info' case 'youtube_channel_videos': @@ -363,6 +455,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, // Search Videos query: { type: 'string', description: 'Search query' }, maxResults: { type: 'number', description: 'Maximum number of results' }, + pageToken: { type: 'string', description: 'Page token for pagination' }, // Search Filters publishedAfter: { type: 'string', description: 'Published after date (RFC 3339)' }, publishedBefore: { type: 'string', description: 'Published before date (RFC 3339)' }, @@ -370,9 +463,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, videoCategoryId: { type: 'string', description: 'YouTube category ID' }, videoDefinition: { type: 'string', description: 'Video quality filter' }, videoCaption: { type: 'string', description: 'Caption availability filter' }, + eventType: { type: 'string', description: 'Live stream filter (live/upcoming/completed)' }, regionCode: { type: 'string', description: 'Region code (ISO 3166-1)' }, relevanceLanguage: { type: 'string', description: 'Language code (ISO 639-1)' }, safeSearch: { type: 'string', description: 'Safe search level' }, + hl: { type: 'string', description: 'Language for category names' }, // Video Details & Comments videoId: { type: 'string', description: 'YouTube video ID' }, // Channel Info @@ -384,7 +479,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, order: { type: 'string', description: 'Sort order' }, }, outputs: { - // Search Videos & Playlist Items + // Search Videos, Trending, Playlist Items, Captions, Categories items: { type: 'json', description: 'List of items returned' }, totalResults: { type: 'number', description: 'Total number of results' }, nextPageToken: { type: 'string', description: 'Token for next page' }, @@ -399,11 +494,33 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, viewCount: { type: 'number', description: 'View count' }, likeCount: { type: 'number', description: 'Like count' }, commentCount: { type: 'number', description: 'Comment count' }, + favoriteCount: { type: 'number', description: 'Favorite count' }, thumbnail: { type: 'string', description: 'Thumbnail URL' }, tags: { type: 'json', description: 'Video tags' }, + categoryId: { type: 'string', description: 'Video category ID' }, + definition: { type: 'string', description: 'Video definition (hd/sd)' }, + caption: { type: 'string', description: 'Has captions (true/false)' }, + licensedContent: { type: 'boolean', description: 'Is licensed content' }, + privacyStatus: { type: 'string', description: 'Privacy status' }, + liveBroadcastContent: { type: 'string', description: 'Live broadcast status' }, + defaultLanguage: { type: 'string', description: 'Default language' }, + defaultAudioLanguage: { type: 'string', description: 'Default audio language' }, + // Live Streaming Details + isLiveContent: { type: 'boolean', description: 'Whether video is/was a live stream' }, + scheduledStartTime: { type: 'string', description: 'Scheduled start time for live streams' }, + actualStartTime: { type: 'string', description: 'Actual start time of live stream' }, + actualEndTime: { type: 'string', description: 'End time of live stream' }, + concurrentViewers: { type: 'number', description: 'Current viewers (live only)' }, + activeLiveChatId: { type: 'string', description: 'Live chat ID' }, // Channel Info subscriberCount: { type: 'number', description: 'Subscriber count' }, videoCount: { type: 'number', description: 'Total video count' }, customUrl: { type: 'string', description: 'Channel custom URL' }, + country: { type: 'string', description: 'Channel country' }, + uploadsPlaylistId: { type: 'string', description: 'Uploads playlist ID' }, + bannerImageUrl: { type: 'string', description: 'Channel banner URL' }, + hiddenSubscriberCount: { type: 'boolean', description: 'Is subscriber count hidden' }, + // Video Categories + assignable: { type: 'boolean', description: 'Whether category can be assigned' }, }, } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 29b0f5ae2..250a9bbf6 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1648,6 +1648,8 @@ import { youtubeCommentsTool, youtubePlaylistItemsTool, youtubeSearchTool, + youtubeTrendingTool, + youtubeVideoCategoriesTool, youtubeVideoDetailsTool, } from '@/tools/youtube' import { @@ -1982,13 +1984,15 @@ export const tools: Record = { typeform_create_form: typeformCreateFormTool, typeform_update_form: typeformUpdateFormTool, typeform_delete_form: typeformDeleteFormTool, - youtube_search: youtubeSearchTool, - youtube_video_details: youtubeVideoDetailsTool, youtube_channel_info: youtubeChannelInfoTool, - youtube_playlist_items: youtubePlaylistItemsTool, - youtube_comments: youtubeCommentsTool, - youtube_channel_videos: youtubeChannelVideosTool, youtube_channel_playlists: youtubeChannelPlaylistsTool, + youtube_channel_videos: youtubeChannelVideosTool, + youtube_comments: youtubeCommentsTool, + youtube_playlist_items: youtubePlaylistItemsTool, + youtube_search: youtubeSearchTool, + youtube_trending: youtubeTrendingTool, + youtube_video_categories: youtubeVideoCategoriesTool, + youtube_video_details: youtubeVideoDetailsTool, notion_read: notionReadTool, notion_read_database: notionReadDatabaseTool, notion_write: notionWriteTool, diff --git a/apps/sim/tools/youtube/channel_info.ts b/apps/sim/tools/youtube/channel_info.ts index 0bdd7e81c..c4294786b 100644 --- a/apps/sim/tools/youtube/channel_info.ts +++ b/apps/sim/tools/youtube/channel_info.ts @@ -7,8 +7,9 @@ export const youtubeChannelInfoTool: ToolConfig< > = { id: 'youtube_channel_info', name: 'YouTube Channel Info', - description: 'Get detailed information about a YouTube channel.', - version: '1.0.0', + description: + 'Get detailed information about a YouTube channel including statistics, branding, and content details.', + version: '1.1.0', params: { channelId: { type: 'string', @@ -33,11 +34,11 @@ export const youtubeChannelInfoTool: ToolConfig< request: { url: (params: YouTubeChannelInfoParams) => { let url = - 'https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,contentDetails' + 'https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,contentDetails,brandingSettings' if (params.channelId) { - url += `&id=${params.channelId}` + url += `&id=${encodeURIComponent(params.channelId)}` } else if (params.username) { - url += `&forUsername=${params.username}` + url += `&forUsername=${encodeURIComponent(params.username)}` } url += `&key=${params.apiKey}` return url @@ -63,6 +64,11 @@ export const youtubeChannelInfoTool: ToolConfig< viewCount: 0, publishedAt: '', thumbnail: '', + customUrl: null, + country: null, + uploadsPlaylistId: null, + bannerImageUrl: null, + hiddenSubscriberCount: false, }, error: 'Channel not found', } @@ -72,19 +78,23 @@ export const youtubeChannelInfoTool: ToolConfig< return { success: true, output: { - channelId: item.id, - title: item.snippet?.title || '', - description: item.snippet?.description || '', + channelId: item.id ?? '', + title: item.snippet?.title ?? '', + description: item.snippet?.description ?? '', subscriberCount: Number(item.statistics?.subscriberCount || 0), videoCount: Number(item.statistics?.videoCount || 0), viewCount: Number(item.statistics?.viewCount || 0), - publishedAt: item.snippet?.publishedAt || '', + publishedAt: item.snippet?.publishedAt ?? '', thumbnail: item.snippet?.thumbnails?.high?.url || item.snippet?.thumbnails?.medium?.url || item.snippet?.thumbnails?.default?.url || '', - customUrl: item.snippet?.customUrl, + customUrl: item.snippet?.customUrl ?? null, + country: item.snippet?.country ?? null, + uploadsPlaylistId: item.contentDetails?.relatedPlaylists?.uploads ?? null, + bannerImageUrl: item.brandingSettings?.image?.bannerExternalUrl ?? null, + hiddenSubscriberCount: item.statistics?.hiddenSubscriberCount ?? false, }, } }, @@ -104,11 +114,11 @@ export const youtubeChannelInfoTool: ToolConfig< }, subscriberCount: { type: 'number', - description: 'Number of subscribers', + description: 'Number of subscribers (0 if hidden)', }, videoCount: { type: 'number', - description: 'Number of videos', + description: 'Number of public videos', }, viewCount: { type: 'number', @@ -120,12 +130,31 @@ export const youtubeChannelInfoTool: ToolConfig< }, thumbnail: { type: 'string', - description: 'Channel thumbnail URL', + description: 'Channel thumbnail/avatar URL', }, customUrl: { type: 'string', - description: 'Channel custom URL', + description: 'Channel custom URL (handle)', optional: true, }, + country: { + type: 'string', + description: 'Country the channel is associated with', + optional: true, + }, + uploadsPlaylistId: { + type: 'string', + description: 'Playlist ID containing all channel uploads (use with playlist_items)', + optional: true, + }, + bannerImageUrl: { + type: 'string', + description: 'Channel banner image URL', + optional: true, + }, + hiddenSubscriberCount: { + type: 'boolean', + description: 'Whether the subscriber count is hidden', + }, }, } diff --git a/apps/sim/tools/youtube/channel_playlists.ts b/apps/sim/tools/youtube/channel_playlists.ts index c2339a085..2f1b5fbc9 100644 --- a/apps/sim/tools/youtube/channel_playlists.ts +++ b/apps/sim/tools/youtube/channel_playlists.ts @@ -10,8 +10,8 @@ export const youtubeChannelPlaylistsTool: ToolConfig< > = { id: 'youtube_channel_playlists', name: 'YouTube Channel Playlists', - description: 'Get all playlists from a specific YouTube channel.', - version: '1.0.0', + description: 'Get all public playlists from a specific YouTube channel.', + version: '1.1.0', params: { channelId: { type: 'string', @@ -47,7 +47,7 @@ export const youtubeChannelPlaylistsTool: ToolConfig< )}&key=${params.apiKey}` url += `&maxResults=${Number(params.maxResults || 10)}` if (params.pageToken) { - url += `&pageToken=${params.pageToken}` + url += `&pageToken=${encodeURIComponent(params.pageToken)}` } return url }, @@ -60,36 +60,49 @@ export const youtubeChannelPlaylistsTool: ToolConfig< transformResponse: async (response: Response): Promise => { const data = await response.json() - if (!data.items) { + if (data.error) { return { success: false, output: { items: [], totalResults: 0, + nextPageToken: null, + }, + error: data.error.message || 'Failed to fetch channel playlists', + } + } + + if (!data.items || data.items.length === 0) { + return { + success: true, + output: { + items: [], + totalResults: 0, + nextPageToken: null, }, - error: 'No playlists found', } } const items = (data.items || []).map((item: any) => ({ - playlistId: item.id, - title: item.snippet?.title || '', - description: item.snippet?.description || '', + playlistId: item.id ?? '', + title: item.snippet?.title ?? '', + description: item.snippet?.description ?? '', thumbnail: item.snippet?.thumbnails?.medium?.url || item.snippet?.thumbnails?.default?.url || item.snippet?.thumbnails?.high?.url || '', - itemCount: item.contentDetails?.itemCount || 0, - publishedAt: item.snippet?.publishedAt || '', + itemCount: Number(item.contentDetails?.itemCount || 0), + publishedAt: item.snippet?.publishedAt ?? '', + channelTitle: item.snippet?.channelTitle ?? '', })) return { success: true, output: { items, - totalResults: data.pageInfo?.totalResults || 0, - nextPageToken: data.nextPageToken, + totalResults: data.pageInfo?.totalResults || items.length, + nextPageToken: data.nextPageToken ?? null, }, } }, @@ -107,6 +120,7 @@ export const youtubeChannelPlaylistsTool: ToolConfig< thumbnail: { type: 'string', description: 'Playlist thumbnail URL' }, itemCount: { type: 'number', description: 'Number of videos in playlist' }, publishedAt: { type: 'string', description: 'Playlist creation date' }, + channelTitle: { type: 'string', description: 'Channel name' }, }, }, }, diff --git a/apps/sim/tools/youtube/channel_videos.ts b/apps/sim/tools/youtube/channel_videos.ts index f0de06210..a44be50ff 100644 --- a/apps/sim/tools/youtube/channel_videos.ts +++ b/apps/sim/tools/youtube/channel_videos.ts @@ -10,8 +10,9 @@ export const youtubeChannelVideosTool: ToolConfig< > = { id: 'youtube_channel_videos', name: 'YouTube Channel Videos', - description: 'Get all videos from a specific YouTube channel, with sorting options.', - version: '1.0.0', + description: + 'Search for videos from a specific YouTube channel with sorting options. For complete channel video list, use channel_info to get uploadsPlaylistId, then use playlist_items.', + version: '1.1.0', params: { channelId: { type: 'string', @@ -30,7 +31,8 @@ export const youtubeChannelVideosTool: ToolConfig< type: 'string', required: false, visibility: 'user-or-llm', - description: 'Sort order: "date" (newest first), "rating", "relevance", "title", "viewCount"', + description: + 'Sort order: "date" (newest first, default), "rating", "relevance", "title", "viewCount"', }, pageToken: { type: 'string', @@ -52,11 +54,9 @@ export const youtubeChannelVideosTool: ToolConfig< params.channelId )}&key=${params.apiKey}` url += `&maxResults=${Number(params.maxResults || 10)}` - if (params.order) { - url += `&order=${params.order}` - } + url += `&order=${params.order || 'date'}` if (params.pageToken) { - url += `&pageToken=${params.pageToken}` + url += `&pageToken=${encodeURIComponent(params.pageToken)}` } return url }, @@ -68,23 +68,38 @@ export const youtubeChannelVideosTool: ToolConfig< transformResponse: async (response: Response): Promise => { const data = await response.json() + + if (data.error) { + return { + success: false, + output: { + items: [], + totalResults: 0, + nextPageToken: null, + }, + error: data.error.message || 'Failed to fetch channel videos', + } + } + const items = (data.items || []).map((item: any) => ({ - videoId: item.id?.videoId, - title: item.snippet?.title, - description: item.snippet?.description, + videoId: item.id?.videoId ?? '', + title: item.snippet?.title ?? '', + description: item.snippet?.description ?? '', thumbnail: item.snippet?.thumbnails?.medium?.url || item.snippet?.thumbnails?.default?.url || item.snippet?.thumbnails?.high?.url || '', - publishedAt: item.snippet?.publishedAt || '', + publishedAt: item.snippet?.publishedAt ?? '', + channelTitle: item.snippet?.channelTitle ?? '', })) + return { success: true, output: { items, - totalResults: data.pageInfo?.totalResults || 0, - nextPageToken: data.nextPageToken, + totalResults: data.pageInfo?.totalResults || items.length, + nextPageToken: data.nextPageToken ?? null, }, } }, @@ -101,6 +116,7 @@ export const youtubeChannelVideosTool: ToolConfig< description: { type: 'string', description: 'Video description' }, thumbnail: { type: 'string', description: 'Video thumbnail URL' }, publishedAt: { type: 'string', description: 'Video publish date' }, + channelTitle: { type: 'string', description: 'Channel name' }, }, }, }, diff --git a/apps/sim/tools/youtube/comments.ts b/apps/sim/tools/youtube/comments.ts index 9f9f6674e..aaf601bae 100644 --- a/apps/sim/tools/youtube/comments.ts +++ b/apps/sim/tools/youtube/comments.ts @@ -4,8 +4,8 @@ import type { YouTubeCommentsParams, YouTubeCommentsResponse } from '@/tools/you export const youtubeCommentsTool: ToolConfig = { id: 'youtube_comments', name: 'YouTube Video Comments', - description: 'Get comments from a YouTube video.', - version: '1.0.0', + description: 'Get top-level comments from a YouTube video with author details and engagement.', + version: '1.1.0', params: { videoId: { type: 'string', @@ -18,14 +18,14 @@ export const youtubeCommentsTool: ToolConfig { - let url = `https://www.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&videoId=${params.videoId}&key=${params.apiKey}` + let url = `https://www.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&videoId=${encodeURIComponent(params.videoId)}&key=${params.apiKey}` url += `&maxResults=${Number(params.maxResults || 20)}` url += `&order=${params.order || 'relevance'}` if (params.pageToken) { - url += `&pageToken=${params.pageToken}` + url += `&pageToken=${encodeURIComponent(params.pageToken)}` } return url }, @@ -60,18 +60,31 @@ export const youtubeCommentsTool: ToolConfig => { const data = await response.json() + if (data.error) { + return { + success: false, + output: { + items: [], + totalResults: 0, + nextPageToken: null, + }, + error: data.error.message || 'Failed to fetch comments', + } + } + const items = (data.items || []).map((item: any) => { const topLevelComment = item.snippet?.topLevelComment?.snippet return { - commentId: item.snippet?.topLevelComment?.id || item.id, - authorDisplayName: topLevelComment?.authorDisplayName || '', - authorChannelUrl: topLevelComment?.authorChannelUrl || '', - textDisplay: topLevelComment?.textDisplay || '', - textOriginal: topLevelComment?.textOriginal || '', - likeCount: topLevelComment?.likeCount || 0, - publishedAt: topLevelComment?.publishedAt || '', - updatedAt: topLevelComment?.updatedAt || '', - replyCount: item.snippet?.totalReplyCount || 0, + commentId: item.snippet?.topLevelComment?.id ?? item.id ?? '', + authorDisplayName: topLevelComment?.authorDisplayName ?? '', + authorChannelUrl: topLevelComment?.authorChannelUrl ?? '', + authorProfileImageUrl: topLevelComment?.authorProfileImageUrl ?? '', + textDisplay: topLevelComment?.textDisplay ?? '', + textOriginal: topLevelComment?.textOriginal ?? '', + likeCount: Number(topLevelComment?.likeCount || 0), + publishedAt: topLevelComment?.publishedAt ?? '', + updatedAt: topLevelComment?.updatedAt ?? '', + replyCount: Number(item.snippet?.totalReplyCount || 0), } }) @@ -79,8 +92,8 @@ export const youtubeCommentsTool: ToolConfig = { id: 'youtube_playlist_items', name: 'YouTube Playlist Items', - description: 'Get videos from a YouTube playlist.', - version: '1.0.0', + description: + 'Get videos from a YouTube playlist. Can be used with a channel uploads playlist to get all channel videos.', + version: '1.1.0', params: { playlistId: { type: 'string', required: true, visibility: 'user-or-llm', - description: 'YouTube playlist ID', + description: + 'YouTube playlist ID. Use uploadsPlaylistId from channel_info to get all channel videos.', }, maxResults: { type: 'number', required: false, visibility: 'user-only', default: 10, - description: 'Maximum number of videos to return', + description: 'Maximum number of videos to return (1-50)', }, pageToken: { type: 'string', @@ -42,10 +44,10 @@ export const youtubePlaylistItemsTool: ToolConfig< request: { url: (params: YouTubePlaylistItemsParams) => { - let url = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=${params.playlistId}&key=${params.apiKey}` + let url = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=${encodeURIComponent(params.playlistId)}&key=${params.apiKey}` url += `&maxResults=${Number(params.maxResults || 10)}` if (params.pageToken) { - url += `&pageToken=${params.pageToken}` + url += `&pageToken=${encodeURIComponent(params.pageToken)}` } return url }, @@ -58,26 +60,40 @@ export const youtubePlaylistItemsTool: ToolConfig< transformResponse: async (response: Response): Promise => { const data = await response.json() + if (data.error) { + return { + success: false, + output: { + items: [], + totalResults: 0, + nextPageToken: null, + }, + error: data.error.message || 'Failed to fetch playlist items', + } + } + const items = (data.items || []).map((item: any, index: number) => ({ - videoId: item.contentDetails?.videoId || item.snippet?.resourceId?.videoId, - title: item.snippet?.title || '', - description: item.snippet?.description || '', + videoId: item.contentDetails?.videoId ?? item.snippet?.resourceId?.videoId ?? '', + title: item.snippet?.title ?? '', + description: item.snippet?.description ?? '', thumbnail: item.snippet?.thumbnails?.medium?.url || item.snippet?.thumbnails?.default?.url || item.snippet?.thumbnails?.high?.url || '', - publishedAt: item.snippet?.publishedAt || '', - channelTitle: item.snippet?.channelTitle || '', + publishedAt: item.snippet?.publishedAt ?? '', + channelTitle: item.snippet?.channelTitle ?? '', position: item.snippet?.position ?? index, + videoOwnerChannelId: item.snippet?.videoOwnerChannelId ?? null, + videoOwnerChannelTitle: item.snippet?.videoOwnerChannelTitle ?? null, })) return { success: true, output: { items, - totalResults: data.pageInfo?.totalResults || 0, - nextPageToken: data.nextPageToken, + totalResults: data.pageInfo?.totalResults || items.length, + nextPageToken: data.nextPageToken ?? null, }, } }, @@ -94,8 +110,18 @@ export const youtubePlaylistItemsTool: ToolConfig< description: { type: 'string', description: 'Video description' }, thumbnail: { type: 'string', description: 'Video thumbnail URL' }, publishedAt: { type: 'string', description: 'Date added to playlist' }, - channelTitle: { type: 'string', description: 'Channel name' }, - position: { type: 'number', description: 'Position in playlist' }, + channelTitle: { type: 'string', description: 'Playlist owner channel name' }, + position: { type: 'number', description: 'Position in playlist (0-indexed)' }, + videoOwnerChannelId: { + type: 'string', + description: 'Channel ID of the video owner', + optional: true, + }, + videoOwnerChannelTitle: { + type: 'string', + description: 'Channel name of the video owner', + optional: true, + }, }, }, }, diff --git a/apps/sim/tools/youtube/search.ts b/apps/sim/tools/youtube/search.ts index 0bb464492..aad342efd 100644 --- a/apps/sim/tools/youtube/search.ts +++ b/apps/sim/tools/youtube/search.ts @@ -5,8 +5,8 @@ export const youtubeSearchTool: ToolConfig => { const data = await response.json() + + if (data.error) { + return { + success: false, + output: { + items: [], + totalResults: 0, + nextPageToken: null, + }, + error: data.error.message || 'Search failed', + } + } + const items = (data.items || []).map((item: any) => ({ - videoId: item.id?.videoId, - title: item.snippet?.title, - description: item.snippet?.description, + videoId: item.id?.videoId ?? '', + title: item.snippet?.title ?? '', + description: item.snippet?.description ?? '', thumbnail: item.snippet?.thumbnails?.default?.url || item.snippet?.thumbnails?.medium?.url || item.snippet?.thumbnails?.high?.url || '', + channelId: item.snippet?.channelId ?? '', + channelTitle: item.snippet?.channelTitle ?? '', + publishedAt: item.snippet?.publishedAt ?? '', + liveBroadcastContent: item.snippet?.liveBroadcastContent ?? 'none', })) return { success: true, output: { items, totalResults: data.pageInfo?.totalResults || 0, - nextPageToken: data.nextPageToken, + nextPageToken: data.nextPageToken ?? null, }, } }, @@ -188,6 +220,13 @@ export const youtubeSearchTool: ToolConfig = { + id: 'youtube_trending', + name: 'YouTube Trending Videos', + description: + 'Get the most popular/trending videos on YouTube. Can filter by region and video category.', + version: '1.0.0', + params: { + regionCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'ISO 3166-1 alpha-2 country code to get trending videos for (e.g., "US", "GB", "JP"). Defaults to US.', + }, + videoCategoryId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by video category ID (e.g., "10" for Music, "20" for Gaming, "17" for Sports)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-only', + default: 10, + description: 'Maximum number of trending videos to return (1-50)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Page token for pagination', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'YouTube API Key', + }, + }, + + request: { + url: (params: YouTubeTrendingParams) => { + let url = `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&chart=mostPopular&key=${params.apiKey}` + url += `&maxResults=${Number(params.maxResults || 10)}` + url += `®ionCode=${params.regionCode || 'US'}` + if (params.videoCategoryId) { + url += `&videoCategoryId=${params.videoCategoryId}` + } + if (params.pageToken) { + url += `&pageToken=${encodeURIComponent(params.pageToken)}` + } + return url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error) { + return { + success: false, + output: { + items: [], + totalResults: 0, + nextPageToken: null, + }, + error: data.error.message || 'Failed to fetch trending videos', + } + } + + const items = (data.items || []).map((item: any) => ({ + videoId: item.id ?? '', + title: item.snippet?.title ?? '', + description: item.snippet?.description ?? '', + thumbnail: + item.snippet?.thumbnails?.high?.url || + item.snippet?.thumbnails?.medium?.url || + item.snippet?.thumbnails?.default?.url || + '', + channelId: item.snippet?.channelId ?? '', + channelTitle: item.snippet?.channelTitle ?? '', + publishedAt: item.snippet?.publishedAt ?? '', + viewCount: Number(item.statistics?.viewCount || 0), + likeCount: Number(item.statistics?.likeCount || 0), + commentCount: Number(item.statistics?.commentCount || 0), + duration: item.contentDetails?.duration ?? '', + })) + + return { + success: true, + output: { + items, + totalResults: data.pageInfo?.totalResults || items.length, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of trending videos', + items: { + type: 'object', + properties: { + videoId: { type: 'string', description: 'YouTube video ID' }, + title: { type: 'string', description: 'Video title' }, + description: { type: 'string', description: 'Video description' }, + thumbnail: { type: 'string', description: 'Video thumbnail URL' }, + channelId: { type: 'string', description: 'Channel ID' }, + channelTitle: { type: 'string', description: 'Channel name' }, + publishedAt: { type: 'string', description: 'Video publish date' }, + viewCount: { type: 'number', description: 'Number of views' }, + likeCount: { type: 'number', description: 'Number of likes' }, + commentCount: { type: 'number', description: 'Number of comments' }, + duration: { type: 'string', description: 'Video duration in ISO 8601 format' }, + }, + }, + }, + totalResults: { + type: 'number', + description: 'Total number of trending videos available', + }, + nextPageToken: { + type: 'string', + description: 'Token for accessing the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/youtube/types.ts b/apps/sim/tools/youtube/types.ts index a93129929..d33195dc3 100644 --- a/apps/sim/tools/youtube/types.ts +++ b/apps/sim/tools/youtube/types.ts @@ -16,6 +16,7 @@ export interface YouTubeSearchParams { regionCode?: string relevanceLanguage?: string safeSearch?: 'moderate' | 'none' | 'strict' + eventType?: 'completed' | 'live' | 'upcoming' } export interface YouTubeSearchResponse extends ToolResponse { @@ -25,9 +26,13 @@ export interface YouTubeSearchResponse extends ToolResponse { title: string description: string thumbnail: string + channelId: string + channelTitle: string + publishedAt: string + liveBroadcastContent: string }> totalResults: number - nextPageToken?: string + nextPageToken?: string | null } } @@ -48,8 +53,24 @@ export interface YouTubeVideoDetailsResponse extends ToolResponse { viewCount: number likeCount: number commentCount: number + favoriteCount: number thumbnail: string - tags?: string[] + tags: string[] + categoryId: string | null + definition: string | null + caption: string | null + licensedContent: boolean | null + privacyStatus: string | null + liveBroadcastContent: string | null + defaultLanguage: string | null + defaultAudioLanguage: string | null + // Live streaming details + isLiveContent: boolean + scheduledStartTime: string | null + actualStartTime: string | null + actualEndTime: string | null + concurrentViewers: number | null + activeLiveChatId: string | null } } @@ -69,7 +90,11 @@ export interface YouTubeChannelInfoResponse extends ToolResponse { viewCount: number publishedAt: string thumbnail: string - customUrl?: string + customUrl: string | null + country: string | null + uploadsPlaylistId: string | null + bannerImageUrl: string | null + hiddenSubscriberCount: boolean } } @@ -90,9 +115,11 @@ export interface YouTubePlaylistItemsResponse extends ToolResponse { publishedAt: string channelTitle: string position: number + videoOwnerChannelId: string | null + videoOwnerChannelTitle: string | null }> totalResults: number - nextPageToken?: string + nextPageToken?: string | null } } @@ -110,15 +137,16 @@ export interface YouTubeCommentsResponse extends ToolResponse { commentId: string authorDisplayName: string authorChannelUrl: string + authorProfileImageUrl: string textDisplay: string textOriginal: string likeCount: number publishedAt: string updatedAt: string - replyCount?: number + replyCount: number }> totalResults: number - nextPageToken?: string + nextPageToken?: string | null } } @@ -138,9 +166,10 @@ export interface YouTubeChannelVideosResponse extends ToolResponse { description: string thumbnail: string publishedAt: string + channelTitle: string }> totalResults: number - nextPageToken?: string + nextPageToken?: string | null } } @@ -160,9 +189,55 @@ export interface YouTubeChannelPlaylistsResponse extends ToolResponse { thumbnail: string itemCount: number publishedAt: string + channelTitle: string + }> + totalResults: number + nextPageToken?: string | null + } +} + +export interface YouTubeTrendingParams { + apiKey: string + regionCode?: string + videoCategoryId?: string + maxResults?: number + pageToken?: string +} + +export interface YouTubeTrendingResponse extends ToolResponse { + output: { + items: Array<{ + videoId: string + title: string + description: string + thumbnail: string + channelId: string + channelTitle: string + publishedAt: string + viewCount: number + likeCount: number + commentCount: number + duration: string + }> + totalResults: number + nextPageToken?: string | null + } +} + +export interface YouTubeVideoCategoriesParams { + apiKey: string + regionCode?: string + hl?: string +} + +export interface YouTubeVideoCategoriesResponse extends ToolResponse { + output: { + items: Array<{ + categoryId: string + title: string + assignable: boolean }> totalResults: number - nextPageToken?: string } } @@ -174,3 +249,5 @@ export type YouTubeResponse = | YouTubeCommentsResponse | YouTubeChannelVideosResponse | YouTubeChannelPlaylistsResponse + | YouTubeTrendingResponse + | YouTubeVideoCategoriesResponse diff --git a/apps/sim/tools/youtube/video_categories.ts b/apps/sim/tools/youtube/video_categories.ts new file mode 100644 index 000000000..95a5311bb --- /dev/null +++ b/apps/sim/tools/youtube/video_categories.ts @@ -0,0 +1,108 @@ +import type { ToolConfig } from '@/tools/types' +import type { + YouTubeVideoCategoriesParams, + YouTubeVideoCategoriesResponse, +} from '@/tools/youtube/types' + +export const youtubeVideoCategoriesTool: ToolConfig< + YouTubeVideoCategoriesParams, + YouTubeVideoCategoriesResponse +> = { + id: 'youtube_video_categories', + name: 'YouTube Video Categories', + description: + 'Get a list of video categories available on YouTube. Use this to discover valid category IDs for filtering search and trending results.', + version: '1.0.0', + params: { + regionCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'ISO 3166-1 alpha-2 country code to get categories for (e.g., "US", "GB", "JP"). Defaults to US.', + }, + hl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Language for category titles (e.g., "en", "es", "fr"). Defaults to English.', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'YouTube API Key', + }, + }, + + request: { + url: (params: YouTubeVideoCategoriesParams) => { + let url = `https://www.googleapis.com/youtube/v3/videoCategories?part=snippet&key=${params.apiKey}` + url += `®ionCode=${params.regionCode || 'US'}` + if (params.hl) { + url += `&hl=${params.hl}` + } + return url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.error) { + return { + success: false, + output: { + items: [], + totalResults: 0, + }, + error: data.error.message || 'Failed to fetch video categories', + } + } + + const items = (data.items || []) + .filter((item: any) => item.snippet?.assignable !== false) + .map((item: any) => ({ + categoryId: item.id ?? '', + title: item.snippet?.title ?? '', + assignable: item.snippet?.assignable ?? false, + })) + + return { + success: true, + output: { + items, + totalResults: items.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of video categories available in the specified region', + items: { + type: 'object', + properties: { + categoryId: { + type: 'string', + description: 'Category ID to use in search/trending filters (e.g., "10" for Music)', + }, + title: { type: 'string', description: 'Human-readable category name' }, + assignable: { + type: 'boolean', + description: 'Whether videos can be tagged with this category', + }, + }, + }, + }, + totalResults: { + type: 'number', + description: 'Total number of categories available', + }, + }, +} diff --git a/apps/sim/tools/youtube/video_details.ts b/apps/sim/tools/youtube/video_details.ts index 78d2c785c..a4027a6c3 100644 --- a/apps/sim/tools/youtube/video_details.ts +++ b/apps/sim/tools/youtube/video_details.ts @@ -7,8 +7,9 @@ export const youtubeVideoDetailsTool: ToolConfig< > = { id: 'youtube_video_details', name: 'YouTube Video Details', - description: 'Get detailed information about a specific YouTube video.', - version: '1.0.0', + description: + 'Get detailed information about a specific YouTube video including statistics, content details, live streaming info, and metadata.', + version: '1.2.0', params: { videoId: { type: 'string', @@ -26,7 +27,7 @@ export const youtubeVideoDetailsTool: ToolConfig< request: { url: (params: YouTubeVideoDetailsParams) => { - return `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id=${params.videoId}&key=${params.apiKey}` + return `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails,status,liveStreamingDetails&id=${encodeURIComponent(params.videoId)}&key=${params.apiKey}` }, method: 'GET', headers: () => ({ @@ -51,32 +52,68 @@ export const youtubeVideoDetailsTool: ToolConfig< viewCount: 0, likeCount: 0, commentCount: 0, + favoriteCount: 0, thumbnail: '', + tags: [], + categoryId: null, + definition: null, + caption: null, + licensedContent: null, + privacyStatus: null, + liveBroadcastContent: null, + defaultLanguage: null, + defaultAudioLanguage: null, + isLiveContent: false, + scheduledStartTime: null, + actualStartTime: null, + actualEndTime: null, + concurrentViewers: null, + activeLiveChatId: null, }, error: 'Video not found', } } const item = data.items[0] + const liveDetails = item.liveStreamingDetails + return { success: true, output: { - videoId: item.id, - title: item.snippet?.title || '', - description: item.snippet?.description || '', - channelId: item.snippet?.channelId || '', - channelTitle: item.snippet?.channelTitle || '', - publishedAt: item.snippet?.publishedAt || '', - duration: item.contentDetails?.duration || '', + videoId: item.id ?? '', + title: item.snippet?.title ?? '', + description: item.snippet?.description ?? '', + channelId: item.snippet?.channelId ?? '', + channelTitle: item.snippet?.channelTitle ?? '', + publishedAt: item.snippet?.publishedAt ?? '', + duration: item.contentDetails?.duration ?? '', viewCount: Number(item.statistics?.viewCount || 0), likeCount: Number(item.statistics?.likeCount || 0), commentCount: Number(item.statistics?.commentCount || 0), + favoriteCount: Number(item.statistics?.favoriteCount || 0), thumbnail: item.snippet?.thumbnails?.high?.url || item.snippet?.thumbnails?.medium?.url || item.snippet?.thumbnails?.default?.url || '', - tags: item.snippet?.tags || [], + tags: item.snippet?.tags ?? [], + categoryId: item.snippet?.categoryId ?? null, + definition: item.contentDetails?.definition ?? null, + caption: item.contentDetails?.caption ?? null, + licensedContent: item.contentDetails?.licensedContent ?? null, + privacyStatus: item.status?.privacyStatus ?? null, + liveBroadcastContent: item.snippet?.liveBroadcastContent ?? null, + defaultLanguage: item.snippet?.defaultLanguage ?? null, + defaultAudioLanguage: item.snippet?.defaultAudioLanguage ?? null, + // Live streaming details + isLiveContent: liveDetails !== undefined, + scheduledStartTime: liveDetails?.scheduledStartTime ?? null, + actualStartTime: liveDetails?.actualStartTime ?? null, + actualEndTime: liveDetails?.actualEndTime ?? null, + concurrentViewers: liveDetails?.concurrentViewers + ? Number(liveDetails.concurrentViewers) + : null, + activeLiveChatId: liveDetails?.activeLiveChatId ?? null, }, } }, @@ -108,7 +145,7 @@ export const youtubeVideoDetailsTool: ToolConfig< }, duration: { type: 'string', - description: 'Video duration in ISO 8601 format', + description: 'Video duration in ISO 8601 format (e.g., "PT4M13S" for 4 min 13 sec)', }, viewCount: { type: 'number', @@ -122,6 +159,10 @@ export const youtubeVideoDetailsTool: ToolConfig< type: 'number', description: 'Number of comments', }, + favoriteCount: { + type: 'number', + description: 'Number of times added to favorites', + }, thumbnail: { type: 'string', description: 'Video thumbnail URL', @@ -132,6 +173,74 @@ export const youtubeVideoDetailsTool: ToolConfig< items: { type: 'string', }, + }, + categoryId: { + type: 'string', + description: 'YouTube video category ID', + optional: true, + }, + definition: { + type: 'string', + description: 'Video definition: "hd" or "sd"', + optional: true, + }, + caption: { + type: 'string', + description: 'Whether captions are available: "true" or "false"', + optional: true, + }, + licensedContent: { + type: 'boolean', + description: 'Whether the video is licensed content', + optional: true, + }, + privacyStatus: { + type: 'string', + description: 'Video privacy status: "public", "private", or "unlisted"', + optional: true, + }, + liveBroadcastContent: { + type: 'string', + description: 'Live broadcast status: "live", "upcoming", or "none"', + optional: true, + }, + defaultLanguage: { + type: 'string', + description: 'Default language of the video metadata', + optional: true, + }, + defaultAudioLanguage: { + type: 'string', + description: 'Default audio language of the video', + optional: true, + }, + isLiveContent: { + type: 'boolean', + description: 'Whether this video is or was a live stream', + }, + scheduledStartTime: { + type: 'string', + description: 'Scheduled start time for upcoming live streams (ISO 8601)', + optional: true, + }, + actualStartTime: { + type: 'string', + description: 'When the live stream actually started (ISO 8601)', + optional: true, + }, + actualEndTime: { + type: 'string', + description: 'When the live stream ended (ISO 8601)', + optional: true, + }, + concurrentViewers: { + type: 'number', + description: 'Current number of viewers (only for active live streams)', + optional: true, + }, + activeLiveChatId: { + type: 'string', + description: 'Live chat ID for the stream (only for active live streams)', optional: true, }, },