UNPKG

@kevinwatt/yt-dlp-mcp

Version:

An MCP server implementation that integrates with yt-dlp, providing video and audio content download capabilities (e.g. YouTube, Facebook, Tiktok, etc.) for LLMs.

199 lines • 8.75 kB
import { _spawnPromise } from "./utils.js"; import { getCookieArgs } from "../config.js"; /** * Map upload date filter to YouTube's sp parameter * These are base64-encoded protobuf parameters */ const UPLOAD_DATE_FILTER_MAP = { hour: "EgIIAQ%3D%3D", // Last hour today: "EgIIAg%3D%3D", // Today week: "EgIIAw%3D%3D", // This week month: "EgIIBA%3D%3D", // This month year: "EgIIBQ%3D%3D", // This year }; /** * Search YouTube videos * @param query Search keywords * @param maxResults Maximum number of results (1-50) * @param offset Number of results to skip for pagination * @param responseFormat Output format ('json' or 'markdown') * @param config Configuration object * @param uploadDateFilter Optional filter by upload date * @returns Search results formatted as string */ export async function searchVideos(query, maxResults = 10, offset = 0, responseFormat = "markdown", config, uploadDateFilter) { // Validate parameters if (!query || query.trim().length === 0) { throw new Error("Search query cannot be empty"); } if (maxResults < 1 || maxResults > 50) { throw new Error("Number of results must be between 1 and 50"); } if (offset < 0) { throw new Error("Offset cannot be negative"); } const cleanQuery = query.trim(); // Request more results to support offset const totalToFetch = maxResults + offset; try { let args; if (uploadDateFilter && UPLOAD_DATE_FILTER_MAP[uploadDateFilter]) { // Use YouTube URL with sp parameter for date filtering const encodedQuery = encodeURIComponent(cleanQuery); const spParam = UPLOAD_DATE_FILTER_MAP[uploadDateFilter]; const searchUrl = `https://www.youtube.com/results?search_query=${encodedQuery}&sp=${spParam}`; args = [ searchUrl, "--flat-playlist", "--print", "title", "--print", "id", "--print", "uploader", "--print", "duration", "--no-download", "--quiet", "--playlist-end", String(totalToFetch), ...getCookieArgs(config) ]; } else { // Use ytsearch prefix for regular search const searchQuery = `ytsearch${totalToFetch}:${cleanQuery}`; args = [ searchQuery, "--print", "title", "--print", "id", "--print", "uploader", "--print", "duration", "--no-download", "--quiet", ...getCookieArgs(config) ]; } const result = await _spawnPromise(config.tools.required[0], args); if (!result || result.trim().length === 0) { return "No videos found"; } // Parse results const lines = result.trim().split('\n'); const allResults = []; // Each video has 4 lines of data: title, id, uploader, duration for (let i = 0; i < lines.length; i += 4) { if (i + 3 < lines.length) { const title = lines[i]?.trim(); const id = lines[i + 1]?.trim(); const uploader = lines[i + 2]?.trim(); const duration = lines[i + 3]?.trim(); if (title && id) { const url = `https://www.youtube.com/watch?v=${id}`; allResults.push({ title, id, url, uploader: uploader || "Unknown", duration: duration || "Unknown" }); } } } // Apply offset and limit const paginatedResults = allResults.slice(offset, offset + maxResults); const hasMore = allResults.length > offset + maxResults; if (paginatedResults.length === 0) { return "No videos found"; } // Format output based on response format if (responseFormat === "json") { const response = { total: allResults.length, count: paginatedResults.length, offset: offset, videos: paginatedResults, has_more: hasMore, ...(hasMore && { next_offset: offset + maxResults }), ...(uploadDateFilter && { upload_date_filter: uploadDateFilter }) }; let output = JSON.stringify(response, null, 2); // Check character limit if (output.length > config.limits.characterLimit) { // Truncate videos array const truncatedCount = Math.ceil(paginatedResults.length / 2); const truncatedResponse = { ...response, count: truncatedCount, videos: paginatedResults.slice(0, truncatedCount), truncated: true, truncation_message: `Response truncated from ${paginatedResults.length} to ${truncatedCount} results. Use offset parameter or reduce maxResults to see more.` }; output = JSON.stringify(truncatedResponse, null, 2); } return output; } else { // Markdown format let output = `Found ${allResults.length} video${allResults.length > 1 ? 's' : ''} (showing ${paginatedResults.length})`; if (uploadDateFilter) { const filterLabels = { hour: "last hour", today: "today", week: "this week", month: "this month", year: "this year" }; output += ` from ${filterLabels[uploadDateFilter]}`; } output += `:\n\n`; paginatedResults.forEach((video, index) => { output += `${offset + index + 1}. **${video.title}**\n`; output += ` šŸ“ŗ Channel: ${video.uploader}\n`; output += ` ā±ļø Duration: ${video.duration}\n`; output += ` šŸ”— URL: ${video.url}\n`; output += ` šŸ†” ID: ${video.id}\n\n`; }); // Add pagination info if (offset > 0 || hasMore) { output += `\nšŸ“Š Pagination: Showing results ${offset + 1}-${offset + paginatedResults.length} of ${allResults.length}`; if (hasMore) { output += ` (${allResults.length - offset - paginatedResults.length} more available)`; } output += '\n'; } output += "\nšŸ’” You can use any URL to download videos, audio, or subtitles!"; // Check character limit if (output.length > config.limits.characterLimit) { output = output.substring(0, config.limits.characterLimit); output += "\n\nāš ļø Response truncated. Use offset parameter or reduce maxResults to see more results."; } return output; } } catch (error) { if (error instanceof Error) { // Provide more actionable error messages if (error.message.includes("network") || error.message.includes("Network")) { throw new Error("Network error while searching. Check your internet connection and retry."); } if (error.message.includes("429") || error.message.includes("rate limit")) { throw new Error("YouTube rate limit exceeded. Wait 60 seconds before searching again."); } throw new Error(`Search failed: ${error.message}. Try a different query or reduce maxResults.`); } throw new Error(`Error searching videos: ${String(error)}`); } } /** * Search videos on specific platform (future expansion feature) * @param query Search keywords * @param platform Platform name ('youtube', 'bilibili', etc.) * @param maxResults Maximum number of results * @param offset Number of results to skip * @param responseFormat Output format * @param config Configuration object */ export async function searchByPlatform(query, platform = 'youtube', maxResults = 10, offset = 0, responseFormat = "markdown", config) { // Currently only supports YouTube, can be expanded to other platforms in the future if (platform.toLowerCase() !== 'youtube') { throw new Error(`Currently only supports YouTube search, ${platform} is not supported`); } return searchVideos(query, maxResults, offset, responseFormat, config); } //# sourceMappingURL=search.js.map