feat: add list subtitles and resolution control

This commit is contained in:
kevinwatt 2025-02-15 13:57:28 +08:00
parent 891654dc08
commit 5d36ffb45a
3 changed files with 120 additions and 68 deletions

View File

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

View File

@ -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",

View File

@ -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<string> {
// Create a temporary directory for subtitle downloads
async function listSubtitles(url: string): Promise<string> {
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<string> {
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<string> {
async function downloadVideo(url: string, resolution: string = "720p"): Promise<string> {
const userDownloadsDir = path.join(os.homedir(), "Downloads");
try {
@ -115,25 +150,37 @@ async function downloadVideo(url: string): Promise<string> {
.replace('T', '_')
.split('.')[0];
// Map resolution to yt-dlp format
const formatMap: Record<string, string> = {
"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,
};
}