mirror of
https://github.com/kevinwatt/yt-dlp-mcp.git
synced 2025-08-10 16:09:14 +03:00
Merge pull request #15 from seszele64/implement-trimmed-download
Implement trimmed download Thanks for the great work on this PR! 🙌 The trimmed download feature looks solid - good implementation with proper tests and documentation. This will be really useful for users who need to download video segments. Appreciate the contribution! LGTM 👍
This commit is contained in:
@@ -65,6 +65,8 @@ pip install yt-dlp
|
||||
* Inputs:
|
||||
* `url` (string, required): URL of the video
|
||||
* `resolution` (string, optional): Video resolution ('480p', '720p', '1080p', 'best'). Defaults to '720p'
|
||||
* `startTime` (string, optional): Start time for trimming (format: HH:MM:SS[.ms]) - e.g., '00:01:30' or '00:01:30.500'
|
||||
* `endTime` (string, optional): End time for trimming (format: HH:MM:SS[.ms]) - e.g., '00:02:45' or '00:02:45.500'
|
||||
|
||||
* **download_audio**
|
||||
* Download audio in best available quality (usually m4a/mp3 format) to user's Downloads folder
|
||||
|
||||
26
docs/api.md
26
docs/api.md
@@ -2,14 +2,16 @@
|
||||
|
||||
## Video Operations
|
||||
|
||||
### downloadVideo(url: string, config?: Config, resolution?: string): Promise<string>
|
||||
### downloadVideo(url: string, config?: Config, resolution?: string, startTime?: string, endTime?: string): Promise<string>
|
||||
|
||||
Downloads a video from the specified URL.
|
||||
Downloads a video from the specified URL with optional trimming.
|
||||
|
||||
**Parameters:**
|
||||
- `url`: The URL of the video to download
|
||||
- `config`: (Optional) Configuration object
|
||||
- `resolution`: (Optional) Preferred video resolution ('480p', '720p', '1080p', 'best')
|
||||
- `startTime`: (Optional) Start time for trimming (format: HH:MM:SS[.ms])
|
||||
- `endTime`: (Optional) End time for trimming (format: HH:MM:SS[.ms])
|
||||
|
||||
**Returns:**
|
||||
- Promise resolving to a success message with the downloaded file path
|
||||
@@ -29,6 +31,26 @@ const hdResult = await downloadVideo(
|
||||
'1080p'
|
||||
);
|
||||
console.log(hdResult);
|
||||
|
||||
// Download with trimming
|
||||
const trimmedResult = await downloadVideo(
|
||||
'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
||||
undefined,
|
||||
'720p',
|
||||
'00:01:30',
|
||||
'00:02:45'
|
||||
);
|
||||
console.log(trimmedResult);
|
||||
|
||||
// Download with fractional seconds
|
||||
const preciseTrim = await downloadVideo(
|
||||
'https://www.youtube.com/watch?v=jNQXAC9IVRw',
|
||||
undefined,
|
||||
'720p',
|
||||
'00:01:30.500',
|
||||
'00:02:45.250'
|
||||
);
|
||||
console.log(preciseTrim);
|
||||
```
|
||||
|
||||
## Audio Operations
|
||||
|
||||
68
src/__tests__/video.test.ts
Normal file
68
src/__tests__/video.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// @ts-nocheck
|
||||
// @jest-environment node
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { downloadVideo } from '../modules/video.js';
|
||||
import { CONFIG } from '../config.js';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// 設置 Python 環境
|
||||
process.env.PYTHONPATH = '';
|
||||
process.env.PYTHONHOME = '';
|
||||
|
||||
describe('downloadVideo with trimming', () => {
|
||||
const testUrl = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
|
||||
const testConfig = {
|
||||
...CONFIG,
|
||||
file: {
|
||||
...CONFIG.file,
|
||||
downloadsDir: path.join(os.tmpdir(), 'yt-dlp-test-downloads'),
|
||||
tempDirPrefix: 'yt-dlp-test-'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.promises.mkdir(testConfig.file.downloadsDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.promises.rm(testConfig.file.downloadsDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('downloads video with start time trimming', async () => {
|
||||
const result = await downloadVideo(testUrl, testConfig, '720p', '00:00:10');
|
||||
expect(result).toContain('Video successfully downloaded');
|
||||
|
||||
const files = await fs.promises.readdir(testConfig.file.downloadsDir);
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
expect(files[0]).toMatch(/\.(mp4|webm|mkv)$/);
|
||||
}, 30000);
|
||||
|
||||
test('downloads video with end time trimming', async () => {
|
||||
const result = await downloadVideo(testUrl, testConfig, '720p', undefined, '00:00:20');
|
||||
expect(result).toContain('Video successfully downloaded');
|
||||
|
||||
const files = await fs.promises.readdir(testConfig.file.downloadsDir);
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
expect(files[0]).toMatch(/\.(mp4|webm|mkv)$/);
|
||||
}, 30000);
|
||||
|
||||
test('downloads video with both start and end time trimming', async () => {
|
||||
const result = await downloadVideo(testUrl, testConfig, '720p', '00:00:10', '00:00:20');
|
||||
expect(result).toContain('Video successfully downloaded');
|
||||
|
||||
const files = await fs.promises.readdir(testConfig.file.downloadsDir);
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
expect(files[0]).toMatch(/\.(mp4|webm|mkv)$/);
|
||||
}, 30000);
|
||||
|
||||
test('downloads video without trimming when no times provided', async () => {
|
||||
const result = await downloadVideo(testUrl, testConfig, '720p');
|
||||
expect(result).toContain('Video successfully downloaded');
|
||||
|
||||
const files = await fs.promises.readdir(testConfig.file.downloadsDir);
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
expect(files[0]).toMatch(/\.(mp4|webm|mkv)$/);
|
||||
}, 30000);
|
||||
});
|
||||
@@ -128,11 +128,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string", description: "URL of the video" },
|
||||
resolution: {
|
||||
type: "string",
|
||||
resolution: {
|
||||
type: "string",
|
||||
description: "Preferred video resolution. For YouTube: '480p', '720p', '1080p', 'best'. For other platforms: '480p' for low quality, '720p'/'1080p' for HD, 'best' for highest quality. Defaults to '720p'",
|
||||
enum: ["480p", "720p", "1080p", "best"]
|
||||
},
|
||||
startTime: {
|
||||
type: "string",
|
||||
description: "Start time for trimming (format: HH:MM:SS[.ms]) - e.g., '00:01:30' or '00:01:30.500'"
|
||||
},
|
||||
endTime: {
|
||||
type: "string",
|
||||
description: "End time for trimming (format: HH:MM:SS[.ms]) - e.g., '00:02:45' or '00:02:45.500'"
|
||||
},
|
||||
},
|
||||
required: ["url"],
|
||||
},
|
||||
@@ -197,10 +205,12 @@ server.setRequestHandler(
|
||||
CallToolRequestSchema,
|
||||
async (request: CallToolRequest) => {
|
||||
const toolName = request.params.name;
|
||||
const args = request.params.arguments as {
|
||||
const args = request.params.arguments as {
|
||||
url: string;
|
||||
language?: string;
|
||||
resolution?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
if (toolName === "list_subtitle_languages") {
|
||||
@@ -215,7 +225,13 @@ server.setRequestHandler(
|
||||
);
|
||||
} else if (toolName === "download_video") {
|
||||
return handleToolExecution(
|
||||
() => downloadVideo(args.url, CONFIG, args.resolution as "480p" | "720p" | "1080p" | "best"),
|
||||
() => downloadVideo(
|
||||
args.url,
|
||||
CONFIG,
|
||||
args.resolution as "480p" | "720p" | "1080p" | "best",
|
||||
args.startTime,
|
||||
args.endTime
|
||||
),
|
||||
"Error downloading video"
|
||||
);
|
||||
} else if (toolName === "download_audio") {
|
||||
|
||||
@@ -11,19 +11,21 @@ import {
|
||||
|
||||
/**
|
||||
* Downloads a video from the specified URL.
|
||||
*
|
||||
*
|
||||
* @param url - The URL of the video to download
|
||||
* @param config - Configuration object for download settings
|
||||
* @param resolution - Preferred video resolution ('480p', '720p', '1080p', 'best')
|
||||
* @param startTime - Optional start time for trimming (format: HH:MM:SS[.ms])
|
||||
* @param endTime - Optional end time for trimming (format: HH:MM:SS[.ms])
|
||||
* @returns Promise resolving to a success message with the downloaded file path
|
||||
* @throws {Error} When URL is invalid or download fails
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Download with default settings
|
||||
* const result = await downloadVideo('https://youtube.com/watch?v=...');
|
||||
* console.log(result);
|
||||
*
|
||||
*
|
||||
* // Download with specific resolution
|
||||
* const hdResult = await downloadVideo(
|
||||
* 'https://youtube.com/watch?v=...',
|
||||
@@ -31,12 +33,24 @@ import {
|
||||
* '1080p'
|
||||
* );
|
||||
* console.log(hdResult);
|
||||
*
|
||||
* // Download with trimming
|
||||
* const trimmedResult = await downloadVideo(
|
||||
* 'https://youtube.com/watch?v=...',
|
||||
* undefined,
|
||||
* '720p',
|
||||
* '00:01:30',
|
||||
* '00:02:45'
|
||||
* );
|
||||
* console.log(trimmedResult);
|
||||
* ```
|
||||
*/
|
||||
export async function downloadVideo(
|
||||
url: string,
|
||||
config: Config,
|
||||
resolution: "480p" | "720p" | "1080p" | "best" = "720p"
|
||||
resolution: "480p" | "720p" | "1080p" | "best" = "720p",
|
||||
startTime?: string,
|
||||
endTime?: string
|
||||
): Promise<string> {
|
||||
const userDownloadsDir = config.file.downloadsDir;
|
||||
|
||||
@@ -103,17 +117,36 @@ export async function downloadVideo(
|
||||
expectedFilename = randomFilename;
|
||||
}
|
||||
|
||||
// Build download arguments
|
||||
const downloadArgs = [
|
||||
"--ignore-config",
|
||||
"--progress",
|
||||
"--newline",
|
||||
"--no-mtime",
|
||||
"-f", format,
|
||||
"--output", outputTemplate
|
||||
];
|
||||
|
||||
// Add trimming parameters if provided
|
||||
if (startTime || endTime) {
|
||||
let downloadSection = "*";
|
||||
|
||||
if (startTime && endTime) {
|
||||
downloadSection = `*${startTime}-${endTime}`;
|
||||
} else if (startTime) {
|
||||
downloadSection = `*${startTime}-`;
|
||||
} else if (endTime) {
|
||||
downloadSection = `*-${endTime}`;
|
||||
}
|
||||
|
||||
downloadArgs.push("--download-sections", downloadSection, "--force-keyframes-at-cuts");
|
||||
}
|
||||
|
||||
downloadArgs.push(url);
|
||||
|
||||
// Download with progress info
|
||||
try {
|
||||
await _spawnPromise("yt-dlp", [
|
||||
"--ignore-config",
|
||||
"--progress",
|
||||
"--newline",
|
||||
"--no-mtime",
|
||||
"-f", format,
|
||||
"--output", outputTemplate,
|
||||
url
|
||||
]);
|
||||
await _spawnPromise("yt-dlp", downloadArgs);
|
||||
} catch (error) {
|
||||
throw new Error(`Download failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user