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:
Kevin Watt
2025-07-28 04:21:22 +08:00
committed by GitHub
5 changed files with 160 additions and 19 deletions

View File

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

View File

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

View 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);
});

View File

@@ -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") {

View File

@@ -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)}`);
}