feat: add list subtitles and resolution control
This commit is contained in:
parent
891654dc08
commit
5d36ffb45a
15
README.md
15
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
|
||||
|
||||
@ -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",
|
||||
|
||||
171
src/index.mts
171
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<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,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user