feat: improve audio download support

- Add support for various audio formats (m4a/mp3)
- Update audio download format selection logic
- Improve error handling and filename display
- Bump version to 0.6.22
This commit is contained in:
kevin
2025-02-21 17:14:28 +08:00
parent 576549bc2c
commit b3e8ed5f58

View File

@@ -14,7 +14,7 @@ import * as path from "path";
import { spawnPromise } from "spawn-rx";
import { rimraf } from "rimraf";
const VERSION = '0.6.21';
const VERSION = '0.6.22';
/**
* System Configuration
@@ -139,6 +139,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ["url"],
},
},
{
name: "download_audio",
description: "Download audio in best available quality (usually m4a/mp3 format) to the user's default Downloads folder (usually ~/Downloads).",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL of the video" },
},
required: ["url"],
},
},
],
};
});
@@ -460,6 +471,110 @@ export async function downloadVideo(url: string, resolution = "720p"): Promise<s
}
}
/**
* Downloads audio from video in m4a format
* @param url The URL of the video
* @returns A detailed success message including the filename
*/
async function downloadAudio(url: string): Promise<string> {
const userDownloadsDir = CONFIG.DOWNLOADS_DIR;
try {
validateUrl(url);
const timestamp = getFormattedTimestamp();
const outputTemplate = path.join(
userDownloadsDir,
`%(title).${CONFIG.MAX_FILENAME_LENGTH}s [%(id)s] ${timestamp}.%(ext)s`
);
let format: string;
if (isYouTubeUrl(url)) {
format = "140/bestaudio[ext=m4a]/bestaudio"; // 優先選擇 m4a
} else {
format = "bestaudio[ext=m4a]/bestaudio[ext=mp3]/bestaudio"; // 優先選擇 m4a/mp3
}
// Get expected filename
let expectedFilename: string;
try {
expectedFilename = await spawnPromise("yt-dlp", [
"--get-filename",
"-f", format,
"--output", outputTemplate,
url
]);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Unsupported URL')) {
throw new VideoDownloadError(
ERROR_CODES.UNSUPPORTED_URL,
'UNSUPPORTED_URL',
error as Error
);
}
if (errorMessage.includes('not available')) {
throw new VideoDownloadError(
ERROR_CODES.VIDEO_UNAVAILABLE,
'VIDEO_UNAVAILABLE',
error as Error
);
}
throw new VideoDownloadError(
ERROR_CODES.UNKNOWN_ERROR,
'UNKNOWN_ERROR',
error as Error
);
}
expectedFilename = expectedFilename.trim();
// Download audio
try {
await spawnPromise("yt-dlp", [
"--progress",
"--newline",
"--no-mtime",
"-f", format,
"--output", outputTemplate,
url
]);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Permission denied')) {
throw new VideoDownloadError(
ERROR_CODES.PERMISSION_ERROR,
'PERMISSION_ERROR',
error as Error
);
}
if (errorMessage.includes('format not available')) {
throw new VideoDownloadError(
ERROR_CODES.FORMAT_ERROR,
'FORMAT_ERROR',
error as Error
);
}
throw new VideoDownloadError(
ERROR_CODES.UNKNOWN_ERROR,
'UNKNOWN_ERROR',
error as Error
);
}
return `Audio successfully downloaded as "${path.basename(expectedFilename)}" to ${userDownloadsDir}`;
} catch (error) {
if (error instanceof VideoDownloadError) {
throw error;
}
throw new VideoDownloadError(
ERROR_CODES.UNKNOWN_ERROR,
'UNKNOWN_ERROR',
error as Error
);
}
}
/**
* Handle tool execution with unified error handling
* @param action Async operation to execute
@@ -514,6 +629,11 @@ server.setRequestHandler(
() => downloadVideo(args.url, args.resolution),
"Error downloading video"
);
} else if (toolName === "download_audio") {
return handleToolExecution(
() => downloadAudio(args.url),
"Error downloading audio"
);
} else {
return {
content: [{ type: "text", text: `Unknown tool: ${toolName}` }],