From 060be63115595245534b3ddbb7afb32e12d9e7a7 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 5 Nov 2025 11:16:39 -0800 Subject: [PATCH] feat(tools): added additional youtube search params, get channel playlists videos and related videos tools --- apps/docs/content/docs/en/tools/youtube.mdx | 75 ++++++- apps/sim/blocks/blocks/youtube.ts | 224 +++++++++++++++++++- apps/sim/tools/registry.ts | 6 + apps/sim/tools/youtube/channel_playlists.ts | 123 +++++++++++ apps/sim/tools/youtube/channel_videos.ts | 117 ++++++++++ apps/sim/tools/youtube/index.ts | 6 + apps/sim/tools/youtube/related_videos.ts | 108 ++++++++++ apps/sim/tools/youtube/search.ts | 117 +++++++++- apps/sim/tools/youtube/types.ts | 79 +++++++ 9 files changed, 846 insertions(+), 9 deletions(-) create mode 100644 apps/sim/tools/youtube/channel_playlists.ts create mode 100644 apps/sim/tools/youtube/channel_videos.ts create mode 100644 apps/sim/tools/youtube/related_videos.ts diff --git a/apps/docs/content/docs/en/tools/youtube.mdx b/apps/docs/content/docs/en/tools/youtube.mdx index d2fb678187..d7a26744d5 100644 --- a/apps/docs/content/docs/en/tools/youtube.mdx +++ b/apps/docs/content/docs/en/tools/youtube.mdx @@ -40,7 +40,7 @@ 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 playlist items, and get video comments. +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. @@ -48,15 +48,26 @@ Integrate YouTube into the workflow. Can search for videos, get video details, g ### `youtube_search` -Search for videos on YouTube using the YouTube Data API. +Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, and more. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `query` | string | Yes | Search query for YouTube videos | -| `maxResults` | number | No | Maximum number of videos to return | +| `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 @@ -118,6 +129,45 @@ Get detailed information about a YouTube channel. | `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 | + +### `youtube_channel_playlists` + +Get all playlists from a specific YouTube channel. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `channelId` | string | Yes | YouTube channel ID to get playlists from | +| `maxResults` | number | No | Maximum number of playlists to return \(1-50\) | +| `pageToken` | string | No | Page token for pagination | +| `apiKey` | string | Yes | YouTube API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of playlists from the channel | + ### `youtube_playlist_items` Get videos from a YouTube playlist. @@ -137,6 +187,25 @@ Get videos from a YouTube playlist. | --------- | ---- | ----------- | | `items` | array | Array of videos in the playlist | +### `youtube_related_videos` + +Find videos related to a specific YouTube video. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `videoId` | string | Yes | YouTube video ID to find related videos for | +| `maxResults` | number | No | Maximum number of related videos to return \(1-50\) | +| `pageToken` | string | No | Page token for pagination | +| `apiKey` | string | Yes | YouTube API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of related videos | + ### `youtube_comments` Get comments from a YouTube video. diff --git a/apps/sim/blocks/blocks/youtube.ts b/apps/sim/blocks/blocks/youtube.ts index 7324646c75..20799085a6 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 playlist items, and get video comments.', + '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.', docsLink: 'https://docs.sim.ai/tools/youtube', category: 'tools', bgColor: '#FF0000', @@ -24,7 +24,10 @@ export const YouTubeBlock: BlockConfig = { { label: 'Search Videos', id: 'youtube_search' }, { label: 'Get Video Details', id: 'youtube_video_details' }, { label: 'Get Channel Info', id: 'youtube_channel_info' }, + { label: 'Get Channel Videos', id: 'youtube_channel_videos' }, + { label: 'Get Channel Playlists', id: 'youtube_channel_playlists' }, { label: 'Get Playlist Items', id: 'youtube_playlist_items' }, + { label: 'Get Related Videos', id: 'youtube_related_videos' }, { label: 'Get Video Comments', id: 'youtube_comments' }, ], value: () => 'youtube_search', @@ -50,13 +53,129 @@ export const YouTubeBlock: BlockConfig = { integer: true, condition: { field: 'operation', value: 'youtube_search' }, }, + { + id: 'channelId', + title: 'Filter by Channel ID', + type: 'short-input', + layout: 'full', + placeholder: 'Filter results to a specific channel', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'publishedAfter', + title: 'Published After', + type: 'short-input', + layout: 'half', + placeholder: '2024-01-01T00:00:00Z', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'publishedBefore', + title: 'Published Before', + type: 'short-input', + layout: 'half', + placeholder: '2024-12-31T23:59:59Z', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'videoDuration', + title: 'Video Duration', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Any', id: 'any' }, + { label: 'Short (<4 min)', id: 'short' }, + { label: 'Medium (4-20 min)', id: 'medium' }, + { label: 'Long (>20 min)', id: 'long' }, + ], + value: () => 'any', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'order', + title: 'Sort Order', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Relevance', id: 'relevance' }, + { label: 'Date (Newest First)', id: 'date' }, + { label: 'View Count', id: 'viewCount' }, + { label: 'Rating', id: 'rating' }, + { label: 'Title', id: 'title' }, + ], + value: () => 'relevance', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'videoCategoryId', + title: 'Category ID', + type: 'short-input', + layout: 'half', + placeholder: '10 for Music, 20 for Gaming', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'videoDefinition', + title: 'Video Quality', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Any', id: 'any' }, + { label: 'HD', id: 'high' }, + { label: 'Standard', id: 'standard' }, + ], + value: () => 'any', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'videoCaption', + title: 'Captions', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Any', id: 'any' }, + { label: 'Has Captions', id: 'closedCaption' }, + { label: 'No Captions', id: 'none' }, + ], + value: () => 'any', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'regionCode', + title: 'Region Code', + type: 'short-input', + layout: 'half', + placeholder: 'US, GB, JP', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'relevanceLanguage', + title: 'Language Code', + type: 'short-input', + layout: 'half', + placeholder: 'en, es, fr', + condition: { field: 'operation', value: 'youtube_search' }, + }, + { + id: 'safeSearch', + title: 'Safe Search', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Moderate', id: 'moderate' }, + { label: 'None', id: 'none' }, + { label: 'Strict', id: 'strict' }, + ], + value: () => 'moderate', + condition: { field: 'operation', value: 'youtube_search' }, + }, // Get Video Details operation inputs { id: 'videoId', title: 'Video ID', type: 'short-input', layout: 'full', - placeholder: 'Enter YouTube video ID (e.g., dQw4w9WgXcQ)', + placeholder: 'Enter YouTube video ID', required: true, condition: { field: 'operation', value: 'youtube_video_details' }, }, @@ -77,6 +196,63 @@ export const YouTubeBlock: BlockConfig = { placeholder: 'Enter channel username (if not using channel ID)', condition: { field: 'operation', value: 'youtube_channel_info' }, }, + // Get Channel Videos operation inputs + { + id: 'channelId', + title: 'Channel ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter YouTube channel ID', + required: true, + condition: { field: 'operation', value: 'youtube_channel_videos' }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'slider', + layout: 'half', + min: 1, + max: 50, + step: 1, + integer: true, + condition: { field: 'operation', value: 'youtube_channel_videos' }, + }, + { + id: 'order', + title: 'Sort Order', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Date (Newest First)', id: 'date' }, + { label: 'Relevance', id: 'relevance' }, + { label: 'View Count', id: 'viewCount' }, + { label: 'Rating', id: 'rating' }, + { label: 'Title', id: 'title' }, + ], + value: () => 'date', + condition: { field: 'operation', value: 'youtube_channel_videos' }, + }, + // Get Channel Playlists operation inputs + { + id: 'channelId', + title: 'Channel ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter YouTube channel ID', + required: true, + condition: { field: 'operation', value: 'youtube_channel_playlists' }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'slider', + layout: 'half', + min: 1, + max: 50, + step: 1, + integer: true, + condition: { field: 'operation', value: 'youtube_channel_playlists' }, + }, // Get Playlist Items operation inputs { id: 'playlistId', @@ -98,6 +274,27 @@ export const YouTubeBlock: BlockConfig = { integer: true, condition: { field: 'operation', value: 'youtube_playlist_items' }, }, + // Get Related Videos operation inputs + { + id: 'videoId', + title: 'Video ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter YouTube video ID to find related videos', + required: true, + condition: { field: 'operation', value: 'youtube_related_videos' }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'slider', + layout: 'half', + min: 1, + max: 50, + step: 1, + integer: true, + condition: { field: 'operation', value: 'youtube_related_videos' }, + }, // Get Video Comments operation inputs { id: 'videoId', @@ -147,7 +344,10 @@ export const YouTubeBlock: BlockConfig = { 'youtube_search', 'youtube_video_details', 'youtube_channel_info', + 'youtube_channel_videos', + 'youtube_channel_playlists', 'youtube_playlist_items', + 'youtube_related_videos', 'youtube_comments', ], config: { @@ -164,8 +364,14 @@ export const YouTubeBlock: BlockConfig = { return 'youtube_video_details' case 'youtube_channel_info': return 'youtube_channel_info' + case 'youtube_channel_videos': + return 'youtube_channel_videos' + case 'youtube_channel_playlists': + return 'youtube_channel_playlists' case 'youtube_playlist_items': return 'youtube_playlist_items' + case 'youtube_related_videos': + return 'youtube_related_videos' case 'youtube_comments': return 'youtube_comments' default: @@ -180,6 +386,16 @@ export const YouTubeBlock: BlockConfig = { // Search Videos query: { type: 'string', description: 'Search query' }, maxResults: { type: 'number', description: 'Maximum number of results' }, + // Search Filters + publishedAfter: { type: 'string', description: 'Published after date (RFC 3339)' }, + publishedBefore: { type: 'string', description: 'Published before date (RFC 3339)' }, + videoDuration: { type: 'string', description: 'Video duration filter' }, + videoCategoryId: { type: 'string', description: 'YouTube category ID' }, + videoDefinition: { type: 'string', description: 'Video quality filter' }, + videoCaption: { type: 'string', description: 'Caption availability filter' }, + 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' }, // Video Details & Comments videoId: { type: 'string', description: 'YouTube video ID' }, // Channel Info @@ -187,8 +403,8 @@ export const YouTubeBlock: BlockConfig = { username: { type: 'string', description: 'YouTube channel username' }, // Playlist Items playlistId: { type: 'string', description: 'YouTube playlist ID' }, - // Comments - order: { type: 'string', description: 'Sort order for comments' }, + // Sort Order (used by multiple operations) + order: { type: 'string', description: 'Sort order' }, }, outputs: { // Search Videos & Playlist Items diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 0e70a643b5..dd3b109b4c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -224,8 +224,11 @@ import { workflowExecutorTool } from '@/tools/workflow' import { xReadTool, xSearchTool, xUserTool, xWriteTool } from '@/tools/x' import { youtubeChannelInfoTool, + youtubeChannelPlaylistsTool, + youtubeChannelVideosTool, youtubeCommentsTool, youtubePlaylistItemsTool, + youtubeRelatedVideosTool, youtubeSearchTool, youtubeVideoDetailsTool, } from '@/tools/youtube' @@ -289,6 +292,9 @@ export const tools: Record = { youtube_channel_info: youtubeChannelInfoTool, youtube_playlist_items: youtubePlaylistItemsTool, youtube_comments: youtubeCommentsTool, + youtube_channel_videos: youtubeChannelVideosTool, + youtube_channel_playlists: youtubeChannelPlaylistsTool, + youtube_related_videos: youtubeRelatedVideosTool, notion_read: notionReadTool, notion_read_database: notionReadDatabaseTool, notion_write: notionWriteTool, diff --git a/apps/sim/tools/youtube/channel_playlists.ts b/apps/sim/tools/youtube/channel_playlists.ts new file mode 100644 index 0000000000..fc13c7a36d --- /dev/null +++ b/apps/sim/tools/youtube/channel_playlists.ts @@ -0,0 +1,123 @@ +import type { ToolConfig } from '@/tools/types' +import type { + YouTubeChannelPlaylistsParams, + YouTubeChannelPlaylistsResponse, +} from '@/tools/youtube/types' + +export const youtubeChannelPlaylistsTool: ToolConfig< + YouTubeChannelPlaylistsParams, + YouTubeChannelPlaylistsResponse +> = { + id: 'youtube_channel_playlists', + name: 'YouTube Channel Playlists', + description: 'Get all playlists from a specific YouTube channel.', + version: '1.0.0', + params: { + channelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'YouTube channel ID to get playlists from', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-only', + default: 10, + description: 'Maximum number of playlists 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: YouTubeChannelPlaylistsParams) => { + let url = `https://www.googleapis.com/youtube/v3/playlists?part=snippet,contentDetails&channelId=${encodeURIComponent( + params.channelId + )}&key=${params.apiKey}` + url += `&maxResults=${params.maxResults || 10}` + if (params.pageToken) { + url += `&pageToken=${params.pageToken}` + } + return url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (!data.items) { + return { + success: false, + output: { + items: [], + totalResults: 0, + }, + error: 'No playlists found', + } + } + + const items = (data.items || []).map((item: any) => ({ + 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 || '', + })) + + return { + success: true, + output: { + items, + totalResults: data.pageInfo?.totalResults || 0, + nextPageToken: data.nextPageToken, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of playlists from the channel', + items: { + type: 'object', + properties: { + playlistId: { type: 'string', description: 'YouTube playlist ID' }, + title: { type: 'string', description: 'Playlist title' }, + description: { type: 'string', description: 'Playlist description' }, + thumbnail: { type: 'string', description: 'Playlist thumbnail URL' }, + itemCount: { type: 'number', description: 'Number of videos in playlist' }, + publishedAt: { type: 'string', description: 'Playlist creation date' }, + }, + }, + }, + totalResults: { + type: 'number', + description: 'Total number of playlists in the channel', + }, + nextPageToken: { + type: 'string', + description: 'Token for accessing the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/youtube/channel_videos.ts b/apps/sim/tools/youtube/channel_videos.ts new file mode 100644 index 0000000000..1a6871417a --- /dev/null +++ b/apps/sim/tools/youtube/channel_videos.ts @@ -0,0 +1,117 @@ +import type { ToolConfig } from '@/tools/types' +import type { + YouTubeChannelVideosParams, + YouTubeChannelVideosResponse, +} from '@/tools/youtube/types' + +export const youtubeChannelVideosTool: ToolConfig< + YouTubeChannelVideosParams, + YouTubeChannelVideosResponse +> = { + id: 'youtube_channel_videos', + name: 'YouTube Channel Videos', + description: 'Get all videos from a specific YouTube channel, with sorting options.', + version: '1.0.0', + params: { + channelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'YouTube channel ID to get videos from', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-only', + default: 10, + description: 'Maximum number of videos to return (1-50)', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: "date" (newest first), "rating", "relevance", "title", "viewCount"', + }, + 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: YouTubeChannelVideosParams) => { + let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&channelId=${encodeURIComponent( + params.channelId + )}&key=${params.apiKey}` + url += `&maxResults=${params.maxResults || 10}` + if (params.order) { + url += `&order=${params.order}` + } + if (params.pageToken) { + url += `&pageToken=${params.pageToken}` + } + return url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + const items = (data.items || []).map((item: any) => ({ + 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 || '', + })) + return { + success: true, + output: { + items, + totalResults: data.pageInfo?.totalResults || 0, + nextPageToken: data.nextPageToken, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of videos from the channel', + 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' }, + publishedAt: { type: 'string', description: 'Video publish date' }, + }, + }, + }, + totalResults: { + type: 'number', + description: 'Total number of videos in the channel', + }, + nextPageToken: { + type: 'string', + description: 'Token for accessing the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/youtube/index.ts b/apps/sim/tools/youtube/index.ts index 20d7470c5e..e231fa4268 100644 --- a/apps/sim/tools/youtube/index.ts +++ b/apps/sim/tools/youtube/index.ts @@ -1,6 +1,9 @@ import { youtubeChannelInfoTool } from '@/tools/youtube/channel_info' +import { youtubeChannelPlaylistsTool } from '@/tools/youtube/channel_playlists' +import { youtubeChannelVideosTool } from '@/tools/youtube/channel_videos' import { youtubeCommentsTool } from '@/tools/youtube/comments' import { youtubePlaylistItemsTool } from '@/tools/youtube/playlist_items' +import { youtubeRelatedVideosTool } from '@/tools/youtube/related_videos' import { youtubeSearchTool } from '@/tools/youtube/search' import { youtubeVideoDetailsTool } from '@/tools/youtube/video_details' @@ -9,3 +12,6 @@ export { youtubeVideoDetailsTool } export { youtubeChannelInfoTool } export { youtubePlaylistItemsTool } export { youtubeCommentsTool } +export { youtubeChannelVideosTool } +export { youtubeChannelPlaylistsTool } +export { youtubeRelatedVideosTool } diff --git a/apps/sim/tools/youtube/related_videos.ts b/apps/sim/tools/youtube/related_videos.ts new file mode 100644 index 0000000000..708e8809bd --- /dev/null +++ b/apps/sim/tools/youtube/related_videos.ts @@ -0,0 +1,108 @@ +import type { ToolConfig } from '@/tools/types' +import type { + YouTubeRelatedVideosParams, + YouTubeRelatedVideosResponse, +} from '@/tools/youtube/types' + +export const youtubeRelatedVideosTool: ToolConfig< + YouTubeRelatedVideosParams, + YouTubeRelatedVideosResponse +> = { + id: 'youtube_related_videos', + name: 'YouTube Related Videos', + description: 'Find videos related to a specific YouTube video.', + version: '1.0.0', + params: { + videoId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'YouTube video ID to find related videos for', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-only', + default: 10, + description: 'Maximum number of related 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: YouTubeRelatedVideosParams) => { + let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&relatedToVideoId=${encodeURIComponent( + params.videoId + )}&key=${params.apiKey}` + url += `&maxResults=${params.maxResults || 10}` + if (params.pageToken) { + url += `&pageToken=${params.pageToken}` + } + return url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + const items = (data.items || []).map((item: any) => ({ + 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 || + '', + channelTitle: item.snippet?.channelTitle || '', + })) + return { + success: true, + output: { + items, + totalResults: data.pageInfo?.totalResults || 0, + nextPageToken: data.nextPageToken, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of related 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' }, + channelTitle: { type: 'string', description: 'Channel name' }, + }, + }, + }, + totalResults: { + type: 'number', + description: 'Total number of related videos available', + }, + nextPageToken: { + type: 'string', + description: 'Token for accessing the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/youtube/search.ts b/apps/sim/tools/youtube/search.ts index a0738ab19a..d3ae3dc754 100644 --- a/apps/sim/tools/youtube/search.ts +++ b/apps/sim/tools/youtube/search.ts @@ -4,7 +4,8 @@ import type { YouTubeSearchParams, YouTubeSearchResponse } from '@/tools/youtube export const youtubeSearchTool: ToolConfig = { id: 'youtube_search', name: 'YouTube Search', - description: 'Search for videos on YouTube using the YouTube Data API.', + description: + 'Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, and more.', version: '1.0.0', params: { query: { @@ -18,7 +19,7 @@ export const youtubeSearchTool: ToolConfig20 min), "any"', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Sort results by: "date", "rating", "relevance" (default), "title", "videoCount", "viewCount"', + }, + videoCategoryId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by YouTube category ID (e.g., "10" for Music, "20" for Gaming)', + }, + // Priority 2: Very useful filters + videoDefinition: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by video quality: "high" (HD), "standard", "any"', + }, + videoCaption: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by caption availability: "closedCaption" (has captions), "none" (no captions), "any"', + }, + regionCode: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Return results relevant to a specific region (ISO 3166-1 alpha-2 country code, e.g., "US", "GB")', + }, + relevanceLanguage: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Return results most relevant to a language (ISO 639-1 code, e.g., "en", "es")', + }, + safeSearch: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Content filtering level: "moderate" (default), "none", "strict"', + }, }, request: { @@ -34,6 +109,44 @@ export const youtubeSearchTool: ToolConfig + totalResults: number + nextPageToken?: string + } +} + +export interface YouTubeChannelPlaylistsParams { + apiKey: string + channelId: string + maxResults?: number + pageToken?: string +} + +export interface YouTubeChannelPlaylistsResponse extends ToolResponse { + output: { + items: Array<{ + playlistId: string + title: string + description: string + thumbnail: string + itemCount: number + publishedAt: string + }> + totalResults: number + nextPageToken?: string + } +} + +export interface YouTubeRelatedVideosParams { + apiKey: string + videoId: string + maxResults?: number + pageToken?: string +} + +export interface YouTubeRelatedVideosResponse extends ToolResponse { + output: { + items: Array<{ + videoId: string + title: string + description: string + thumbnail: string + channelTitle: string + }> + totalResults: number + nextPageToken?: string + } +} + export type YouTubeResponse = | YouTubeSearchResponse | YouTubeVideoDetailsResponse | YouTubeChannelInfoResponse | YouTubePlaylistItemsResponse | YouTubeCommentsResponse + | YouTubeChannelVideosResponse + | YouTubeChannelPlaylistsResponse + | YouTubeRelatedVideosResponse