@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
JavaScript
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