diff --git a/README.md b/README.md index 718bf50..9e5125a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ An MCP server implementation that integrates with yt-dlp, providing YouTube cont ## Features * **YouTube Subtitles**: Download subtitles in SRT format for LLMs to read -* **Video Download**: Save videos to your Downloads folder +* **Video Download**: Save videos to your Downloads folder with resolution control +* **Privacy-Focused**: Direct download without tracking * **MCP Integration**: Works with Dive and other MCP-compatible LLMs ## Installation @@ -47,22 +48,30 @@ pip install yt-dlp ## Tool Documentation +* **list_youtube_subtitles** + * List all available subtitles for a YouTube video + * Inputs: + * `url` (string, required): URL of the YouTube video + * **download_youtube_srt** * Download YouTube subtitles in SRT format * Inputs: * `url` (string, required): URL of the YouTube video + * `language` (string, optional): Language code (e.g., 'en', 'zh-Hant', 'ja'). Defaults to 'en' * **download_youtube_video** * Download YouTube video to user's Downloads folder * Inputs: * `url` (string, required): URL of the YouTube video + * `resolution` (string, optional): Video resolution ('480p', '720p', '1080p', 'best'). Defaults to '720p' ## Usage Examples Ask your LLM to: ``` -"Download subtitles from this YouTube video: https://youtube.com/watch?v=..." -"Download this YouTube video: https://youtube.com/watch?v=..." +"List available subtitles for this video: https://youtube.com/watch?v=..." +"Download Chinese subtitles from this video: https://youtube.com/watch?v=..." +"Download this video in 1080p: https://youtube.com/watch?v=..." ``` ## Manual Start diff --git a/package.json b/package.json index 21ca914..231a229 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevinwatt/yt-dlp-mcp", - "version": "0.6.8", + "version": "0.6.9", "description": "YouTube yt-dlp MCP Server - Download YouTube content via Model Context Protocol", "keywords": [ "mcp", diff --git a/src/index.mts b/src/index.mts index 734d112..2f20cba 100644 --- a/src/index.mts +++ b/src/index.mts @@ -17,7 +17,7 @@ import { rimraf } from "rimraf"; const server = new Server( { name: "yt-dlp-mcp", - version: "0.6.8", + version: "0.6.9", }, { capabilities: { @@ -33,9 +33,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { - name: "download_youtube_srt", - description: - "Download YouTube subtitles in SRT format so that LLM can read them.", + name: "list_youtube_subtitles", + description: "List all available subtitles for a YouTube video", inputSchema: { type: "object", properties: { @@ -44,6 +43,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { required: ["url"], }, }, + { + name: "download_youtube_srt", + description: "Download YouTube subtitles in SRT format. Default language is English, falls back to available languages.", + inputSchema: { + type: "object", + properties: { + url: { type: "string", description: "URL of the YouTube video" }, + language: { type: "string", description: "Language code (e.g., 'en', 'zh-Hant', 'ja'). Optional, defaults to 'en'" }, + }, + required: ["url"], + }, + }, { name: "download_youtube_video", description: @@ -52,6 +63,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { type: "object", properties: { url: { type: "string", description: "URL of the YouTube video" }, + resolution: { + type: "string", + description: "Video resolution (e.g., '720p', '1080p'). Optional, defaults to '720p'", + enum: ["480p", "720p", "1080p", "best"] + }, }, required: ["url"], }, @@ -61,51 +77,70 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { }); /** - * Downloads YouTube subtitles (SRT format) and returns the concatenated content. - * @param url The URL of the YouTube video. - * @returns Concatenated subtitles text. + * Lists all available subtitles for a YouTube video + * @param url The URL of the YouTube video + * @returns Formatted list of available subtitles */ -async function downloadSubtitles(url: string): Promise { - // Create a temporary directory for subtitle downloads +async function listSubtitles(url: string): Promise { + const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "youtube-")); + + try { + const result = await spawnPromise( + "yt-dlp", + ["--list-subs", "--skip-download", url], + { cwd: tempDirectory } + ); + return result; + } finally { + rimraf.sync(tempDirectory); + } +} + +/** + * Downloads YouTube subtitles in specified language + * @param url The URL of the YouTube video + * @param language The language code for subtitles + * @returns Subtitle content + */ +async function downloadSubtitles(url: string, language: string = "en"): Promise { const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "youtube-")); - // Use yt-dlp to download subtitles without downloading the video - await spawnPromise( - "yt-dlp", - [ - "--write-sub", - "--write-auto-sub", - "--sub-lang", - "en", - "--skip-download", - "--sub-format", - "srt", - url, - ], - { cwd: tempDirectory, detached: true } - ); - - let subtitlesContent = ""; try { + await spawnPromise( + "yt-dlp", + [ + "--write-sub", + "--write-auto-sub", + "--sub-lang", + language, + "--skip-download", + "--sub-format", + "srt", + url, + ], + { cwd: tempDirectory } + ); + + let subtitlesContent = ""; const files = fs.readdirSync(tempDirectory); for (const file of files) { const filePath = path.join(tempDirectory, file); const fileData = fs.readFileSync(filePath, "utf8"); subtitlesContent += `${file}\n====================\n${fileData}\n\n`; } + return subtitlesContent; } finally { - // Clean up the temporary directory after processing rimraf.sync(tempDirectory); } - return subtitlesContent; } /** - * Downloads a YouTube video to the user's default Downloads folder. - * @param url The URL of the YouTube video. - * @returns A detailed success message including the filename. + * Downloads a YouTube video with specified resolution + * @param url The URL of the YouTube video + * @param resolution The desired video resolution + * @returns A detailed success message including the filename */ -async function downloadVideo(url: string): Promise { +async function downloadVideo(url: string, resolution: string = "720p"): Promise { const userDownloadsDir = path.join(os.homedir(), "Downloads"); try { @@ -115,25 +150,37 @@ async function downloadVideo(url: string): Promise { .replace('T', '_') .split('.')[0]; + // Map resolution to yt-dlp format + const formatMap: Record = { + "480p": "bestvideo[height<=480]+bestaudio/best[height<=480]", + "720p": "bestvideo[height<=720]+bestaudio/best[height<=720]", + "1080p": "bestvideo[height<=1080]+bestaudio/best[height<=1080]", + "best": "bestvideo+bestaudio/best" + }; + + const format = formatMap[resolution] || formatMap["720p"]; + const outputTemplate = path.join( userDownloadsDir, - // Limit title length to 50 characters to avoid filename too long error + // Limit title length to 50 characters `%(title).50s [%(id)s] ${timestamp}.%(ext)s` ); // Get expected filename const infoResult = await spawnPromise("yt-dlp", [ "--get-filename", + "-f", format, "--output", outputTemplate, url ]); const expectedFilename = infoResult.trim(); - // Download with progress info and use current time + // Download with progress info await spawnPromise("yt-dlp", [ "--progress", - "--newline", - "--no-mtime", // Don't use video upload time + "--newline", + "--no-mtime", + "-f", format, "--output", outputTemplate, url ]); @@ -151,49 +198,45 @@ server.setRequestHandler( CallToolRequestSchema, async (request: CallToolRequest) => { const toolName = request.params.name; - const args = request.params.arguments as { url: string }; + const args = request.params.arguments as { + url: string; + language?: string; + resolution?: string; + }; - if (toolName === "download_youtube_srt") { + if (toolName === "list_youtube_subtitles") { try { - const subtitles = await downloadSubtitles(args.url); + const subtitlesList = await listSubtitles(args.url); return { - content: [ - { - type: "text", - text: subtitles, - }, - ], + content: [{ type: "text", text: subtitlesList }], }; } catch (error) { return { - content: [ - { - type: "text", - text: `Error downloading subtitles: ${error}`, - }, - ], + content: [{ type: "text", text: `Error listing subtitles: ${error}` }], + isError: true, + }; + } + } else if (toolName === "download_youtube_srt") { + try { + const subtitles = await downloadSubtitles(args.url, args.language); + return { + content: [{ type: "text", text: subtitles }], + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error downloading subtitles: ${error}` }], isError: true, }; } } else if (toolName === "download_youtube_video") { try { - const message = await downloadVideo(args.url); + const message = await downloadVideo(args.url, args.resolution); return { - content: [ - { - type: "text", - text: message, - }, - ], + content: [{ type: "text", text: message }], }; } catch (error) { return { - content: [ - { - type: "text", - text: `Error downloading video: ${error}`, - }, - ], + content: [{ type: "text", text: `Error downloading video: ${error}` }], isError: true, }; }