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.

255 lines 9.49 kB
import * as os from "os"; import * as path from "path"; import * as fs from "fs"; /** * Valid browser names for cookie extraction */ export const VALID_BROWSERS = [ 'brave', 'chrome', 'chromium', 'edge', 'firefox', 'opera', 'safari', 'vivaldi', 'whale' ]; /** * Default configuration */ const defaultConfig = { file: { maxFilenameLength: 50, downloadsDir: path.join(os.homedir(), "Downloads"), tempDirPrefix: "ytdlp-", sanitize: { replaceChar: '_', truncateSuffix: '...', illegalChars: /[<>:"/\\|?*\x00-\x1F]/g, // Windows illegal characters reservedNames: [ 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9' ] } }, tools: { required: ['yt-dlp'] }, download: { defaultResolution: "720p", defaultAudioFormat: "m4a", defaultSubtitleLanguage: "en" }, limits: { characterLimit: 25000, // Standard MCP character limit maxTranscriptLength: 50000 // Transcripts can be larger }, cookies: { file: undefined, fromBrowser: undefined } }; /** * Load configuration from environment variables */ function loadEnvConfig() { const envConfig = {}; // File configuration const fileConfig = { sanitize: { replaceChar: process.env.YTDLP_SANITIZE_REPLACE_CHAR, truncateSuffix: process.env.YTDLP_SANITIZE_TRUNCATE_SUFFIX, illegalChars: (() => { if (!process.env.YTDLP_SANITIZE_ILLEGAL_CHARS) return undefined; try { return new RegExp(process.env.YTDLP_SANITIZE_ILLEGAL_CHARS); } catch { console.warn('[yt-dlp-mcp] Invalid regex in YTDLP_SANITIZE_ILLEGAL_CHARS, using default'); return undefined; } })(), reservedNames: process.env.YTDLP_SANITIZE_RESERVED_NAMES?.split(',') } }; if (process.env.YTDLP_MAX_FILENAME_LENGTH) { const parsed = parseInt(process.env.YTDLP_MAX_FILENAME_LENGTH, 10); if (!isNaN(parsed) && parsed >= 5) { fileConfig.maxFilenameLength = parsed; } else { console.warn('[yt-dlp-mcp] Invalid YTDLP_MAX_FILENAME_LENGTH, using default'); } } if (process.env.YTDLP_DOWNLOADS_DIR) { fileConfig.downloadsDir = process.env.YTDLP_DOWNLOADS_DIR; } if (process.env.YTDLP_TEMP_DIR_PREFIX) { fileConfig.tempDirPrefix = process.env.YTDLP_TEMP_DIR_PREFIX; } if (Object.keys(fileConfig).length > 0) { envConfig.file = fileConfig; } // Download configuration const downloadConfig = {}; if (process.env.YTDLP_DEFAULT_RESOLUTION && ['480p', '720p', '1080p', 'best'].includes(process.env.YTDLP_DEFAULT_RESOLUTION)) { downloadConfig.defaultResolution = process.env.YTDLP_DEFAULT_RESOLUTION; } if (process.env.YTDLP_DEFAULT_AUDIO_FORMAT && ['m4a', 'mp3'].includes(process.env.YTDLP_DEFAULT_AUDIO_FORMAT)) { downloadConfig.defaultAudioFormat = process.env.YTDLP_DEFAULT_AUDIO_FORMAT; } if (process.env.YTDLP_DEFAULT_SUBTITLE_LANG) { downloadConfig.defaultSubtitleLanguage = process.env.YTDLP_DEFAULT_SUBTITLE_LANG; } if (Object.keys(downloadConfig).length > 0) { envConfig.download = downloadConfig; } // Cookie configuration const cookiesConfig = {}; if (process.env.YTDLP_COOKIES_FILE) { cookiesConfig.file = process.env.YTDLP_COOKIES_FILE; } if (process.env.YTDLP_COOKIES_FROM_BROWSER) { cookiesConfig.fromBrowser = process.env.YTDLP_COOKIES_FROM_BROWSER; } if (Object.keys(cookiesConfig).length > 0) { envConfig.cookies = cookiesConfig; } return envConfig; } /** * Validate configuration */ function validateConfig(config) { // Validate filename length if (config.file.maxFilenameLength < 5) { throw new Error('maxFilenameLength must be at least 5'); } // Validate downloads directory if (!config.file.downloadsDir) { throw new Error('downloadsDir must be specified'); } // Validate temporary directory prefix if (!config.file.tempDirPrefix) { throw new Error('tempDirPrefix must be specified'); } // Validate default resolution if (!['480p', '720p', '1080p', 'best'].includes(config.download.defaultResolution)) { throw new Error('Invalid defaultResolution'); } // Validate default audio format if (!['m4a', 'mp3'].includes(config.download.defaultAudioFormat)) { throw new Error('Invalid defaultAudioFormat'); } // Validate default subtitle language if (!/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/i.test(config.download.defaultSubtitleLanguage)) { throw new Error('Invalid defaultSubtitleLanguage'); } // Validate cookies (lenient - warnings only) validateCookiesConfig(config); } /** * Validate cookie configuration (lenient - logs warnings but doesn't throw) */ function validateCookiesConfig(config) { // Validate cookie file path if (config.cookies.file) { if (!fs.existsSync(config.cookies.file)) { console.warn(`[yt-dlp-mcp] Cookie file not found: ${config.cookies.file}, continuing without cookies`); config.cookies.file = undefined; } } // Validate browser name only // Format: BROWSER[:PROFILE_OR_PATH][::CONTAINER] // We only validate browser name; yt-dlp will validate path/container if (config.cookies.fromBrowser) { const browserName = config.cookies.fromBrowser.split(':')[0].toLowerCase(); if (!VALID_BROWSERS.includes(browserName)) { console.warn(`[yt-dlp-mcp] Invalid browser name: ${browserName}. Valid browsers: ${VALID_BROWSERS.join(', ')}`); config.cookies.fromBrowser = undefined; } } } /** * Merge configuration */ function mergeConfig(base, override) { return { file: { maxFilenameLength: override.file?.maxFilenameLength || base.file.maxFilenameLength, downloadsDir: override.file?.downloadsDir || base.file.downloadsDir, tempDirPrefix: override.file?.tempDirPrefix || base.file.tempDirPrefix, sanitize: { replaceChar: override.file?.sanitize?.replaceChar || base.file.sanitize.replaceChar, truncateSuffix: override.file?.sanitize?.truncateSuffix || base.file.sanitize.truncateSuffix, illegalChars: (override.file?.sanitize?.illegalChars || base.file.sanitize.illegalChars), reservedNames: (override.file?.sanitize?.reservedNames || base.file.sanitize.reservedNames) } }, tools: { required: (override.tools?.required || base.tools.required) }, download: { defaultResolution: override.download?.defaultResolution || base.download.defaultResolution, defaultAudioFormat: override.download?.defaultAudioFormat || base.download.defaultAudioFormat, defaultSubtitleLanguage: override.download?.defaultSubtitleLanguage || base.download.defaultSubtitleLanguage }, limits: { characterLimit: override.limits?.characterLimit || base.limits.characterLimit, maxTranscriptLength: override.limits?.maxTranscriptLength || base.limits.maxTranscriptLength }, cookies: { file: override.cookies?.file ?? base.cookies.file, fromBrowser: override.cookies?.fromBrowser ?? base.cookies.fromBrowser } }; } /** * Load configuration */ export function loadConfig() { const envConfig = loadEnvConfig(); const config = mergeConfig(defaultConfig, envConfig); validateConfig(config); return config; } /** * Safe filename processing function */ export function sanitizeFilename(filename, config) { // Remove illegal characters let safe = filename.replace(config.sanitize.illegalChars, config.sanitize.replaceChar); // Check reserved names const basename = path.parse(safe).name.toUpperCase(); if (config.sanitize.reservedNames.includes(basename)) { safe = `_${safe}`; } // Handle length limitation if (safe.length > config.maxFilenameLength) { const ext = path.extname(safe); const name = safe.slice(0, config.maxFilenameLength - ext.length - config.sanitize.truncateSuffix.length); safe = `${name}${config.sanitize.truncateSuffix}${ext}`; } return safe; } /** * Get cookie-related yt-dlp arguments * Priority: file > fromBrowser * @param config Configuration object * @returns Array of yt-dlp arguments for cookie handling */ export function getCookieArgs(config) { // Guard against missing cookies config if (!config.cookies) { return []; } // Cookie file takes precedence over browser extraction if (config.cookies.file) { return ['--cookies', config.cookies.file]; } if (config.cookies.fromBrowser) { return ['--cookies-from-browser', config.cookies.fromBrowser]; } return []; } // Export current configuration instance export const CONFIG = loadConfig(); //# sourceMappingURL=config.js.map