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.

153 lines 5.99 kB
import { _spawnPromise, validateUrl } from "./utils.js"; /** * Extract video metadata without downloading the actual video content. * Uses yt-dlp's --dump-json flag to get comprehensive metadata. * * @param url - The URL of the video to extract metadata from * @param fields - Optional array of specific fields to extract. If not provided, returns all available metadata * @param config - Configuration object (currently unused but kept for consistency) * @returns Promise resolving to formatted metadata string or JSON object * @throws {Error} When URL is invalid or metadata extraction fails * * @example * ```typescript * // Get all metadata * const metadata = await getVideoMetadata('https://youtube.com/watch?v=...'); * console.log(metadata); * * // Get specific fields only * const specificData = await getVideoMetadata( * 'https://youtube.com/watch?v=...', * ['id', 'title', 'description', 'channel'] * ); * console.log(specificData); * ``` */ export async function getVideoMetadata(url, fields, _config) { // Validate the URL validateUrl(url); const args = [ "--dump-json", "--no-warnings", "--no-check-certificate", url ]; try { // Execute yt-dlp to get metadata const output = await _spawnPromise("yt-dlp", args); // Parse the JSON output const metadata = JSON.parse(output); // If specific fields are requested, filter the metadata if (fields !== undefined && fields.length >= 0) { const filteredMetadata = {}; for (const field of fields) { if (metadata.hasOwnProperty(field)) { filteredMetadata[field] = metadata[field]; } } return JSON.stringify(filteredMetadata, null, 2); } // Return formatted JSON string with all metadata return JSON.stringify(metadata, null, 2); } catch (error) { if (error instanceof Error) { // Handle common yt-dlp errors if (error.message.includes("Video unavailable")) { throw new Error(`Video is unavailable or private: ${url}`); } else if (error.message.includes("Unsupported URL")) { throw new Error(`Unsupported URL or extractor not found: ${url}`); } else if (error.message.includes("network")) { throw new Error(`Network error while extracting metadata: ${error.message}`); } else { throw new Error(`Failed to extract video metadata: ${error.message}`); } } throw new Error(`Failed to extract video metadata from ${url}`); } } /** * Get a human-readable summary of key video metadata fields. * This is useful for quick overview without overwhelming JSON output. * * @param url - The URL of the video to extract metadata from * @param config - Configuration object (currently unused but kept for consistency) * @returns Promise resolving to a formatted summary string * @throws {Error} When URL is invalid or metadata extraction fails * * @example * ```typescript * const summary = await getVideoMetadataSummary('https://youtube.com/watch?v=...'); * console.log(summary); * // Output: * // Title: Example Video Title * // Channel: Example Channel * // Duration: 10:30 * // Views: 1,234,567 * // Upload Date: 2023-12-01 * // Description: This is an example video... * ``` */ export async function getVideoMetadataSummary(url, _config) { // Get the full metadata first const metadataJson = await getVideoMetadata(url, undefined, _config); const metadata = JSON.parse(metadataJson); // Format key fields into a readable summary const lines = []; if (metadata.title) { lines.push(`Title: ${metadata.title}`); } if (metadata.channel) { lines.push(`Channel: ${metadata.channel}`); } if (metadata.uploader && metadata.uploader !== metadata.channel) { lines.push(`Uploader: ${metadata.uploader}`); } if (metadata.duration_string) { lines.push(`Duration: ${metadata.duration_string}`); } else if (metadata.duration) { const hours = Math.floor(metadata.duration / 3600); const minutes = Math.floor((metadata.duration % 3600) / 60); const seconds = metadata.duration % 60; const durationStr = hours > 0 ? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` : `${minutes}:${seconds.toString().padStart(2, '0')}`; lines.push(`Duration: ${durationStr}`); } if (metadata.view_count !== undefined) { lines.push(`Views: ${metadata.view_count.toLocaleString()}`); } if (metadata.like_count !== undefined) { lines.push(`Likes: ${metadata.like_count.toLocaleString()}`); } if (metadata.upload_date) { // Format YYYYMMDD to YYYY-MM-DD const dateStr = metadata.upload_date; if (dateStr.length === 8) { const formatted = `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`; lines.push(`Upload Date: ${formatted}`); } else { lines.push(`Upload Date: ${dateStr}`); } } if (metadata.live_status && metadata.live_status !== 'not_live') { lines.push(`Status: ${metadata.live_status.replace('_', ' ')}`); } if (metadata.tags && metadata.tags.length > 0) { lines.push(`Tags: ${metadata.tags.slice(0, 5).join(', ')}${metadata.tags.length > 5 ? '...' : ''}`); } if (metadata.description) { // Truncate description to first 200 characters const desc = metadata.description.length > 200 ? metadata.description.substring(0, 200) + '...' : metadata.description; lines.push(`Description: ${desc}`); } return lines.join('\n'); } //# sourceMappingURL=metadata.js.map