chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { YouTubeChannelInfoParams, YouTubeChannelInfoResponse } from '@/tools/youtube/types'
|
||||
|
||||
export const youtubeChannelInfoTool: ToolConfig<
|
||||
YouTubeChannelInfoParams,
|
||||
YouTubeChannelInfoResponse
|
||||
> = {
|
||||
id: 'youtube_channel_info',
|
||||
name: 'YouTube Channel Info',
|
||||
description:
|
||||
'Get detailed information about a YouTube channel including statistics, branding, and content details.',
|
||||
version: '1.1.0',
|
||||
params: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'YouTube channel ID starting with "UC" (24-character string, use either channelId or username)',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'YouTube channel username (use either channelId or username)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'YouTube API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: YouTubeChannelInfoParams) => {
|
||||
let url =
|
||||
'https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,contentDetails,brandingSettings'
|
||||
if (params.channelId) {
|
||||
url += `&id=${encodeURIComponent(params.channelId)}`
|
||||
} else if (params.username) {
|
||||
url += `&forUsername=${encodeURIComponent(params.username)}`
|
||||
}
|
||||
url += `&key=${params.apiKey}`
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<YouTubeChannelInfoResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.items || data.items.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
channelId: '',
|
||||
title: '',
|
||||
description: '',
|
||||
subscriberCount: 0,
|
||||
videoCount: 0,
|
||||
viewCount: 0,
|
||||
publishedAt: '',
|
||||
thumbnail: '',
|
||||
customUrl: null,
|
||||
country: null,
|
||||
uploadsPlaylistId: null,
|
||||
bannerImageUrl: null,
|
||||
hiddenSubscriberCount: false,
|
||||
},
|
||||
error: 'Channel not found',
|
||||
}
|
||||
}
|
||||
|
||||
const item = data.items[0]
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
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 ?? '',
|
||||
thumbnail:
|
||||
item.snippet?.thumbnails?.high?.url ||
|
||||
item.snippet?.thumbnails?.medium?.url ||
|
||||
item.snippet?.thumbnails?.default?.url ||
|
||||
'',
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
description: 'YouTube channel ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Channel name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Channel description',
|
||||
},
|
||||
subscriberCount: {
|
||||
type: 'number',
|
||||
description: 'Number of subscribers (0 if hidden)',
|
||||
},
|
||||
videoCount: {
|
||||
type: 'number',
|
||||
description: 'Number of public videos',
|
||||
},
|
||||
viewCount: {
|
||||
type: 'number',
|
||||
description: 'Total channel views',
|
||||
},
|
||||
publishedAt: {
|
||||
type: 'string',
|
||||
description: 'Channel creation date',
|
||||
},
|
||||
thumbnail: {
|
||||
type: 'string',
|
||||
description: 'Channel thumbnail/avatar URL',
|
||||
},
|
||||
customUrl: {
|
||||
type: 'string',
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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 public playlists from a specific YouTube channel.',
|
||||
version: '1.1.0',
|
||||
params: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'YouTube channel ID starting with "UC" (24-character string) to get playlists from',
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 10,
|
||||
description: 'Maximum number of playlists to return (1-50)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page token for pagination (from previous response nextPageToken)',
|
||||
},
|
||||
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=${Number(params.maxResults || 10)}`
|
||||
if (params.pageToken) {
|
||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<YouTubeChannelPlaylistsResponse> => {
|
||||
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 playlists',
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.items || data.items.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items: [],
|
||||
totalResults: 0,
|
||||
nextPageToken: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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: Number(item.contentDetails?.itemCount || 0),
|
||||
publishedAt: item.snippet?.publishedAt ?? '',
|
||||
channelTitle: item.snippet?.channelTitle ?? '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items,
|
||||
totalResults: data.pageInfo?.totalResults || items.length,
|
||||
nextPageToken: data.nextPageToken ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
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' },
|
||||
channelTitle: { type: 'string', description: 'Channel name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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:
|
||||
'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',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'YouTube channel ID starting with "UC" (24-character string) to get videos from',
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
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, default), "rating", "relevance", "title", "viewCount"',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page token for pagination (from previous response nextPageToken)',
|
||||
},
|
||||
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=${Number(params.maxResults || 10)}`
|
||||
url += `&order=${params.order || 'date'}`
|
||||
if (params.pageToken) {
|
||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<YouTubeChannelVideosResponse> => {
|
||||
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 ?? '',
|
||||
thumbnail:
|
||||
item.snippet?.thumbnails?.medium?.url ||
|
||||
item.snippet?.thumbnails?.default?.url ||
|
||||
item.snippet?.thumbnails?.high?.url ||
|
||||
'',
|
||||
publishedAt: item.snippet?.publishedAt ?? '',
|
||||
channelTitle: item.snippet?.channelTitle ?? '',
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items,
|
||||
totalResults: data.pageInfo?.totalResults || items.length,
|
||||
nextPageToken: data.nextPageToken ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
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' },
|
||||
channelTitle: { type: 'string', description: 'Channel name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { YouTubeCommentsParams, YouTubeCommentsResponse } from '@/tools/youtube/types'
|
||||
|
||||
export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeCommentsResponse> = {
|
||||
id: 'youtube_comments',
|
||||
name: 'YouTube Video Comments',
|
||||
description: 'Get top-level comments from a YouTube video with author details and engagement.',
|
||||
version: '1.1.0',
|
||||
params: {
|
||||
videoId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'YouTube video ID (11-character string, e.g., "dQw4w9WgXcQ")',
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 20,
|
||||
description: 'Maximum number of comments to return (1-100)',
|
||||
},
|
||||
order: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 'relevance',
|
||||
description: 'Order of comments: "time" (newest first) or "relevance" (most relevant first)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page token for pagination (from previous response nextPageToken)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'YouTube API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: YouTubeCommentsParams) => {
|
||||
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=${encodeURIComponent(params.pageToken)}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<YouTubeCommentsResponse> => {
|
||||
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 ?? '',
|
||||
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),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items,
|
||||
totalResults: data.pageInfo?.totalResults || items.length,
|
||||
nextPageToken: data.nextPageToken ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Array of top-level comments from the video',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
commentId: { type: 'string', description: 'Comment ID' },
|
||||
authorDisplayName: { type: 'string', description: 'Comment author display name' },
|
||||
authorChannelUrl: { type: 'string', description: 'Comment author channel URL' },
|
||||
authorProfileImageUrl: {
|
||||
type: 'string',
|
||||
description: 'Comment author profile image URL',
|
||||
},
|
||||
textDisplay: { type: 'string', description: 'Comment text (HTML formatted)' },
|
||||
textOriginal: { type: 'string', description: 'Comment text (plain text)' },
|
||||
likeCount: { type: 'number', description: 'Number of likes on the comment' },
|
||||
publishedAt: { type: 'string', description: 'When the comment was posted' },
|
||||
updatedAt: { type: 'string', description: 'When the comment was last edited' },
|
||||
replyCount: { type: 'number', description: 'Number of replies to this comment' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of comment threads available',
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for accessing the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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 { youtubeSearchTool } from '@/tools/youtube/search'
|
||||
import { youtubeTrendingTool } from '@/tools/youtube/trending'
|
||||
import { youtubeVideoCategoriesTool } from '@/tools/youtube/video_categories'
|
||||
import { youtubeVideoDetailsTool } from '@/tools/youtube/video_details'
|
||||
|
||||
export { youtubeSearchTool }
|
||||
export { youtubeVideoDetailsTool }
|
||||
export { youtubeChannelInfoTool }
|
||||
export { youtubePlaylistItemsTool }
|
||||
export { youtubeCommentsTool }
|
||||
export { youtubeChannelVideosTool }
|
||||
export { youtubeChannelPlaylistsTool }
|
||||
export { youtubeTrendingTool }
|
||||
export { youtubeVideoCategoriesTool }
|
||||
@@ -0,0 +1,138 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
YouTubePlaylistItemsParams,
|
||||
YouTubePlaylistItemsResponse,
|
||||
} from '@/tools/youtube/types'
|
||||
|
||||
export const youtubePlaylistItemsTool: ToolConfig<
|
||||
YouTubePlaylistItemsParams,
|
||||
YouTubePlaylistItemsResponse
|
||||
> = {
|
||||
id: 'youtube_playlist_items',
|
||||
name: 'YouTube Playlist Items',
|
||||
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 starting with "PL" or "UU" (34-character string). Use uploadsPlaylistId from channel_info to get all channel videos.',
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 10,
|
||||
description: 'Maximum number of videos to return (1-50)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page token for pagination (from previous response nextPageToken)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'YouTube API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: YouTubePlaylistItemsParams) => {
|
||||
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=${encodeURIComponent(params.pageToken)}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<YouTubePlaylistItemsResponse> => {
|
||||
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 ?? '',
|
||||
thumbnail:
|
||||
item.snippet?.thumbnails?.medium?.url ||
|
||||
item.snippet?.thumbnails?.default?.url ||
|
||||
item.snippet?.thumbnails?.high?.url ||
|
||||
'',
|
||||
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 || items.length,
|
||||
nextPageToken: data.nextPageToken ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Array of videos in the playlist',
|
||||
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: 'Date added to 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of items in playlist',
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for accessing the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { YouTubeSearchParams, YouTubeSearchResponse } from '@/tools/youtube/types'
|
||||
|
||||
export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchResponse> = {
|
||||
id: 'youtube_search',
|
||||
name: 'YouTube Search',
|
||||
description:
|
||||
'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.',
|
||||
version: '1.2.0',
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query for YouTube videos',
|
||||
},
|
||||
maxResults: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
default: 5,
|
||||
description: 'Maximum number of videos to return (1-50)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page token for pagination (from previous response nextPageToken)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'YouTube API Key',
|
||||
},
|
||||
channelId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter results to a specific YouTube channel ID starting with "UC" (24-character string)',
|
||||
},
|
||||
publishedAfter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Only return videos published after this date (RFC 3339 format: "2024-01-01T00:00:00Z")',
|
||||
},
|
||||
publishedBefore: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Only return videos published before this date (RFC 3339 format: "2024-01-01T00:00:00Z")',
|
||||
},
|
||||
videoDuration: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter by video length: "short" (<4 min), "medium" (4-20 min), "long" (>20 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). Use video_categories to list IDs.',
|
||||
},
|
||||
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"',
|
||||
},
|
||||
eventType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter by live broadcast status: "live" (currently live), "upcoming" (scheduled), "completed" (past streams)',
|
||||
},
|
||||
regionCode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
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-or-llm',
|
||||
description: 'Return results most relevant to a language (ISO 639-1 code, e.g., "en", "es")',
|
||||
},
|
||||
safeSearch: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content filtering level: "moderate" (default), "none", "strict"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: YouTubeSearchParams) => {
|
||||
let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&key=${params.apiKey}&q=${encodeURIComponent(
|
||||
params.query
|
||||
)}`
|
||||
url += `&maxResults=${Number(params.maxResults || 5)}`
|
||||
|
||||
if (params.pageToken) {
|
||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
||||
}
|
||||
if (params.channelId) {
|
||||
url += `&channelId=${encodeURIComponent(params.channelId)}`
|
||||
}
|
||||
if (params.publishedAfter) {
|
||||
url += `&publishedAfter=${encodeURIComponent(params.publishedAfter)}`
|
||||
}
|
||||
if (params.publishedBefore) {
|
||||
url += `&publishedBefore=${encodeURIComponent(params.publishedBefore)}`
|
||||
}
|
||||
if (params.videoDuration) {
|
||||
url += `&videoDuration=${params.videoDuration}`
|
||||
}
|
||||
if (params.order) {
|
||||
url += `&order=${params.order}`
|
||||
}
|
||||
if (params.videoCategoryId) {
|
||||
url += `&videoCategoryId=${params.videoCategoryId}`
|
||||
}
|
||||
if (params.videoDefinition) {
|
||||
url += `&videoDefinition=${params.videoDefinition}`
|
||||
}
|
||||
if (params.videoCaption) {
|
||||
url += `&videoCaption=${params.videoCaption}`
|
||||
}
|
||||
if (params.eventType) {
|
||||
url += `&eventType=${params.eventType}`
|
||||
}
|
||||
if (params.regionCode) {
|
||||
url += `®ionCode=${params.regionCode}`
|
||||
}
|
||||
if (params.relevanceLanguage) {
|
||||
url += `&relevanceLanguage=${params.relevanceLanguage}`
|
||||
}
|
||||
if (params.safeSearch) {
|
||||
url += `&safeSearch=${params.safeSearch}`
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<YouTubeSearchResponse> => {
|
||||
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 ?? '',
|
||||
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 ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Array of YouTube videos matching the search query',
|
||||
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 that uploaded the video' },
|
||||
channelTitle: { type: 'string', description: 'Channel name' },
|
||||
publishedAt: { type: 'string', description: 'Video publish date' },
|
||||
liveBroadcastContent: {
|
||||
type: 'string',
|
||||
description: 'Live broadcast status: "none", "live", or "upcoming"',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of search results available',
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for accessing the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { YouTubeTrendingParams, YouTubeTrendingResponse } from '@/tools/youtube/types'
|
||||
|
||||
export const youtubeTrendingTool: ToolConfig<YouTubeTrendingParams, YouTubeTrendingResponse> = {
|
||||
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-or-llm',
|
||||
default: 10,
|
||||
description: 'Maximum number of trending videos to return (1-50)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page token for pagination (from previous response nextPageToken)',
|
||||
},
|
||||
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<YouTubeTrendingResponse> => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface YouTubeSearchParams {
|
||||
apiKey: string
|
||||
query: string
|
||||
maxResults?: number
|
||||
pageToken?: string
|
||||
channelId?: string
|
||||
publishedAfter?: string
|
||||
publishedBefore?: string
|
||||
videoDuration?: 'any' | 'short' | 'medium' | 'long'
|
||||
order?: 'date' | 'rating' | 'relevance' | 'title' | 'videoCount' | 'viewCount'
|
||||
videoCategoryId?: string
|
||||
videoDefinition?: 'any' | 'high' | 'standard'
|
||||
videoCaption?: 'any' | 'closedCaption' | 'none'
|
||||
regionCode?: string
|
||||
relevanceLanguage?: string
|
||||
safeSearch?: 'moderate' | 'none' | 'strict'
|
||||
eventType?: 'completed' | 'live' | 'upcoming'
|
||||
}
|
||||
|
||||
export interface YouTubeSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
items: Array<{
|
||||
videoId: string
|
||||
title: string
|
||||
description: string
|
||||
thumbnail: string
|
||||
channelId: string
|
||||
channelTitle: string
|
||||
publishedAt: string
|
||||
liveBroadcastContent: string
|
||||
}>
|
||||
totalResults: number
|
||||
nextPageToken?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface YouTubeVideoDetailsParams {
|
||||
apiKey: string
|
||||
videoId: string
|
||||
}
|
||||
|
||||
export interface YouTubeVideoDetailsResponse extends ToolResponse {
|
||||
output: {
|
||||
videoId: string
|
||||
title: string
|
||||
description: string
|
||||
channelId: string
|
||||
channelTitle: string
|
||||
publishedAt: string
|
||||
duration: string
|
||||
viewCount: number
|
||||
likeCount: number
|
||||
commentCount: number
|
||||
favoriteCount: number
|
||||
thumbnail: 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
|
||||
}
|
||||
}
|
||||
|
||||
export interface YouTubeChannelInfoParams {
|
||||
apiKey: string
|
||||
channelId?: string
|
||||
username?: string
|
||||
}
|
||||
|
||||
export interface YouTubeChannelInfoResponse extends ToolResponse {
|
||||
output: {
|
||||
channelId: string
|
||||
title: string
|
||||
description: string
|
||||
subscriberCount: number
|
||||
videoCount: number
|
||||
viewCount: number
|
||||
publishedAt: string
|
||||
thumbnail: string
|
||||
customUrl: string | null
|
||||
country: string | null
|
||||
uploadsPlaylistId: string | null
|
||||
bannerImageUrl: string | null
|
||||
hiddenSubscriberCount: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface YouTubePlaylistItemsParams {
|
||||
apiKey: string
|
||||
playlistId: string
|
||||
maxResults?: number
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface YouTubePlaylistItemsResponse extends ToolResponse {
|
||||
output: {
|
||||
items: Array<{
|
||||
videoId: string
|
||||
title: string
|
||||
description: string
|
||||
thumbnail: string
|
||||
publishedAt: string
|
||||
channelTitle: string
|
||||
position: number
|
||||
videoOwnerChannelId: string | null
|
||||
videoOwnerChannelTitle: string | null
|
||||
}>
|
||||
totalResults: number
|
||||
nextPageToken?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface YouTubeCommentsParams {
|
||||
apiKey: string
|
||||
videoId: string
|
||||
maxResults?: number
|
||||
order?: 'time' | 'relevance'
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface YouTubeCommentsResponse extends ToolResponse {
|
||||
output: {
|
||||
items: Array<{
|
||||
commentId: string
|
||||
authorDisplayName: string
|
||||
authorChannelUrl: string
|
||||
authorProfileImageUrl: string
|
||||
textDisplay: string
|
||||
textOriginal: string
|
||||
likeCount: number
|
||||
publishedAt: string
|
||||
updatedAt: string
|
||||
replyCount: number
|
||||
}>
|
||||
totalResults: number
|
||||
nextPageToken?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface YouTubeChannelVideosParams {
|
||||
apiKey: string
|
||||
channelId: string
|
||||
maxResults?: number
|
||||
order?: 'date' | 'rating' | 'relevance' | 'title' | 'viewCount'
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface YouTubeChannelVideosResponse extends ToolResponse {
|
||||
output: {
|
||||
items: Array<{
|
||||
videoId: string
|
||||
title: string
|
||||
description: string
|
||||
thumbnail: string
|
||||
publishedAt: string
|
||||
channelTitle: string
|
||||
}>
|
||||
totalResults: number
|
||||
nextPageToken?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export type YouTubeResponse =
|
||||
| YouTubeSearchResponse
|
||||
| YouTubeVideoDetailsResponse
|
||||
| YouTubeChannelInfoResponse
|
||||
| YouTubePlaylistItemsResponse
|
||||
| YouTubeCommentsResponse
|
||||
| YouTubeChannelVideosResponse
|
||||
| YouTubeChannelPlaylistsResponse
|
||||
| YouTubeTrendingResponse
|
||||
| YouTubeVideoCategoriesResponse
|
||||
@@ -0,0 +1,109 @@
|
||||
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-or-llm',
|
||||
description:
|
||||
'Language for category titles (ISO 639-1 code, 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<YouTubeVideoCategoriesResponse> => {
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { YouTubeVideoDetailsParams, YouTubeVideoDetailsResponse } from '@/tools/youtube/types'
|
||||
|
||||
export const youtubeVideoDetailsTool: ToolConfig<
|
||||
YouTubeVideoDetailsParams,
|
||||
YouTubeVideoDetailsResponse
|
||||
> = {
|
||||
id: 'youtube_video_details',
|
||||
name: 'YouTube Video Details',
|
||||
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',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'YouTube video ID (11-character string, e.g., "dQw4w9WgXcQ")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'YouTube API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: YouTubeVideoDetailsParams) => {
|
||||
return `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails,status,liveStreamingDetails&id=${encodeURIComponent(params.videoId)}&key=${params.apiKey}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<YouTubeVideoDetailsResponse> => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.items || data.items.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
videoId: '',
|
||||
title: '',
|
||||
description: '',
|
||||
channelId: '',
|
||||
channelTitle: '',
|
||||
publishedAt: '',
|
||||
duration: '',
|
||||
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 ?? '',
|
||||
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 ?? [],
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
videoId: {
|
||||
type: 'string',
|
||||
description: 'YouTube video ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Video title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Video description',
|
||||
},
|
||||
channelId: {
|
||||
type: 'string',
|
||||
description: 'Channel ID',
|
||||
},
|
||||
channelTitle: {
|
||||
type: 'string',
|
||||
description: 'Channel name',
|
||||
},
|
||||
publishedAt: {
|
||||
type: 'string',
|
||||
description: 'Published date and time',
|
||||
},
|
||||
duration: {
|
||||
type: 'string',
|
||||
description: 'Video duration in ISO 8601 format (e.g., "PT4M13S" for 4 min 13 sec)',
|
||||
},
|
||||
viewCount: {
|
||||
type: 'number',
|
||||
description: 'Number of views',
|
||||
},
|
||||
likeCount: {
|
||||
type: 'number',
|
||||
description: 'Number of likes',
|
||||
},
|
||||
commentCount: {
|
||||
type: 'number',
|
||||
description: 'Number of comments',
|
||||
},
|
||||
favoriteCount: {
|
||||
type: 'number',
|
||||
description: 'Number of times added to favorites',
|
||||
},
|
||||
thumbnail: {
|
||||
type: 'string',
|
||||
description: 'Video thumbnail URL',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Video tags',
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user