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.

196 lines 8.66 kB
import { getCookieArgs } from "../config.js"; 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 if (!validateUrl(url)) { throw new Error("Invalid or unsupported URL format"); } const args = [ "--dump-json", "--no-warnings", "--no-check-certificate", ...(_config ? getCookieArgs(_config) : []), 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]; } } let result = JSON.stringify(filteredMetadata, null, 2); // Check character limit if (_config && result.length > _config.limits.characterLimit) { // Add truncation info inside JSON before truncating filteredMetadata._truncated = true; filteredMetadata._message = "Response truncated. Specify fewer fields to see complete data."; result = JSON.stringify(filteredMetadata, null, 2); // If still too long, truncate the string content if (result.length > _config.limits.characterLimit) { result = result.substring(0, _config.limits.characterLimit) + '\n... }'; } } return result; } // Return formatted JSON string with all metadata let result = JSON.stringify(metadata, null, 2); // Check character limit for full metadata if (_config && result.length > _config.limits.characterLimit) { // Try to return essential fields only const essentialFields = ['id', 'title', 'description', 'channel', 'channel_id', 'uploader', 'duration', 'duration_string', 'view_count', 'like_count', 'upload_date', 'tags', 'categories', 'webpage_url']; const essentialMetadata = {}; for (const field of essentialFields) { if (metadata.hasOwnProperty(field)) { essentialMetadata[field] = metadata[field]; } } // Add truncation info inside the JSON object essentialMetadata._truncated = true; essentialMetadata._message = 'Full metadata truncated to essential fields. Use the "fields" parameter to request specific fields.'; result = JSON.stringify(essentialMetadata, null, 2); } return result; } catch (error) { if (error instanceof Error) { // Handle common yt-dlp errors with actionable messages if (error.message.includes("Video unavailable") || error.message.includes("private")) { throw new Error(`Video is unavailable or private: ${url}. Check the URL and video privacy settings.`); } else if (error.message.includes("Unsupported URL") || error.message.includes("extractor")) { throw new Error(`Unsupported platform or video URL: ${url}. Ensure the URL is from a supported platform like YouTube.`); } else if (error.message.includes("network") || error.message.includes("Connection")) { throw new Error("Network error while extracting metadata. Check your internet connection and retry."); } else { throw new Error(`Failed to extract video metadata: ${error.message}. Verify the URL is correct.`); } } 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) { try { // 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'); } catch (error) { // Re-throw errors from getVideoMetadata with context if (error instanceof Error) { throw error; } throw new Error(`Failed to generate metadata summary for ${url}`); } } //# sourceMappingURL=metadata.js.map